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.