Anthony Attwood

Punny Stuff

Azure AD and the Un-validatable Access Token

2020-02-25 Anthony Attwoodauthentication

On any project with a backend API, you probably want to secure access to that API so that only authenticated users can call it. If you’re wanting to authenticate users that already exist in an Azure Active Directory tenant, then an Azure AD App Registration will probably be involved.

This post aims to explain a major gotchya with using an Azure AD App Registration for securing access to an API, and assumes you’re reasonably familiar with modern authentication using OAuth and OIDC. In particular, the idea of acquiring an access token for a particular api (or “audience” in OAuth terms).

Inspired by this SO post and a related Github issue

tl;dr

To get the Microsoft Identity Platform to issue access tokens you can pass to your api, you need to set up a custom scope in the App Registration’s Expose an API tab, and request that scope when you acquire the tokens. If you don’t, you will get access tokens that are intended to be presented to the Microsoft Graph API and cannot be validated by vanilla JWT middleware.

… continued …

If you’re reasonably familiar with OAuth and OIDC, you might think that it’s straightforward to;

  • set up an App Registration in Azure AD,
  • implement a login flow using your chosen authentication flow using the App Registration’s client id,
  • request an access token via the /token endpoint, and lastly,
  • present the access token to your api (usually as an Authorization: Bearer {access_token} header)

Your API will then validate the token, ensure the subject represented by the token is allowed to access the desired resource, and execute the API call.

Gotchya

…And your API won’t be able to validate the token.

The App Registration blade in the Azure Portal has a tab called Expose an API.

App Registration blade - Expose an API  - no scopes

When I first encountered this issue, I didn’t set up any scopes for my API because I thought;

“This only lets me set up scopes - my API doesn’t need any scopes, I only want the verified subject details of a logged in user, which I can get from an access token”

So what do you actually get?

If you go through a login flow and acquire an id and access token with the App Registration set up like this, you’ll get an (decoded) id token that looks like something like this;

ID Token

Header
{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "HlC0R**snip**"
}

Payload
{
  "aud": "{app-registration-client-id}",
  "iss": "https://login.microsoftonline.com/{azure-ad-tenant-id}/v2.0",
  "iat": 1582618197,
  "nbf": 1582618197,
  "exp": 1582622097,
  "email": "anthony.attwood@****",
  "nonce": "{nonce-value}",
  "preferred_username": "anthony.attwood@***",
  "sub": "{subject-id}",
  "ver": "2.0",
  "**lots-more-claims**": "**snip**"
}

Nothing particularly unexpected in the id token.

The access token on the other hand… When decoded it’ll look something like this;

Access Token

Header
{
  "typ": "JWT",
  "nonce": "ekY15jb**snip**",
  "alg": "RS256",
  "x5t": "HlC0R**snip**",
  "kid": "HlC0R**snip**"
}

Payload
{
  "aud": "00000003-0000-0000-c000-000000000000",
  "iss": "https://sts.windows.net/{azure-ad-tenant-id}/",
  "iat": 1582618197,
  "nbf": 1582618197,
  "exp": 1582622097,
  "app_displayname": "{app-registration-name}",
  "name": "Anthony Attwood",
  "scp": "User.Read profile openid email",
  "sub": "{different-sub-claim-to-the-id-token}",
  "unique_name": "anthony.attwood@***",
  "upn": "anthony.attwood@***",
  "**lots-more-claims**": "**snip**"
}

Your API won’t be able to validate the access token, indeed, there’s a couple of things wrong with it if you intend to present it to your own api.

  • The header has a nonce parameter. This breaks validation and is indeed what the SO post I mentioned at the start of the article is about. A regular JWT does not have a nonce parameter in the header.
  • The aud claim (“aud” = audience) is a weird looking GUID that’s mostly zero’s, 00000003-0000-0000-c000-000000000000. You’d probably expect it to be your App Registrations client id.

The funny looking aud claim indicates that the access token is actually intended to be presented to the Microsoft Graph API, not your API. The Graph API knows how to validate that kind of token.

The missing piece of the puzzle

Remember the Expose an API tab where I didn’t set up any scopes? This is the missing piece of the puzzle.

Even if your API doesn’t need any custom scopes — remember, your API might only care about knowing that the token was issued to a known logged in user — you still need to set up a custom scope and request it in your /token or /authorize request.

Set up a custom scope in the Expose an API tab, and you’ll see your new scope in the list, which will always be prefixed with your Application ID URI.

App Registration blade - Expose an API  - with scope

Now does it work?

Now when you acquire an access token, you request your special scope, and you’ll get back an access token in a different format that you can safely pass to your API.

For simplicity and for when I don’t have a UI hooked up to do an interactive flow, I use an implicit flow to get tokens. So using a call to the /token,

GET https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize /
  ?scope=api://thisismyappid/SuperSpecialScope profile email openid /
  &response_type=id_token token /
  &client_id={client-id} /
  &redirect_uri={some-imaginary-but-valid-redirect-uri} /
  &nonce={anything} /
  &state={anything}

Notice the scopes I’m requesting include the scope I just set up in the Expose an API tab.

You get back an id token that still looks like it did before, but the access token looks different.

Access Token

Header
{
  "typ": "JWT",
  "alg": "RS256",
  "x5t": "HlC0RHlC0R**snip**",
  "kid": "HlC0RHlC0R**snip**"
}

Payload
{
  "aud": "api://thisismyappid",
  "iss": "https://sts.windows.net/{azure-ad-tenant-id}/",
  "iat": 1582620705,
  "nbf": 1582620705,
  "exp": 1582624605,
  "appid": "{client-id}",
  "name": "Anthony Attwood",
  "scp": "SuperSpecialScope",
  "sub": "{same-sub-as-the-id-token}",
  "unique_name": "anthony.attwood@***",
  "upn": "anthony.attwood@***",
  "**lots-more-claims**": "**snip**"
}

A few things to notice about this new format of access token include;

  • There’s no nonce parameter in the header
  • The audience aud claim is now what we’ve configured in the App Registration as the App Id URI, not the special GUID that represents the Graph API

This token will validate correctly using the JWKS signing material we can discover from the OpenIDConnect discovery document. We can get that by appending /.well-known/openid-configuration to the value of the issuer iss claim, for example, https://sts.windows.net/{azure-ad-tenant-id}/.well-known/openid-configuration. Your JWT validation middleware probably does this for you automatically based on the value of the iss claim.

The solution

By adding a custom claim to the App Registration’s Expose an API tab and requesting that scope when we acquire tokens, the Microsoft Identity Platform will now generate access tokens that we can present to our API, and that will validate correctly by any compliant JWT middleware.

Happy authenticating!