
Image source: https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1334#issuecomment-587050110
Finding Your Object ID
Once you have setup your environment based on my earlier this post, you can create your own JWT tokens based on your own official Object ID from Microsoft Entra ID. This was especially useful for me (and for others as well) lately while debugging and development of Microsoft Custom Engine Agents for Microsoft 365.
There are 2 ways (there could be more!) you can find your own (or anyone's) Object ID -
Using Microsoft Azure Portal
Login to Microsoft Azure Portal with your official credentials.
Go to "Microsoft Entra ID" -> "Users" -> Find and select your user
In the "Overview" tab, you'd see the Object ID.
Using Microsoft Graph Explorer
Login with your official email address credentials.
Use the GET "https://graph.microsoft.com/v1.0/me" REST API (it defaults to this one anyways) and click on "Run query".
Grab the "id" value from the response.
The Code: Generating a JWT
With below small piece of code (console application), you can get your own access token to be used further in your headers.
But wait!
using Azure.Core;
using Azure.Identity;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography;
const string EXTERNAL_UPN = "<Your Object ID from above>";
const string MICROSOFT_GRAPH_APPLICATION_ID = "00000003-0000-0000-c000-000000000000";
var defaultCredentialOptions = new DefaultAzureCredentialOptions
{
ManagedIdentityClientId = EXTERNAL_UPN
};
var tokenCredential = new DefaultAzureCredential(defaultCredentialOptions);
var accessToken = await tokenCredential.GetTokenAsync(
new TokenRequestContext([MICROSOFT_GRAPH_APPLICATION_ID]) { }
);
var jwtToken = accessToken.Token;
You don't find this working. You get an error something like below -
"IDX10511: Signature validation failed. Keys tried: 'Microsoft.IdentityModel.Tokens.X509SecurityKey, KeyId: '<something>', InternalId: '<something>'. , KeyId: <something>\r\n'. \nNumber of keys in TokenValidationParameters: '10'. \nNumber of keys in Configuration: '0'. \nMatched key was in 'TokenValidationParameters'. \nkid: '<something>'. \nExceptions caught:\n '[PII of type 'System.String' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.\ntoken: '[PII of type 'System.IdentityModel.Tokens.Jwt.JwtSecurityToken' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'. See https://aka.ms/IDX10511 for details."
The culprit? A seemingly innocent JWT header claim called nonce.
A Quick Nonce Refresher
A nonce (short for “number used once”) is your personal bodyguard in the identity world, preventing an attacker from replaying old tokens or forging requests. Typically, the nonce:
Shows up when using OpenID Connect or certain OAuth flows to mitigate replay attacks.
Is unique per authentication request (like a random handshake phrase).
Returns within a token (ID token or Access token) so the client can verify it’s truly fresh and was intended for this exact request.
The token signature is computed based on both the header and the payload. If the identity provider (Microsoft Entra ID in this case) expects to see a hashed nonce in the header (as part of the signature), but the raw token you receive has the nonce in plain text, your local validation can fail.
Here’s the trick: Some identity providers hash the nonce before signing the token. However, you might receive the JWT with the original (plain-text) nonce. If your local validation code compares the hashed part in the signature to a plain-text header, you get the dreaded mismatch.
The Fix: Hashing the Nonce Before Validation
Hash the nonce in your local code so that it matches what the issuer used when creating the signature. Then, re-assemble the token so the header is consistent with the real signing data.
Complete code here -
using Azure.Core;
using Azure.Identity;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography;
const string EXTERNAL_UPN = "<Your Object ID from above>";
const string MICROSOFT_GRAPH_APPLICATION_ID = "00000003-0000-0000-c000-000000000000";
var defaultCredentialOptions = new DefaultAzureCredentialOptions
{
ManagedIdentityClientId = EXTERNAL_UPN
};
var tokenCredential = new DefaultAzureCredential(defaultCredentialOptions);
var accessToken = await tokenCredential.GetTokenAsync(
new TokenRequestContext([MICROSOFT_GRAPH_APPLICATION_ID]) { }
);
var jwtToken = accessToken.Token;
JwtSecurityTokenHandler tokenHandler = new();
JwtSecurityToken jsonToken = tokenHandler.ReadJwtToken(jwtToken);
string[] parts = jwtToken.Split('.');
string header = parts[0];
string payload = parts[1];
string signature = parts[2];
string userEmail = string.Empty;
//Hash nonce and update header with the hash before validating
if (jsonToken.Header.TryGetValue("nonce", out object nonceAsObject))
{
string plainNonce = nonceAsObject.ToString();
using SHA256 sha256 = SHA256.Create();
byte[] hashedNonceAsBytes = sha256.ComputeHash(
System.Text.Encoding.UTF8.GetBytes(plainNonce));
string hashedNonce = IdentityModel.Base64Url.Encode(hashedNonceAsBytes);
_ = jsonToken.Header.Remove("nonce");
jsonToken.Header.Add("nonce", hashedNonce);
header = tokenHandler.WriteToken(jsonToken).Split('.')[0];
jwtToken = $"{header}.{payload}.{signature}";
}
Console.WriteLine($"Your JWT Token: {jwtToken}");
Console.ReadKey();
What’s happening here?
Parse the token to access the header values.
Check if “nonce” exists in the header.
If present, hash it with SHA256 and then replace the plain nonce in the header with its hashed counterpart.
Reassemble the token’s three parts (header, payload, and signature) so the header matches the signature context.
Validate the updated token.
Now, your validation routine should pass the signature check—assuming all else (like keys and claims) is correct.
Hope this was useful.
Comments