Llama 4 is now on available in Azure Databricks
April 6, 2025Introduction – AZ-900 Certification Course
April 7, 2025
I have a command-line program that implements native auth and validates the tokens.
See this post about implementing native auth.
As usual, the code is in a gist.
The ID token was easy; the access token was another matter.
I kept getting this error:
IDX10503: Signature validation failed.
To validate the signature, you need the keys.
First, go to the well-known URL:
"https://tenant.ciamlogin.com/tenant.onmicrosoft.com/
.well-known/openid-configuration";

Then go to the “jwks_uri” URL.

You need to find the key whose “kid” matches the “kid” in the JWT header.
I was trying to figure out how to validate the JWT signature, not realising the rabbit hole I was about to enter.
This long thread provides an example of checking the access token signature (or validation in general).
The scope is:
{ "scope", "openid offline_access" }
The access token is:
{
"typ": "JWT",
"nonce": "QltrBofYb5am7UJ8mCmOJjlNOXqr1bAu2h_H2s3ARpk",
"alg": "RS256",
"x5t": "CNv0OI3RwqlHFEVnaoMAshCH2XE",
"kid": "CNv0OI3RwqlHFEVnaoMAshCH2XE"
}
{
"aud": "00000003–0000–0000-c000–000000000000",
"iss": "https://sts.windows.net/26...b2a/",
"iat": 1743738155,
"nbf": 1743738155,
"exp": 1743742293,
"acct": 0,
"acr": "1",
"aio": "AUQ...mQAg==",
"amr": [
"pwd"
],
"app_displayname": "Native Auth",
"appid": "22f...342",
"appidacr": "0",
"idtyp": "user",
"ipaddr": "222.111.88.121",
"name": "Native Auth12",
"oid": "c45...2e6",
"platf": "14",
"puid": "10...445",
"rh": "1.Ab...4AA.",
"scp": "User.Read profile openid email",
"sid": "003...22",
"sub": "2am...CVY",
"tenant_region_scope": "NA",
"tid": "263...2a",
"unique_name": "nativeauth12@company.co.nz",
"upn": "c45...2e6@tenant.onmicrosoft.com",
"uti": "Dvq...CAA",
"ver": "1.0",
"wids": [
"b79...09"
],
"xms_ftd": "OBp...-L18",
"xms_idrel": "28 1",
"xms_st": {
"sub": "-3r...wmE"
},
"xms_tcdt": 1739333930
}
The first thing to notice is that it has a nonce. The second is the “aud” = “00000003–0000–0000-c000–000000000000,” and the third is that it is version 1.0.
That implies that it is a Graph API call.
Comment in the thread:
“OK that explains it then. I missed it before, but if look at the Jwt.Header you will see a ‘nonce’. This means you need special processing. Normal processing will fail.”
There are several threads that refer to “special processing”, but none explain what it is.
Then the comment:
“The token you have is for graph, why are you trying to validate it?”
And then you go down the rabbit hole.
One side says that the access token is opaque and that only the API should validate it (although that ignores the point of how the validation is done), and the other side says that if it looks like a duck, walks like a duck, and quacks like a duck, it’s a duck! Since the access token looks like a Microsoft JWT, you should be able to validate it.
Let’s look at the header of the ID token:
{
"typ": "JWT",
"alg": "RS256",
"kid": "CNv0OI3RwqlHFEVnaoMAshCH2XE"
}
Notice that the “kid” (the ID of the public key used to validate the signature) is the same for both the ID and access token.
So if that key validates the ID token, why does it not validate the access token?
If you look at the jwks data in the link in the well-known URL, you see:
"use":"sig",
So it is valid to use this key to validate a signature.
The error message says:
Token validation failed: IDX10511: Signature validation failed.
Keys tried: 'Microsoft.IdentityModel.Tokens.RsaSecurityKey, KeyId:
'CNv0OI3RwqlHFEVnaoMAshCH2XE',
InternalId: 'v2Sm...wHTc'.'.
Number of keys in TokenValidationParameters: '7'.
Number of keys in Configuration: '0'.
Matched key was in 'TokenValidationParameters'.
kid: 'CNv0OI3RwqlHFEVnaoMAshCH2XE'.
Exceptions caught:
''.
token: 'eyJ...djg'. See https://aka.ms/IDX10511 for details.
Then I found this post, which explained a lot.
“The 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.”
“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.”
In our app. registration, we create a dummy API:

We then add the permissions and grant admin access.

The scope is now:
{ "scope", "openid offline_access api://validate/ValidateJWT"}
And, bingo:
ID Token
Token is valid.
Access token
Token is valid.
The code to validate the token is:
var validationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = iss,
ValidateAudience = true,
ValidAudience = aud,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKeys = signingKeys,
RequireExpirationTime = true,
RequireSignedTokens = true,
RequireAudience = true,
};
In the new access token, there is no nonce:
{
"typ": "JWT",
"alg": "RS256",
"kid": "CNv0OI3RwqlHFEVnaoMAshCH2XE"
}
and the payload is:
{
"aud": "22f...342",
"iss": "https://263...b2a.ciamlogin.com/263..b2a/v2.0",
"iat": 1743894005,
"nbf": 1743894005,
"exp": 1743899468,
"aio": "AVQA...cA=",
"azp": "22f...342",
"azpacr": "0",
"name": "Native Auth12",
"oid": "c45...2e6",
"preferred_username": "nativeauth12@company.co.nz",
"rh": "1.A...4AA.",
"scp": "ValidateJWT",
"sid": "003...0ad",
"sub": "-3rb...wmE",
"tid": "263...b2a",
"uti": "HGV...GAA",
"ver": "2.0"
}
Note the version is now 2.0 and the scope is:
"scp": "ValidateJWT",
Looking at jwt.io, you see “Invalid signature”.
How do you get the signature to validate?
You need the public key, which the code in the gist calculates as a PEM:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhz6fUSCSAuiyQz6L1nQj4za8kItevJzx
...
CMh7xwIDAQAB
-----END PUBLIC KEY-----
You also need the key modulus, which is the “n” in the key array:
// n
string modulus = "hz6fUSCSAuiyQz6L1..."
Place this in the top box (indicated by the arrow).

and you get:

This works for both the ID and access token.
All good!
Validating the ID and Access JWT signature in Entra External ID was originally published in The new control plane on Medium, where people are continuing the conversation by highlighting and responding to this story.