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: http://blogs.msdn.com/b/exchangedev/archive/2015/01/22/building-demon-or-service-apps-with-office-365-mail-calendar-and-contacts-apis-oauth2-client-credential-flow.aspx
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
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.
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.
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
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