9

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
simondefreeze
  • 189
  • 16
Nick Maher
  • 189
  • 2
  • 8
  • Hey @Nick did you ever get this to work? I'd like to see your code. I'm trying to do something similar in Ruby and can't seem to get it to work even after trying to do what Jason's answer said to do. – Joel Smith Mar 18 '15 at 23:17
  • Hey Joel, just added some more detail to the question, with a gist of my code – Nick Maher Mar 19 '15 at 23:55

4 Answers4

11

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.

Jason Johnston
  • 17,194
  • 2
  • 20
  • 34
  • 4
    Perfect, thanks again Jason. It would be a good idea to document this for people not developing with ADAL. Possibly update the Azure AD client credentials document here: https://msdn.microsoft.com/en-us/library/azure/dn645543.aspx – Nick Maher Feb 06 '15 at 12:07
  • 1
    @NickMaher That document should definitely be updated. I've made a request to Microsoft about this. – Andrew Thaddeus Martin Jun 26 '15 at 19:11
  • @Jason, one question: why this certificate is need when calling Office365 API but it's not needed when calling Graph API? I'm able to auth with `clientId` and `clientSecret` with client credentials grant flow in Graph API, but not in Office365. Why is that? – Krzysztof Wolny Apr 26 '16 at 11:54
  • The Exchange Online service requires a certificate for this flow, so my guess is that even with Graph if you try to access mail, calendar, or contacts with a token obtained with a secret instead of an assertion it will fail. – Jason Johnston Apr 26 '16 at 12:04
5

I just managed to get this working, so I thought I'd throw one more piece of advice into the mix. All the instruction articles out there say that you should add your certificate to the manifest file. I had trouble with that, but here is what I did that finally made it work:

  • In Azure, go to Settings > Management Certificates
  • Upload the public key as a .cer file (google around if you don't know how to convert it). This should be a binary file that your text editor barfs on.
  • Now that it's uploaded, Microsoft will give you the thumbprint. It's in the "Thumbprint" column. But, it's in hex, not base64. So, convert it like this:

    # Hint: use your actual thumbprint, not this fake one
    echo '5292850026FADB09700E7D6C1BCB1CD1F3270BCC' | xxd -r -p | base64
    
  • Finally, use this base64 encoded thumbprint as the value for x5t in the JSON header.

  • Herm, I see the thumbprint, but the column isn't wide enough so it cuts it short and adds ellipses. No obvious way to expand the information... Ideas? – stu Nov 13 '15 at 21:57
  • never mind, I got it... openssl x509 -in server.cer -fingerprint -noout | tr -d ':' | awk -F"=" '{print $2}' | xxd -r -p | base64 – stu Nov 13 '15 at 22:04
1

I added a function in HomeController on the git to demo how to request an access token by hand using client assertion w/o ADAL. It might be easier to port using this: https://github.com/mattleib/o365api-as-apponly-webapp/commit/12d5b6dc66055625683020576139f5771e6059e1

0

Just some additions: The audience claim in the assertion is the same as the endpoint you address with the token request. As Jason correctly identified, this is the token endpoint of AAD: https://login.windows.net/{the tenant you want an app token for}/oauth2/token. Also the nbf and exp are the time you created the assertion in unix epoche time, e.g. in .net you would do something like "WebConvert.EpocTime(DateTime.UtcNow)". For "not before" (nbf) maybe subtract a buffer for clock skew, e.g. 5 minutes; and for expires in (exp) add some time, e.g. 15 minutes (so the assertion remains valid for that time).

Here is a fiddler trace of a token request (raw): POST https://login.windows.net/0e49ef1f-ca07-45f1-b4c0-ac9409d3e576/oauth2/token HTTP/1.1 Content-Type: application/x-www-form-urlencoded client-request-id: a8108f88-275b-424d-ac28-f675aabe548e return-client-request-id: true x-client-SKU: .NET x-client-Ver: 2.12.0.0 x-client-CPU: x64 x-client-OS: Microsoft Windows NT 6.2.9200.0 Host: login.windows.net Content-Length: 983 Expect: 100-continue Connection: Keep-Alive

resource=https%3A%2F%2Fgraph.windows.net%2F&client_id=f17bb8a5-2bef-4ad5-a83f-cd7113449fc2&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&client_assertion=eyJhbGciOiJSUzI1NiIsIng1dCI6ImY4S2JVY0xtMnItS2s4b1Z3ZVZYTFU0NzhJcyJ9.eyJhdWQiOiJodHRwczpcL1wvbG9naW4ud2luZG93cy5uZXRcLzBlNDllZjFmLWNhMDctNDVmMS1iNGMwLWFjOTQwOWQzZTU3Nlwvb2F1dGgyXC90b2tlbiIsImV4cCI6MTQyMjk4NDMzNSwiaXNzIjoiZjE3YmI4YTUtMmJlZi00YWQ1LWE4M2YtY2Q3MTEzNDQ5ZmMyIiwianRpIjoiZTI3OTA5YTctZGYwMC00NjBhLTlmZjctOGZkNDExOWVmNTYzIiwibmJmIjoxNDIyOTgzNzM1LCJzdWIiOiJmMTdiYjhhNS0yYmVmLTRhZDUtYTgzZi1jZDcxMTM0NDlmYzIifQ.g9bo4-lxpNJ4kEOMuQxODU-5iakwSVIzyRQEPLdbpuNn_XD4lcvt2yBIWT12EQaUVKkMyqFrDiIh4Oav565-Po7HfhmSPF3URXVj8Kx5lx17Zh0nWiaNkRXEi1vhwswsfjm1o-8B8LGUJTtT6JXTognrueuSL1aEE_-4qSG1y74aoc949Un1pQCjwuBtao4vs4CPJLu9Y9mVbirVRRtiIfxkUMmzf6yfMtuhugoGmrvUYntUo4x6N2fu4LxGjuIs7czyrMMAmDRo-XK4sAhDo5uof10HKb8ETEU8mhObwNZcz86MYHWbZm3Z_HDOwzC9kA_tp6hWqmlJ3c-gLg5VXA&grant_type=client_credentials

Hope this helps! Good luck!

Matthias

  • 1
    Also note: You can see if your assertion is correct by using http://jwt.calebb.net and paste the base64url encoded value that you generate/send in there. It should successful show content like in Jason's example above. – Matthias Leibmann Feb 06 '15 at 01:57