Client Credentials Flow for Mail API in Python

Matthias announced support for the Client Credentials flow in the Mail, Calendar, and Contacts APIs a couple of weeks ago, and since then, we’ve had a lot of questions about implementing it. Matthias published a great sample using .NET and ADAL, but many of you have asked for details on implementing this on other platforms, especially those that don’t have an ADAL library. I enjoyed working with Python so much, I figured I’d try implementing this in Python.

Note: I’m going to go into gory details here. If you just want to get to the code, you can find it on GitHub.

Details, Details

Matthias’ post covers the steps to request consent and access tokens. For the most part this was straightforward. However, I quickly ran into an issue. What information do I need to include in my token request? ADAL makes it look simple. I know I need the private key from the certificate that I uploaded to my app’s manifest, but what do I do with it? Well, ADAL is open source, so I could go dig through their source, but luckily, Vittorio posted the details on his blog. Matthias also updated his sample to include an alternative method of requesting the token that bypasses ADAL.

So the POST body isn’t that much different than in the authorization code grant flow. As expected, the grant_type is different. The big difference is that instead of a client_secret, we have two new fields: client_assertion_type and client_assertion. The client_assertion_type is always "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", so that just leaves the real challenge: client_assertion.

Building the Assertion

Vittorio’s blog provides the details. We just need to create a JWT with the appropriate fields. Here’s the breakdown:

  • alg: Always set to "RS256".
  • x5t: Thumbprint of the app’s cert. This is the same value that is present in the customKeyIdentifier field of the keyCredentials parameter in your app’s manifest file in Azure AD.

Payload

  • sub: Your app’s client ID.
  • iss: Your app’s client ID.
  • jti: A random GUID.
  • exp: A UNIX epoch time value that specifies when this assertion expires. Recommended to be Now + 10 minutes.
  • nbf: A UNIX epoch time value that specifies the start time of this assertion’s validity period. Recommended to be Now – 5 minutes.
  • aud: The URL to the tenant-specific token endpoint. Note: The common endpoint "https://login.windows.net/common/oauth2/token" will NOT work.

Sample Assertion

So let’s say you have an app with a client ID of bcdb5ae8-1ab8-47c3-b0b2-6756048e2ddc, and the consenting admin’s tenant ID (from the ID token you received during the consent process) is 5e0699a2-7e10-4d08-8ebb-4f7d7406ad09. Your assertion might look like:

 {
 "alg": "RS256",
 "x5t": "W-Kt0b4B7RkK34cdXVR91e_nf9x"
},
{
 "sub": "bcdb5ae8-1ab8-47c3-b0b2-6756048e2ddc",
 "iss": "bcdb5ae8-1ab8-47c3-b0b2-6756048e2ddc",
 "jti": "9ef089d9-b116-40d8-bbe2-57a756e32ea9",
 "exp": 1423168488,
 "nbf": 1423167888,
 "aud": "https://login.windows.net/5e0699a2-7e10-4d08-8ebb-4f7d7406ad09/oauth2/token" 
} 

This is very easy to do in Python using dictionaries.

Encoding and Signing

Now that you have the assertion, we need to encode it and sign it before it’s ready to use in the client_assertion claim.

Encoding

To encode the assertion, take each part separately (the header and the payload), and base64-encode it. You should then make it url-safe, and remove any trailing '=' characters. Finally, you join the two values together with a '.' character. Using the sample assertion above, that gives us:

 eyJhbGciOiAiUlMyNTYiLCAieDV0IjogIlctS3QwYjRCN1JrSzM0Y2RYVlI5MWVfbmY5eCJ9.eyJhdWQiOiAiaHR0cHM6Ly9sb2dpbi53aW5kb3dzLm5ldC81ZTA2OTlhMi03ZTEwLTRkMDgtOGViYi00ZjdkNzQwNmFkMDkvb2F1dGgyL3Rva2VuIiwgImp0aSI6ICI5ZWYwODlkOS1iMTE2LTQwZDgtYmJlMi01N2E3NTZlMzJlYTkiLCAiZXhwIjogMTQyMzE2ODQ4OCwgImlzcyI6ICJiY2RiNWFlOC0xYWI4LTQ3YzMtYjBiMi02NzU2MDQ4ZTJkZGMiLCAic3ViIjogImJjZGI1YWU4LTFhYjgtNDdjMy1iMGIyLTY3NTYwNDhlMmRkYyIsICJuYmYiOiAxNDIzMTY3ODg4fQ 

In Python, this involves using the built-in JSON library to dump the dictionaries to a string serialization, encode that into a UTF-8 byte array, and base64-encode it with the urlsafe_b64encode function.

Signing

With our encoded value in hand, you now need to sign it. Using the same certificate that you used when registering the app in Azure AD, create an RSA-256 signature. You then base64-encode the signature, and append it to the existing value, again using the '.' character as a separator. With our sample data, that results in something like:

 eyJhbGciOiAiUlMyNTYiLCAieDV0IjogIlctS3QwYjRCN1JrSzM0Y2RYVlI5MWVfbmY5eCJ9.eyJhdWQiOiAiaHR0cHM6Ly9sb2dpbi53aW5kb3dzLm5ldC81ZTA2OTlhMi03ZTEwLTRkMDgtOGViYi00ZjdkNzQwNmFkMDkvb2F1dGgyL3Rva2VuIiwgImp0aSI6ICI5ZWYwODlkOS1iMTE2LTQwZDgtYmJlMi01N2E3NTZlMzJlYTkiLCAiZXhwIjogMTQyMzE2ODQ4OCwgImlzcyI6ICJiY2RiNWFlOC0xYWI4LTQ3YzMtYjBiMi02NzU2MDQ4ZTJkZGMiLCAic3ViIjogImJjZGI1YWU4LTFhYjgtNDdjMy1iMGIyLTY3NTYwNDhlMmRkYyIsICJuYmYiOiAxNDIzMTY3ODg4fQ.BjLYFjloB4XojO5YELKeXc7m_-uhEHsDZ2MKjYHY4UX5fJYSLxVpNsn6uv_ICEhEcmn5wqoh6H7x2qgCUyf-jFto56GK7ygGqf1thGAVpDMzWF3fYsvZ7g3G-x_xPxOSul2EehXse8TqDhM1fhYHD2wpTeRGCSgZtuQwJiXy4R-ysnV9YFhzu0OhBxSwjfwA7XZaqPgvZNNm1BI-L9KQ-yAIrhBkv1LnVJ3k6oNAdxRbCmzLGdEm-Dq4EYZEVXqgdPn5wo98xtETDQ6ee6VH3xjyxpoyxHAfw3BO9Ve27EdVNiQLCEwOhsuRjQvmuzqKa3IUzuVkIzudO3RYQL3qfw 

Side Note: You can parse these tokens using any JWT parser, like https://jwt.calebb.net/.

Signing in Python was a little trickier. I found the Python-RSA library, which worked for me. I had to use OpenSSL to extract the private key from my certificate into an RSA-formatted PEM file in order to use this library.

Seeing it in Action

The sample app on GitHub implements all of this logic (see clientcredhelper.py) and uses the resulting access token to call the Mail API to list messages in the Inbox. Once you login with an admin account and grant permission, you can then view the 10 most recent messages in the Inbox of any user in your organization by entering their email address in the User Email field and clicking Set User. As always, I'd love to hear your feedback in the comments or on Twitter (@JasonJohMSFT).