Making Azure AD OIDC Compliant
Microsoft redesigned their authentication system under a new name Microsoft Identity Platform marked as v2.0 to indicate the different endpoints from the previous version.
The v2.0 is meant to be an improved implementation of OpenID Connect (OIDC) and other OAuth 2.0 authorization flows. While it does a good job of it compared to v1.0, it still requires some more work on your part to build a working web application that talks pure OIDC.
Recently I had to setup Azure AD as an Authorization Server for a web application consisting of a Javascript based SPA component which relied on a backend REST API serving protected resources. The implementation was specific to a Microsoft tenant and used the Work or School account to authenticate users. This blog will focus on the challenges that had to be overcome to have a working setup.
Note: Microsoft is continuously improving their implementation of v2.0 with new features so the information here is valid as of May 2020. Please check the extensive documentation for latest information.
Experiencing App Registration
It all starts with registering an application in Azure AD as an identity for the software you are developing. Most of the steps are straight-forward and are documented quite well here: Quickstart: Register an application with the Microsoft identity platform (you will need access to an Azure Active Directory).
Every App registration can have a redirect URL of http://localhost
. This is useful for quickly testing with a client like Postman or Insomnia. You will have to configure another redirect URL where your client should receive the tokens.
Another important thing to configure is Client Secret. This is needed mainly for Authorization Code Flow but as my application does not use that flow this could be skipped.
Finally, for Javascript based SPAs Implicit Flow is required, so Implicit Grant should be enabled. ID tokens are needed if the SPA needs to access the user information. Access tokens are only needed if SPA needs to call a REST API to access secured resources.
Ideally, this is all that should have been needed for OIDC to work but that’s not the case. There are more things to be configured but first let’s take a look at the problems before finding the solution.
Microsoft Identity Platform v2.0 Endpoints
Every OIDC provider has an Issuer Discovery URI. It is optional but most popular providers implement it. For Azure AD tenants, the Issuer URI looks like https://login.microsoftonline.com/{tenant-id}/v2.0
where tenant-id
is UUID of your Azure AD tenant. You can find this on the Azure Active Directory page on Azure Portal.
The Issuer URI itself isn’t a valid Internet address. That is taken care of by the OpenID Provider Configuration Document which is built by concatenating /.well-known/openid-configuration
to the Issuer URI. So Azure AD tenants’ Configuration URL will be https://login.microsoftonline.com/{tenant-id}/v2.0/.well-known/openid-configuration
. As per specs, this should respond with a valid JSON document that provides all relevant URL’s you need to make a successful OIDC authentication.
The JSON document has many URLs but the most important ones are authorization_endpoint and token_endpoint. Most often this is all you need to make a valid authentication request.
https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize
https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token
Now, it’s time for the fun stuff. We assume the Implicit Flow so only the authorization_endpoint is used. But this is equally applicable to Authorization Code Flow.
Problem 1: Azure AD returns invalid JWT access token
Scenario: Your application connects to the authorization endpoint to request for an access token in order to access secured resources behind a REST API.
What OIDC Spec says: Depending on the value of response_type
, a valid ID Token and/or Access token must be returned. The process of validating an access token is defined here: https://openid.net/specs/openid-connect-core-1_0.html#ImplicitTokenValidation
What Azure AD does: It returns a JWT access token which cannot be validated as per above spec. Why not? Because Azure adds a nonce
claim in the header of the token after the token has been signed using the public key. This makes it virtually impossible to validate. This is also confirmed by parsing the token at https://jwt.io.
{
"typ": "JWT",
"nonce": "Q0_yoiMxqlPUMkBaOwh8vBgM5vir9TkhbVpGQfaZP_8",
"alg": "RS256",
"x5t": "ie_qWCXhXxt1zIEsu4c7acQVGn4",
"kid": "ie_qWCXhXxt1zIEsu4c7acQVGn4"
}
The exact reason for adding the nonce
claim is described in a Github issue as below:
MsGraph recognized an opportunity to improve security for users. They achieved this by putting a ‘nonce’ into the jwt header. The JWS is signed with a SHA2 of the nonce, the ‘nonce’ is replaced before the JWS is serialized. To Validate this token, the ‘nonce’ will need to be replace with the SHA2 of the ‘nonce’ in the header. Now this can change since there is no public contract.
Effectively, by default Azure AD returns a valid JWT token only for Graph APIs. If you want to use the Azure AD OIDC authentication for your own API, you are dealing with a non-compliant provider.
What is the Solution?
We need to tell Azure AD that the token we need is not for Graph API but an API of our own. To achieve this, a custom scope should be created in the App Registration → Expose an API page. You can name this scope whatever you like. Then make sure to add this scope in the authorization request initiated by the client along with the default openid
scope.
This forces Azure to return a JWT token without the post-processed nonce
claim which results in an access token that can be validated as per the spec.
Problem 2: Incorrect Issuer claim returned in Access Token.
Scenario: Your application connects to the authorization endpoint to request for an access token in order to access secured resources behind a REST API.
What OIDC Spec says: The iss
claim inside the Access token must have the same value as returned in the JSON response of the Issuer Discovery URI that issued it. Refer https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery) MUST exactly match the value of the iss (issuer) Claim.
What Azure AD does: The iss
claim in the token does not match the actual Issuer. Any library that follows the OpenID spec will mark the token as invalid. In my case, the Spring Security OAuth library rejected the token.
Example: The JSON response contains below Issuer URI:
issuer: “https://login.microsoftonline.com/407b9272-20f5-421c-a25f-e7a189309c4b/v2.0"
The token issued by Azure AD for this tenant has iss
claim value:
{
...
"iss": "https://sts.windows.net/407b9272-20f5-421c-a25f-e7a189309c4b/",
...
}
So what is happening here?
Azure AD has two versions of OIDC implementation. Both are currently supported but they recommend to move to v2.0 if the limitations are not affecting your use case. Don’t forget to read this document for more details: https://docs.microsoft.com/en-us/azure/active-directory/develop/azure-ad-endpoint-comparison
Azure AD is returning the v1.0 token (with iss
claim pointing to v1.0 Issuer URI) even when v2.0 endpoints are being called. Since the Issuer for v1.0 tokens is different, the issuer and token iss
claim no longer match breaking the OpenID spec. To verify the version of a token, check the ver
claim. The previous link says this:
v1.0 and v2.0 tokens can be issued by both the v1.0 and v2.0 endpoints! id_tokens always match the endpoint they’re requested from, and access tokens always match the format expected by the Web API your client will call using that token. So if your app uses the v2.0 endpoint to get a token to call Microsoft Graph, which expects v1.0 format access tokens, your app will receive a token in the v1.0 format.
Note the highlighted part. While the id token version always matches the endpoints called, the access token does not. It matches the format expected by the Web API. What is that format? Where is it defined? Read on.
What is the solution?
We need to force Azure AD to return the v2.0 token. This is done by directly editing the Manifest configuration (definition of all attributes that Microsoft supports) of the App registration. Go to the App registration → Manifest page where you will be presented with a big JSON text editor.
Find the entry accessTokenAcceptedVersion
somewhere at the top, change the default value of null
to 2
.
{
...
“accessTokenAcceptedVersion”: 2
...
}
That’s it. Now Azure AD knows your web API expects a v2.0 token and will return a token with correct iss
claim. Phew!
With that, we have an Azure AD provider that actually is OpenID certified and can be used with any compliant OIDC client.
Optional Claims
Let’s talk some more about claims. Azure AD v1.0 was designed to return all claims in the token that the requestor had access to without any extra configuration. In Azure AD v2.0, Microsoft wanted to keep the size of the token small by making many claims optional. The full list is available here: https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-optional-claims but I want to discuss some important ones.
The email
claim is usually the claim used by most APIs to identify users and apply rules on. Why would Azure decide to make this optional I can’t say. Probably because there are scenarios where this claim won’t have a value. Interestingly, this claim will only ever have a value if the Work or School account (or owning tenant) has an Outlook 365 license. For Microsoft Live accounts it is not a problem.
The email address is also returned in preferred_username
claim but it is not guaranteed to be an email. As per Azure docs:
It could be an email address, phone number, or a generic username without a specified format. Its value is mutable and might change over time. Since it is mutable, this value must not be used to make authorization decisions.
So best to obtain an email
claim if available.
given_name, family_name
These claims are also not returned by default, but if set in Azure AD, can be requested using optional claims. It is useful to have these claims available because often Azure AD that is synced with on-premise ADs would have the name
claim in format like Sonkar, AB (Abhinav)
. You don’t want to split that to figure out the first and last names.
Enabling Optional Claims
Go to the App Registrations → Manifest page and add below JSON text:
"optionalClaims": {
"idToken": [],
"accessToken": [
{
"name": "given_name",
"source": null,
"essential": false,
"additionalProperties": []
},
{
"name": "family_name",
"source": null,
"essential": false,
"additionalProperties": []
},
{
"name": "email",
"source": null,
"essential": false,
"additionalProperties": []
}
],
"saml2Token": []
}
Update (26th May 2020)
Azure has updated the UI since writing this post which makes it easier to enable optional claims. Go to App Registrations → Token Configuration page and click on Add Optional Claim
button. Choose the type of token (in this case Access
) and select given_name
, family_name
and email
. Finally click on Add
button at the bottom to add these claims.
Request a new access token and you should have these claims available (assuming values are set and available in Azure AD).
That’s all for now. Hope this helps and saves you some time trying to use Azure AD as OIDC certified provider.