OAuth 2.0 Authorization Code Flow with Azure Functions and Microsoft Identity – Part 3: Validating the ID token

This is part three in a three part series on Authorization Code Flow with Microsoft Identity. If you haven’t started at Part 1, I would suggest you do as it would make more sense than starting from here.

One thing that was pointed out in the previous post, though, was that any id token for any app on that AzureAD tenant could be used to get a refreshed access token, and that isn’t very secure. What we will be doing in this post is looking into ways we can be more confident the id token is valid, so we can be more confident the caller is allowed to get their refreshed access token.

A simple way to validate the token

The current function app extracts the tid and the sub from the id token, which identifies the user against an AzureAD Tenant and to a specific app, and then we encode it in base 64 to give a unique ID. So any request to get a refreshed token for the user, would require the id token to have the correct values for both the tid as well as the sub; and if they provide that, you could be somewhat sure that the user is who they say they are.

That isn’t good enough though. Our app could receive an id token that has expired, and currently it would happily accept it. It would also happily accept an id token where the signature doesn’t match the payload.

Let’s do something better

According to Microsoft, an ID token needs to be validated in the following ways:

  • Validating the signature
  • Ensure the current time is within the timestamp claims on the token
  • Confirm the audience is correct
  • Check the nonce matches the one in the original request

Thankfully, all of this can be done with the Microsoft.IdentityModel.Protocols.OpenIdConnect library. Install that in your project.

Regarding the nonce, this is needed to check when you have made a specific request for an ID token, or in a hybrid flow scenario, where the initial request has a nonce parameter in the header. Seeing as we didn’t do this, we don’t need to check the nonce.

To validate the signature, our backend needs to get a series of keys from the OpenID provider, which in our case is Microsoft Identity. We then use the library to validate the signature against those keys.

In the MicrosoftIdentityClient class, add the following method:

private async Task<OpenIdConnectConfiguration> GetOpenIdConfiguration()
{
    ConfigurationManager<OpenIdConnectConfiguration> configManager = new ConfigurationManager<OpenIdConnectConfiguration>(
        $"https://login.microsoftonline.com/{tenantId}/v2.0/.well-known/openid-configuration",
		new OpenIdConnectConfigurationRetriever(),
		new HttpDocumentRetriever());

    return await configManager.GetConfigurationAsync();
}

This is doing a fair bit of heavy lifting for us, but essentially is calling two URLs to get the information we need:

We use some info in the returned OpenIdConnectConfiguration to validate the signature, as well as the issuer.

This next snippet below, will do all of the validations mentioned above, apart from the nonce

public async Task ValidateIdToken(string idToken)
{
	OpenIdConnectConfiguration config = await this.GetOpenIdConfiguration();

	TokenValidationParameters tokenValidationParameters = new TokenValidationParameters
	{
		ValidAudience = clientId,
		ValidIssuer = config.Issuer,
		IssuerSigningKeys = config.SigningKeys
	};

	new JwtSecurityTokenHandler().ValidateToken(idToken, tokenValidationParameters, out _);
}

As you can see in the tokenValidationParameters, the ValidIssuer and IssuerSigningKeys are provided from the call to collect the config, and the ValidAudience needs to be the client id of the application we are validating. You see no mention of validating the timestamp claims as they are done automatically.

If validation fails, an exception will be thrown with details, this will need to be handled. Probably by returning an error to the front-end asking to sign in again with a new authorization code.

This is the minimum set needed to be confident that the ID token is valid. But you can do more. You can also turn off certain validations if they don’t work for your situation. See this documentation for more on what you can do.

That’s it

That is the end of a three-part series on how to do authorization code flow, complete with refreshing tokens, and validating the identity of the caller. I have done my best to ensure security while keeping this small.