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
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.