Search code examples
rubyrestssloffice365exchangewebservices

Office 365 Rest API - Daemon week authentication


I am trying to build a Ruby Daemon service to access the Office 365 rest API. It was recently made possible to do this via the OAuth 'client_credentials' flow, as detailed in this blog post: https://learn.microsoft.com/en-us/archive/blogs/exchangedev/building-daemon-or-service-apps-with-office-365-mail-calendar-and-contacts-apis-oauth2-client-credential-flow

I am struggling to generate a valid access token. The token endpoint returns me a JWT however when using this token I received a 401 with this message:

The access token is acquired using an authentication method that is too weak to allow access for this application. Presented auth strength was 1, required is 2

I understand that the client_credentials flow requires you to present a X.509 cert, unfortunately all the examples in the blog post are for C#.

I am using a generated self signed cert and private key to do a client assertion when requesting the token. I followed the steps in the blog post to generate the cert and update the manifest to use this cert.

This is the ruby code for reference:

def request_token
  uri = URI.parse("https://login.windows.net/== TENANT-ID ==/oauth2/token?api-version=1.0")
  https = Net::HTTP.new(uri.host, uri.port)

  req = Net::HTTP::Post.new(uri.request_uri)
  req.set_form_data(
    :grant_type    => 'client_credentials',
    :redirect_uri  => 'http://spready.dev',
    :resource      => 'https://outlook.office365.com/',
    :client_id     => '== Client ID ==',
    :client_secret => '== Client secret =='
  )

  https.use_ssl = true
  https.cert = client_cert
  https.key = client_key
  https.verify_mode = OpenSSL::SSL::VERIFY_PEER

  resp = https.start { |cx| cx.request(req) }

  @access_token = JSON.parse(resp.body)
end

Obviously I have removed certain bits of information for security. Even though it is ruby you can see I am using my cert to validate the client using an SSL connection.

Here's some more infomation on the error:

"x-ms-diagnostics" => "2000010;
    reason=\"The access token is acquired using an authentication method that is too weak to allow access for this application. Presented auth strength was 1, required is 2.\";
    error_category=\"insufficient_auth_strength\"", 
"x-diaginfo"=>"AM3PR01MB0662", 
"x-beserver"=>"AM3PR01MB0662"

Any help would be appreciate.


Edit

For others looking to do something similar in Ruby here's a Gist of the code I use: https://gist.github.com/NGMarmaduke/a088943edbe4e703129d

The example uses a Rails environment but it should be fairly easy to strip out the Rails specific bits.

Remember to replace YOUR CLIENT ID, TENANT_ID and CERT_THUMBPRINT with the correct values and point the cert path and client key methods to the right file path.

Then you can do something like this:

mailbox = OfficeAPI.new("nick@test.com")
messages = mailbox.request_messages

Solution

  • Instead of a client_secret in your request body, you need a client_assertion. This is a bit more complex, but it's the reason you need that certificate.

    Basically you need to build a JSON Web Token and sign it with your certificate using a SHA256 hash. The token is going to look something like this:

    Header:

    { 
      "alg": "RS256",
      "x5t": "..." // THUMBPRINT of Cert
    }
    

    Payload:

    {
      "aud": "https:\\/\\/login.windows.net\\/<The logged in user's tenant ID>\\/oauth2\\/token",
      "exp": 1423168488,
      "iss": "YOUR CLIENT ID",
      "jti": "SOME GUID YOU ASSIGN",
      "nbf": 1423167888,
      "sub": "YOUR CLIENT ID"
    }
    

    If you're still with me, you now need to base64-encode both pieces (separately), then concatenate them with a '.'. So now you should have:

    base64_header.base64_payload
    

    Now you take that string and sign it with your certificate, using a SHA256 hash. Then base64-encode the result of that, url-encode it, then append to the string, so now you have:

    base64_header.base64_payload.base64_signature
    

    Finally, include this in your POST to the token endpoint as the client_assertion parameter, and also include a client_assertion_type parameter set to "urn:ietf:params:oauth:client-assertion-type:jwt-bearer":

    req.set_form_data(
        :grant_type    => 'client_credentials',
        :redirect_uri  => 'http://spready.dev',
        :resource      => 'https://outlook.office365.com/',
        :client_id     => '== Client ID ==',
        :client_assertion_type => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
        :client_assertion => 'base64_header.base64_payload.base64_signature'
      )
    

    I hope that helps! This is all based on my research into how ADAL does it, and I haven't tested it myself in Ruby.