By AndyPublished

Azure AD JWT Token Validation: Validating Microsoft Entra ID Access Tokens

To validate an Azure AD (Microsoft Entra ID) access token, your API reads the OpenID Connect metadata for the token's version, for examplehttps://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration. From it, the API verifies the RS256 signature against the key in jwks_uri that matches the token's kid. It then checks that iss matches the tenant (https://login.microsoftonline.com/{tid}/v2.0 for v2,https://sts.windows.net/{tid}/ for v1), that aud is your API, and that exp and nbf hold. Finally it authorises on scp (delegated) or roles (app roles). Only validate tokens issued for your own API; Microsoft Graph tokens are not meant to be validated by you. This guide follows Microsoft's identity platform documentation on learn.microsoft.com as of September 2026.

Which Azure Tokens Should You Validate?

Microsoft is specific about this. Web APIs must validate the access tokens sent to them, and must only accept tokens whose aud is one of their own identifiers. Client apps (SPAs, mobile and desktop apps) should treat access tokens as opaque strings and not validate them at all.

⚠
Do not validate Microsoft Graph access tokens. Microsoft's docs say you can't validate tokens for Microsoft Graph by the standard rules "due to their proprietary format", and that accepting a token meant for another resource is an example of the confused deputy problem. A Graph token failing signature validation in your code is expected behaviour. If your API needs a token, expose a scope on your own app registration and have the client request api://{your-api-client-id}/scope-name.

Microsoft's documentation on access token claims and validation applies only to tokens for registered APIs, not to tokens for Microsoft-owned APIs. Tokens that a Microsoft API receives might not be a decodable JWT at all.

Azure JWT Token Example (v2.0 Access Token)

This is the shape of Microsoft's published v2.0 sample, with identifiers replaced by placeholders:

// header
{ "typ": "JWT", "alg": "RS256", "kid": "H4iJ5kL6mN7oP8qR9sT0uV1wX2yZ3a" }

// payload
{
  "aud": "00001111-aaaa-2222-bbbb-3333cccc4444",
  "iss": "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0",
  "iat": 1537231048,
  "nbf": 1537231048,
  "exp": 1537234948,
  "aio": "AXQAi/8IAAAA...",
  "azp": "11112222-bbbb-3333-cccc-4444dddd5555",
  "azpacr": "0",
  "name": "Abe Lincoln",
  "oid": "690222be-ff1a-4d56-abd1-7e4f7d38e474",
  "preferred_username": "abeli@contoso.com",
  "rh": "I",
  "scp": "access_as_user",
  "sub": "HKZpfaHyWadeOouYlitjrI-KffTm222X5rrV3xDqfKQ",
  "tid": "aaaabbbb-0000-cccc-1111-dddd2222eeee",
  "uti": "fqiBqXLPj0eQa82S-IYFAA",
  "ver": "2.0"
}

Azure AD v1 vs v2 Tokens

The access token version is chosen by the API's app registration, not by the endpoint the client calls. The setting is requestedAccessTokenVersion in Microsoft Graph and the current manifest; older manifests and many blog posts call it accessTokenAcceptedVersion. Values of null or 1 produce v1.0 tokens, and2 produces v2.0 tokens. It must be 2 if the app accepts personal Microsoft accounts. A client using the v2.0 endpoint can therefore still receive a v1.0 access token, which is why so many APIs are surprised to see sts.windows.net issuers.

Itemv1.0 tokenv2.0 token
ver claim"1.0""2.0"
isshttps://sts.windows.net/{tenant-id}/https://login.microsoftonline.com/{tenant-id}/v2.0
audClient ID or the App ID URI (e.g. api://...), depending on the requestAlways the API's client ID (GUID)
Client applicationappid, appidacrazp, azpacr
Usernameunique_name, upnpreferred_username
Header key IDskid and x5tkid only
OIDC metadatalogin.microsoftonline.com/{tenant}/.well-known/openid-configurationlogin.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration

Microsoft's rule is to validate each version against its own metadata document. A v1.0 token (ver = "1.0") uses the metadata URL without /v2.0, even if your API is configured with a v2.0 authority, and vice versa. The simplest fix is usually to set the API to v2 and accept one format.

Azure AD JWT Claims That Matter for Validation

  • ·aud: must be your API. For v2 tokens this is always the API's client ID. For v1 tokens it can be the client ID or the App ID URI the client requested (such as api://...), so v1 APIs often need both in their valid audiences. See the audience claim.
  • ·iss and tid: the issuer embeds the tenant ID, and tid repeats it. The personal Microsoft account tenant is 9188040d-6c67-4c5b-b112-36a304b66dad.
  • ·scp: a space-separated string of delegated scopes, present only when a user is involved.
  • ·roles: an array of app roles. For user tokens these are the user's assigned roles on your app. For client-credentials (app-only) tokens they are the application permissions, and there is no scp.
  • ·azp / appid: the client application that requested the token. You can use it to allow-list callers.
  • ·groups, wids: group object IDs and directory role template IDs, if configured. Above 200 groups in a JWT, Entra ID drops groups and emits an overage claim (_claim_names / _claim_sources) that points you to Graph.
  • ·name, preferred_username: mutable display values. Microsoft says not to use them for authorisation.
  • ·aio, rh, uti: internal or token-ID claims. Ignore the first two; uti plays the role of jti.

Microsoft also warns that claims appear only when they have a value and that new claims may be added, so do not fail a token because an optional claim is missing. For the general meaning of each registered claim, see JWT claims explained.

JWT sub vs oid in Entra ID

Both identify the user, but at different scopes:

  • ·sub is a pairwise identifier: unique to the user and the application. The same person signing in to two different client IDs gets two different sub values.
  • ·oid is the user's (or service principal's) object ID in the tenant. It is the same across every application in that tenant, and it is the id Microsoft Graph returns for the user.

Use oid together with tid as the database key when several of your apps or services need to agree on who a user is. Use sub when you want to limit correlation. Either way, include the tenant: the same user in two tenants has two different object IDs, and Microsoft notes that claims are interpreted within the issuer and tenant.

Azure JWT Token Validation, Step by Step

  • ·1. Load metadata. Single tenant: https://login.microsoftonline.com/{tenant-id}/v2.0/.well-known/openid-configuration. Multi-tenant: /organizations/v2.0/... or /common/v2.0/....
  • ·2. Verify the signature with the key from jwks_uri whose kid matches the header, and allow only RS256. Keys rotate, so refresh them; Microsoft suggests checking about every 24 hours. Apps using claims-mapping with custom signing keys must add ?appid={client-id} to the metadata URL.
  • ·3. Validate the issuer. Single tenant: exact match with the metadata issuer. Multi-tenant: the metadata issuer is the template https://login.microsoftonline.com/{tenantid}/v2.0. Substitute the token's tid, require an exact match, confirm tid is a GUID, and check the signing key's own issuer property in the keys document. Then restrict to the tenants you actually serve.
  • ·4. Validate aud against your client ID (and App ID URI for v1).
  • ·5. Check exp and nbf with a small leeway.
  • ·6. Authorise on scp for delegated calls or roles for app-only calls. See scopes vs roles.

Multi-tenant pitfalls

The most dangerous shortcut in multi-tenant APIs is turning issuer validation off because the issuer "changes per tenant". With issuer validation off and the common keys, any Entra tenant in the world can mint a token for an app registration it controls, and only the audience check stands in its way. Follow Microsoft's substitution rule instead: build the expected issuer from the token'stid, require an exact match, and then check tid against the tenants you have onboarded. Also include tid in every data lookup, because Microsoft notes that the same sub in two tenants describes two different users.

ASP.NET Core: Microsoft.Identity.Web

On ASP.NET Core, Microsoft recommends Microsoft.Identity.Web, which applies these rules for you:

// appsettings.json: "AzureAd": { "Instance": "https://login.microsoftonline.com/",
//                                "TenantId": "<tenant-id>", "ClientId": "<api-client-id>" }
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

Plain AddJwtBearer also works for a single tenant; the ASP.NET Core JWT guide covers the claim-mapping settings you need for roles to drive [Authorize(Roles = ...)].

Node.js with jose

import { createRemoteJWKSet, jwtVerify } from "jose";

const TENANT_ID = "aaaabbbb-0000-cccc-1111-dddd2222eeee";
const API_CLIENT_ID = "00001111-aaaa-2222-bbbb-3333cccc4444";
// jwks_uri from the v2.0 openid-configuration document for the tenant
const JWKS = createRemoteJWKSet(
  new URL(`https://login.microsoftonline.com/${TENANT_ID}/discovery/v2.0/keys`)
);

export async function validateEntraAccessToken(token) {
  const { payload } = await jwtVerify(token, JWKS, {
    issuer: `https://login.microsoftonline.com/${TENANT_ID}/v2.0`,
    audience: API_CLIENT_ID,
    algorithms: ["RS256"],
    clockTolerance: 60,
    requiredClaims: ["tid", "oid"],
  });
  if (payload.ver !== "2.0") throw new Error("Expected a v2.0 access token");
  if (payload.tid !== TENANT_ID) throw new Error("Wrong tenant");

  const scopes = typeof payload.scp === "string" ? payload.scp.split(" ") : [];
  const roles = Array.isArray(payload.roles) ? payload.roles : [];
  if (!scopes.includes("Orders.Read") && !roles.includes("Orders.Read.All")) {
    throw new Error("Missing Orders.Read permission");
  }
  return payload;
}

In production, read jwks_uri from the metadata document rather than hard-coding it. The example was run against a locally signed test token with the same claim layout. For the mechanics of key sets and kid lookups, see what is a JWKS.

JWT Validation in Azure API Management (APIM)

APIM can reject bad tokens at the gateway. For Entra ID tokens, Microsoft points to the dedicatedvalidate-azure-ad-token policy; the generic validate-jwtpolicy is intended for other identity providers but also accepts Entra metadata. Both are available in all APIM tiers and run in the inbound section.

<!-- Entra ID: dedicated policy -->
<validate-azure-ad-token tenant-id="{{aad-tenant-id}}" output-token-variable-name="jwt">
    <client-application-ids>
        <application-id>{{aad-client-application-id}}</application-id>
    </client-application-ids>
    <audiences>
        <audience>00001111-aaaa-2222-bbbb-3333cccc4444</audience>
    </audiences>
</validate-azure-ad-token>

<!-- Generic policy with Entra metadata -->
<validate-jwt header-name="Authorization" require-scheme="Bearer"
              failed-validation-httpcode="401" clock-skew="60">
    <openid-config url="https://login.microsoftonline.com/{tenant-id}/v2.0/.well-known/openid-configuration" />
    <audiences>
        <audience>00001111-aaaa-2222-bbbb-3333cccc4444</audience>
    </audiences>
</validate-jwt>
  • ·validate-jwt defaults to a clock-skew of 0 seconds and requires exp unless require-expiration-time="false". Its supported asymmetric algorithms are PS256, RS256, RS512 and ES256.
  • ·APIM re-fetches OpenID configuration hourly, and at most every five minutes when it meets an unknown kid.
  • ·Both policies accept a required-claims block (with match="all" or "any") for simple claim-value checks at the gateway.
  • ·Gateway validation does not replace validation in the API itself if the API can be reached without going through APIM.

Azure JWT Decode and Troubleshooting

Decoding first answers most questions: ver, iss andaud show at once whether you have a v1 or v2 token, and whether it was issued for your API or for Graph. Microsoft's docs mention jwt.ms for this. The jwtdecode.app decoder also works and decodes in your browser without sending the token anywhere. Then match the error:

Error (Microsoft.IdentityModel)Usual meaning
IDX10214: Audience validation failedaud is the client ID but you configured the App ID URI (or the reverse), or the token is for another API such as Graph
IDX10205: Issuer validation failedv1 token (sts.windows.net) checked against a v2 issuer, or a multi-tenant app expecting one tenant
IDX10503 / IDX10511: Signature validation failedKeys fetched from the wrong metadata document, a Graph token, or a token from another cloud or tenant
IDX10223: Lifetime validation failed. The token is expiredExpired token or clock drift; clients should refresh silently through MSAL

More general failure patterns are in common JWT errors and fixesand the JWT debugging routine.

Summary

Azure AD JWT token validation means validating only tokens for your own API (never Microsoft Graph tokens) against the metadata document for the token's version. Check the RS256 signature viajwks_uri and kid, then the issuer (sts.windows.net for v1, login.microsoftonline.com/{tid}/v2.0 for v2, with tid substitution for multi-tenant apps), aud, and the lifetime. Authorise on scp or roles, and key users onoid plus tid; sub is per-application. Set requestedAccessTokenVersion to 2 to keep to one format, and usevalidate-azure-ad-token when APIM sits in front of the API.

Ready to decode a token?
Use the free JWT decoder — paste any token for instant results, entirely in your browser.
Open JWT Decoder