Azure Function with Azure AD access token

Viewed 876

I'm building a set of custom Azure Functions (Java) to be accessed by iOS and Android native clients using essentially and Oauth2 authentication strategy.

I'm able to successfully acquire a bearer access token from https://login.microsoftonline.com/XXXXX-XXXXXXXXX/oauth2/v2.0/token, but when I present this token to my Azure Function and attempt to validate it in code, I receive the following error. I'm doing something similar to the example provided here when validating the bearer token: https://dev.to/425show/secure-apis-with-azure-functions-java-azure-ad-and-ms-graph-49p1

AADSTS50013: Assertion failed signature validation. [Reason - The provided signature value did not match the expected signature value., Thumbprint of key used by client: 'XXXXXXXXXXXXXXXXXXXXXXXXXX'

Documentation from Microsoft is absurdly hard to use for what should be such a standard use case. Any help would be greatly appreciated.

2 Answers

I have answered several similar questions before. The root cause is whether the access token used in the assertion is for the Microsoft Graph api resource or the web api resource. You can determine the issuer of the token by parsing the token to see the aud claim.

If you use this access token to call the Microsoft Graph api, then you only need to set the scope to: https://graph.microsoft.com/.default.

If you use the access token to call web api (this is the api you expose in the Azure portal), then you need to set the scope to: api://{your api application client id}/{scope name}.

Please note: You can only put one type of api resource in the scope.

I spent quite some time trying to resolve a similar issue, and while some answers point to the solution, none that I have seen offer much insight into the underlying cause. So I thought I would offer some of my own findings.

Here is a sample Spring controller that extracts the access token:

package com.matthewcasperson.onenote.controllers;

import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class Test {
  @Autowired
  OAuth2AuthorizedClientRepository clientRepository;

  @GetMapping("/test")
  public String test(HttpServletRequest request) {
    final OAuth2AuthenticationToken oauth2Token = (OAuth2AuthenticationToken) SecurityContextHolder
        .getContext()
        .getAuthentication();

    final OAuth2AuthorizedClient client = clientRepository
        .loadAuthorizedClient(oauth2Token.getAuthorizedClientRegistrationId(), oauth2Token, request);

    final String accessToken = client.getAccessToken().getTokenValue();

    System.out.println(accessToken);

    return "test";
  }
}

This Spring application has been configured as a web application with the following application.yml file:

azure:
  activedirectory:
    tenant-id: ${TENANT_ID}
    client-id: ${CLIENT_ID}
    client-secret: ${CLIENT_SECRET}

The token that is printed by the controller decodes to this:

{
  "typ": "JWT",
  "nonce": "nx2pG8jshz2m6AY8eBafUJDXVgJNjjg526wpwsVRVoU",
  "alg": "RS256",
  "x5t": "l3sQ-50cCH4xBVZLHTGwnSR7680",
  "kid": "l3sQ-50cCH4xBVZLHTGwnSR7680"
}.{
  "aud": "https://graph.microsoft.com",
  "iss": "https://sts.windows.net/2ed832a8-cd8c-4f7d-89b3-935447e260c8/",
  "iat": 1633941558,
  "nbf": 1633941558,
  "exp": 1633945458,
  "acct": 0,
  "acr": "1",
  "aio": "AUQAu/8TAAAAPyU5VrhiE9R+skcJpwWKTVMqg/DfvQs1i0CQFYQgUAAHS5uZI72LdEjZxRn7VjMNfaq8gL1CbRcoD1PYVjWPfQ==",
  "amr": [
    "pwd",
    "mfa"
  ],
  "app_displayname": "OneNote",
  "appid": "abfb7030-ff37-48cd-8bd7-1bf5ec0777db",
  "appidacr": "1",
  "family_name": "Casperson",
  "given_name": "Matthew",
  "idtyp": "user",
  "ipaddr": "45.67.96.131",
  "name": "Matthew Casperson",
  "oid": "4c037c2f-4b2f-46c1-a25f-d214e2396d47",
  "platf": "3",
  "puid": "1003200188788F6F",
  "rh": "0.AUEAqDLYLozNfU-Js5NUR-JgyDBw-6s3_81Ii9cb9ewHd9tBACU.",
  "scp": "openid profile User.Read email",
  "signin_state": [
    "kmsi"
  ],
  "sub": "56Tq2KFJPnykNSAwOCFPSjq1s9kqybnOmJ99Nfnn53M",
  "tenant_region_scope": "OC",
  "tid": "2ed832a8-cd8c-4f7d-89b3-935447e260c8",
  "unique_name": "matthewcasperson@matthewcasperson.onmicrosoft.com",
  "upn": "matthewcasperson@matthewcasperson.onmicrosoft.com",
  "uti": "WWzRGjCqAEWNwcH4le9TAQ",
  "ver": "1.0",
  "wids": [
    "62e90394-69f5-4237-9190-012177145e10",
    "b79fbf4d-3ef9-4689-8143-76b194e85509"
  ],
  "xms_st": {
    "sub": "gnpeDIbUpAnTYqfXYe3lNOwC2_MV5UvEIm3zvZK6LVc"
  },
  "xms_tcdt": 1632346078
}.[Signature]

Note the aud of https://graph.microsoft.com (I've also seen an aud of 00000003-0000-0000-c000-000000000000, which is the client id of the Microsoft Graph API).

Despite being a JWT, and thus something we can inspect, this particular token should be treated as opaque:

You should not be looking at the access token, as the access token is meant for the resource (Graph in your case), and not the client (your app). You can just use the claims in the id token in your case.

If you try to use this JWT to generate an OBO token, you will recieve a similar error as the OP:

{
    "error": "invalid_grant",
    "error_description": "AADSTS50013: Assertion failed signature validation. [Reason - The provided signature value did not match the expected signature value., Thumbprint of key used by client: '977B10FB9D1C087E3105564B1D31B09D247BEBCD', Found key 'Start=02/07/2021 17:00:39, End=02/06/2026 17:00:39'].\r\nTrace ID: 384637e2-0853-4a10-b42a-a7cb53aa8500\r\nCorrelation ID: 1e9a639e-45e4-48e4-99b6-028ce43fe815\r\nTimestamp: 2021-10-11 08:54:13Z",
    "error_codes": [
        50013
    ],
    "timestamp": "2021-10-11 08:54:13Z",
    "trace_id": "384637e2-0853-4a10-b42a-a7cb53aa8500",
    "correlation_id": "1e9a639e-45e4-48e4-99b6-028ce43fe815",
    "error_uri": "https://login.microsoftonline.com/error?code=50013"
}

What I believe is happening here is this JWT is not meant to do anything other than obtain a /userinfo token that in turn contains the roles that an application would need to secure access to its code. Even though the access token is a JWT, it is meant to be treated as an opaque token, and can not be used in other flows like OBO.

You can create an authroized client that requires a scope defined on the Azure AD application. This scope is usually just some dummy value with no particular significance (I called my scope default, but any scope name would do):

azure:
  activedirectory:
    tenant-id: ${TENANT_ID}
    client-id: ${CLIENT_ID}
    client-secret: ${CLIENT_SECRET}
    authorization-clients:
      api:
        scopes:
          - api://${CLIENT_ID}/default

This client can be used like so:

  @GetMapping("/test2")
  public String test2(@RegisteredOAuth2AuthorizedClient("api") OAuth2AuthorizedClient api) {

    final String accessToken = api.getAccessToken().getTokenValue();

    System.out.println(accessToken);

    return "test";
  }

The access token created for this authorized client decodes to something like this:

{
  "typ": "JWT",
  "alg": "RS256",
  "x5t": "l3sQ-50cCH4xBVZLHTGwnSR7680",
  "kid": "l3sQ-50cCH4xBVZLHTGwnSR7680"
}.{
  "aud": "api://abfb7030-ff37-48cd-8bd7-1bf5ec0777db",
  "iss": "https://sts.windows.net/2ed832a8-cd8c-4f7d-89b3-935447e260c8/",
  "iat": 1633942486,
  "nbf": 1633942486,
  "exp": 1633946386,
  "acr": "1",
  "aio": "AVQAq/8TAAAAjlqywvUGKhdO5xKGTXyMxif6+n+PW+HQfqQUE2rz5vXQHmeqAWCJuWbxQEZJrn5FQwxPibQ/FZ8ZjsvlMWbscs8bG7AiCNwHepHg9HqCYTo=",
  "amr": [
    "pwd",
    "mfa"
  ],
  "appid": "abfb7030-ff37-48cd-8bd7-1bf5ec0777db",
  "appidacr": "1",
  "family_name": "Casperson",
  "given_name": "Matthew",
  "ipaddr": "45.67.96.131",
  "name": "Matthew Casperson",
  "oid": "4c037c2f-4b2f-46c1-a25f-d214e2396d47",
  "rh": "0.AUEAqDLYLozNfU-Js5NUR-JgyDBw-6s3_81Ii9cb9ewHd9tBACU.",
  "scp": "default",
  "sub": "gnpeDIbUpAnTYqfXYe3lNOwC2_MV5UvEIm3zvZK6LVc",
  "tid": "2ed832a8-cd8c-4f7d-89b3-935447e260c8",
  "unique_name": "matthewcasperson@matthewcasperson.onmicrosoft.com",
  "upn": "matthewcasperson@matthewcasperson.onmicrosoft.com",
  "uti": "AqpGPbtGNkayP_368gJ1AQ",
  "ver": "1.0"
}.[Signature]

Note the aud field is the Azure AD application rather than the Microsoft Graph API. I believe this token is mean to be transparent, meaning we can use it for other flows like OBO. Indeed this token works as you would expect when using it to create OBO tokens.

It doesn't make much sense for a web app to use an OBO flow, as web apps can simply ask for the appropiate access tokens directly with any required user permissions as part of an authorization code flow. So the above exmaple is a bit of a hack, but highlights the differences between the opaque tokens you get automatically, and the transparent ones you get through an authroized client.

Transparent JWTs are what you send to an OAuth resource server, and it does make sense for resource servers to generate their own OBO tokens.

For further reading, this post has a detailed walkthrough generating tokens with Azure AD.

Related