JWT Decoder & Claims Inspector: Code Examples

Paste any JWT to inspect its header, payload, and expiry. Nothing leaves your browser.

ZERO UPLOAD · ALL LOCAL
  1. Copy your JWT token — it looks like three Base64 sections separated by dots (eyJ…)
  2. Paste the token into the input field — decoding happens automatically on paste.
  3. Read the Header panel: it shows the signing algorithm (alg) and token type (typ).
  4. Read the Payload panel: it shows all claims — user ID, roles, issued-at (iat), and expiry (exp).
  5. Check the expiry status indicator — it shows whether the token is currently valid or expired, with a human-readable time delta.
  6. Note: the tool decodes only — it does not verify the signature. Never trust decoded claims in a security context without server-side verification.

HEADER
          
PAYLOAD
          
EXPIRY

Auth0 JWT Format and Claims

Auth0 issues two types of JWTs: ID tokens (for the application) and access tokens (for APIs). Both are signed JWTs with different claim sets.1 Pasting either type into the decoder above shows the full payload without transmitting the token to any server.

Token types and structure

Auth0 issues two JWT types for every application: an ID token for the front-end and an access token for APIs. The ID token carries identity claims: name, email, and picture.2 Consequently, it belongs in your application only and must never be forwarded to an API endpoint. Access tokens carry the aud of your API resource, enabling authorisation decisions on the server side.3 Because the two tokens serve different audiences, mixing them produces hard-to-diagnose 401 errors. When your API receives a token, check whether the aud claim matches your API identifier or your application client ID to determine which token type you are handling before reading any other claims.

Key claims explained

Inside every Auth0 token, the sub claim identifies the user by connection type and provider ID. Google sign-ins produce sub values like google-oauth2|108512345, while email/password accounts use auth0|64a1b2c3. The azp claim carries the client_id of the application that requested the token: useful in multi-app tenants where several front-ends share one Auth0 tenant. Building on this, the iss claim always takes the pattern https://<tenant>.auth0.com/, and comparing it against your expected tenant domain prevents cross-tenant token acceptance.2 The exp claim sets the token lifetime in Unix seconds, and your server must reject any token whose exp has passed before trusting the sub or azp values for authorization decisions.

Signature verification

Auth0 signs all production tokens with RS256, an asymmetric algorithm where a private key signs and a public key verifies. Fetching the public key set from https://<tenant>.auth0.com/.well-known/jwks.json gives you the current signing keys.4 Use the kid in the token header to select the correct key from the set.4 Libraries like jose (JavaScript/TypeScript) and python-jose (Python) handle key fetching and rotation automatically, but client-side verification alone is insufficient: always perform verification server-side before trusting any claims.5 Auth0 rotates signing keys periodically, so cache the JWKS response using the Cache-Control header and refresh only when you encounter an unknown kid value in a new token header.

Practical decoding workflow

When you decode an Auth0 token, separate inspection from trust. The payload shows claims immediately, but it does not prove the token is valid until the signature and issuer match your expected configuration. Start by checking iss, aud, and exp in the tool above, then compare the decoded values with the endpoint that rejected the request.

Reading a 401 response

A 401 usually points to the wrong audience, expired token, or token type mismatch rather than a decoder problem. If an API expects an access token and receives an ID token, the aud claim will not match the API identifier. Use that mismatch to narrow the fix before changing application code. When the decoded exp shows a timestamp still in the future, the audience or token type is the most likely cause rather than a clock issue on your server.

Comparing the decoded audience against the endpoint that rejected the request is the fastest way to localise a 401 without changing server code. When the access token and ID token are decoded side by side, the difference in their aud values becomes obvious and the correct fix usually follows from that single observation. Keep the decoded exp visible next to your server clock so you can rule out clock drift before blaming the application logic.

Custom claims via JWT Templates

In Auth0's pipeline, JWT Templates and Actions inject custom claims into every issued token. Navigate to Dashboard > Actions > Flows to add a post-login Action that sets a custom claim. Auth0 requires a URL namespace for custom claim names: a bare key like role is rejected, while https://yourapp.com/role is accepted. The namespace prevents collision with standard JWT registered claims and with future additions to the specification.3 When you add a new custom claim through an Action, the claim appears in tokens issued after the Action runs, but existing tokens retain their previous claim set until the user signs in again.

Inspecting custom claim output

Custom claims appear in both ID tokens and access tokens unless your Action targets only one type. Inspect the output by decoding a fresh token in the tool above immediately after saving the Action. Claims added through Actions take effect on the next sign-in; no propagation delay applies to newly issued tokens. If a custom claim does not appear after two consecutive sign-ins, review the Action code for conditional logic that might skip the claim for certain connection types or user metadata values.

Auth0 token refresh and session management

When an Auth0 access token expires, your application requests a new one using the refresh token the auth SDK stored at sign-in.1 The Auth0 SPA SDK handles silent refresh automatically through a running session on the Auth0 authorization server. Your application code always reads the current token from the SDK at request time; the SDK performs the refresh before handing you a stale token.

Server-side refresh token exchange

For server-side applications using the Auth0 Node.js SDK or Auth0 Python SDK, call the POST /oauth/token endpoint with grant_type=refresh_token when a 401 response signals expiry. Auth0 returns a new access token with a fresh exp value and optionally a rotated refresh token. Configure refresh token rotation in the Auth0 dashboard under Applications > Advanced > OAuth to enable automatic rotation on every exchange.

Notes

The iss claim takes the form https://<tenant>.auth0.com/. The aud in an ID token is the client_id of your application; the aud in an access token is the API audience identifier (often an array). The azp claim identifies the requesting client. The sub claim follows the pattern |: for example google-oauth2|108512345678901234567 for a Google login.

Examples

Typical Auth0 ID token payload

{
  "iss": "https://yourapp.auth0.com/",
  "sub": "google-oauth2|108512345678901234567",
  "aud": "YOUR_CLIENT_ID",
  "exp": 1716998400,
  "iat": 1716994800,
  "name": "Jane Smith",
  "email": "[email protected]"
}

Auth0 access token payload

{
  "iss": "https://yourapp.auth0.com/",
  "sub": "google-oauth2|108512345678901234567",
  "aud": ["https://api.yourapp.com", "https://yourapp.auth0.com/userinfo"],
  "azp": "YOUR_CLIENT_ID",
  "scope": "openid profile email",
  "exp": 1716998400
}

Access tokens have aud as an array when multiple audiences are requested.

Verify with the JWT Decoder & Claims Inspector tool.

Typical Auth0 ID token payload

{
  "iss": "https://yourapp.auth0.com/",
  "sub": "google-oauth2|108512345678901234567",
  "aud": "YOUR_CLIENT_ID",
  "exp": 1716998400,
  "iat": 1716994800,
  "name": "Jane Smith",
  "email": "[email protected]"
}
Sources
  1. 1.

    Auth0, "Tokens," auth0.com, accessed June 2026. https://auth0.com/docs/secure/tokens

  2. 2.

    N. Sakimura, J. Bradley, M. Jones, and B. de Medeiros, "OpenID Connect Core 1.0 incorporating errata set 2," openid.net, December 2023. https://openid.net/specs/openid-connect-core-1_0.html

  3. 3.

    M. Jones, J. Bradley, and N. Sakimura, "JSON Web Token (JWT)," RFC 7519, IETF, May 2015. https://www.rfc-editor.org/rfc/rfc7519

  4. 4.

    M. Jones, "JSON Web Key (JWK)," RFC 7517, IETF, May 2015. https://www.rfc-editor.org/rfc/rfc7517

  5. 5.

    Auth0, "Validate JSON Web Tokens," auth0.com, accessed June 2026. https://auth0.com/docs/secure/tokens/json-web-tokens/validate-json-web-tokens

FAQ

Firebase JWT and Custom Token Claims

For Firebase Authentication, ID tokens carry the signed user identity that client SDKs exchange with your backend.1 Custom tokens minted server-side via the Admin SDK have a different structure and must be exchanged for an ID token before use.2 Both can be decoded offline in the tool above.

Token types and structure

Firebase Authentication separates tokens into two distinct types that serve different stages of the auth flow. The ID token is what your client SDK holds after sign-in: a JWT issued by Firebase Authentication service. Custom tokens, by contrast, are minted server-side by the Admin SDK and sent to the client for exchange. Calling signInWithCustomToken() converts a custom token into an ID token, which the client then uses for all subsequent API calls.3 Sending a custom token directly to your backend will fail verification. Understanding this two-stage flow is essential because each token type has a different claim structure, issuer, and lifetime, and treating them interchangeably leads to authentication errors that are difficult to diagnose.

Key claims explained

Verifying a Firebase ID token reveals a firebase nested claim containing provider-specific sign-in details. sign_in_provider shows how the user authenticated: google.com, password, phone, or anonymous: enabling provider-specific flows in your application. The identities object within firebase maps each provider to the user's ID at that provider. Furthermore, sub and user_id always hold the same Firebase UID in an ID token, making them interchangeable for database lookups.1 Custom tokens use uid at the top level instead, because they lack the firebase nested claim.2 When your application needs to determine which authentication method a user employed, read the sign_in_provider value from the decoded firebase claim rather than attempting to infer it from other fields.

Signature verification

Firebase ID tokens use RS256, and manual verification must match the token header kid to Google's current public key set.4 The Firebase Admin SDK verifies and decodes ID tokens directly; manual verification must also check exp, iat, aud, iss, and sub, then use the Cache-Control header to know when to refresh regularly rotated public keys.4 For high-throughput applications, cache the verified UID in a short-lived session rather than re-verifying on every request. Google rotates its public keys on a regular schedule, so your verification code must handle key rotation gracefully by refreshing the JWKS cache whenever a token arrives with an unfamiliar kid value.

When custom tokens fail

Custom token failures usually happen before the client ever receives an ID token. If signInWithCustomToken() rejects the request, inspect the minted custom token in the tool above and check the service account issuer, uid, exp, and aud fields. A token can decode successfully and still be unusable if it was signed by the wrong service account or sent to the wrong Firebase project.

Troubleshooting custom token exchange

Compare the decoded uid with the Firebase Authentication record you expect. Also check whether your server code calls signInWithCustomToken() on the client and waits for the returned ID token before making API requests. The custom token is only a temporary bridge into Firebase Authentication, not a credential your backend should verify directly. A mismatched aud value is the most common reason a well-formed custom token fails at the exchange step, so always compare the decoded aud against your Firebase project ID before investigating service account configuration.

Adding custom claims to Firebase ID tokens

Adding custom claims uses the Admin SDK's setCustomUserClaims(uid, { key: value }) method called from your server.5 The method accepts JSON-serializable key-value pairs within a 1,000-byte total size limit, and the custom claims appear in the user's next ID token after their current token expires, which takes up to one hour.5 Call auth.revokeRefreshTokens(uid) immediately after setting claims to invalidate the current session and force a fresh token on next sign-in.

Reading custom claims efficiently

Read custom claims from the decoded token in your application rather than querying the Admin SDK on each request. The token carries the claims, so your API reads them without a database round-trip. Common patterns include a premium boolean, a subscription tier string, or a role array. Keep the total size under 1,000 bytes; exceeding it causes the setCustomUserClaims call to throw without a partial write. Structuring your custom claims as a single flat object with short keys keeps you well under that limit, while deeply nested objects with verbose key names consume the budget quickly and leave less room for future additions.

A flat claim structure also makes debugging simpler because every value appears at a predictable path in the decoded token. When a route needs a claim, reading a short top-level key is faster than walking a nested object and reduces the chance of a path typo. Keep the most frequently checked permissions at the root level so your API validation stays straightforward.

Firebase token revocation and forced re-authentication

Firebase token revocation invalidates a user's refresh tokens by calling auth.revokeRefreshTokens(uid) from the Admin SDK.6 After revocation, those refresh tokens no longer produce new ID tokens. Existing ID tokens remain valid for up to one hour because Firebase issues JWTs with fixed lifetimes and has no built-in token blocklist.6

Enabling immediate revocation on sensitive routes

For routes that require immediate revocation enforcement, pass { checkRevoked: true } to auth.verifyIdToken(token). This adds a network call to Firebase's token revocation service on every request to that route. Enable it only on endpoints where immediate invalidation is worth the latency cost: a logout endpoint, a payment route, or an admin action requiring fresh authentication. Leave it disabled on general API routes where the one-hour revocation window is an acceptable trade-off.

Notes

A Firebase ID token's iss takes the form https://securetoken.google.com/<project-id>. The sub and user_id claims both contain the Firebase UID: they are always identical. The firebase claim is a nested object with sign_in_provider (e.g., google.com, password, phone, anonymous) and optionally identities. Custom tokens have iss set to the service account email and exp of 1 hour.

Examples

Firebase ID token payload

{
  "iss": "https://securetoken.google.com/my-project",
  "aud": "my-project",
  "user_id": "abc123xyz",
  "sub": "abc123xyz",
  "exp": 1716998400,
  "firebase": {
    "identities": { "google.com": ["108512345"] },
    "sign_in_provider": "google.com"
  }
}

Firebase custom token payload

{
  "iss": "[email protected]",
  "sub": "[email protected]",
  "aud": "https://identitytoolkit.googleapis.com/...",
  "uid": "abc123xyz",
  "exp": 1716998400,
  "claims": { "premium": true }
}

Custom tokens must be exchanged for an ID token via signInWithCustomToken() before use in API calls.

Verify with the JWT Decoder & Claims Inspector tool.

Firebase ID token payload

{
  "iss": "https://securetoken.google.com/my-project",
  "aud": "my-project",
  "user_id": "abc123xyz",
  "sub": "abc123xyz",
  "exp": 1716998400,
  "firebase": {
    "identities": { "google.com": ["108512345"] },
    "sign_in_provider": "google.com"
  }
}
Sources
  1. 1.

    Google, "DecodedIdToken interface," firebase.google.com, July 2022. https://firebase.google.com/docs/reference/admin/node/firebase-admin.auth.decodedidtoken

  2. 2.

    Firebase, "FirebaseTokenGenerator source," github.com, accessed June 2026. https://github.com/firebase/firebase-admin-node/blob/main/src/auth/token-generator.ts

  3. 3.

    Google Cloud, "Signing in users with a custom authentication system," docs.cloud.google.com, June 2026. https://cloud.google.com/identity-platform/docs/web/custom

  4. 4.

    Google for Developers, "Verify the Google ID token on your server side," developers.google.com, December 2025. https://developers.google.com/identity/gsi/web/guides/verify-google-id-token

  5. 5.

    Firebase, "Control Access with Custom Claims and Security Rules," firebase.google.com, June 2026. https://firebase.google.com/docs/auth/admin/custom-claims

  6. 6.

    Google Cloud, "Managing Identity Platform tenants programmatically," docs.cloud.google.com, June 2026. https://cloud.google.com/identity-platform/docs/multi-tenancy-managing-tenants

FAQ

Google OAuth ID Token and Service Account JWT Claims

During Google sign-in, the user-facing credential is an ID token that proves the account behind the request.1 For server-to-server work, a service account creates a separate JWT and exchanges it for an access token.2 You can decode either form offline in the tool above.

Token types and structure

Google's OAuth 2.0 flow produces two JWT types with distinct purposes. The ID token is a signed assertion of user identity, containing sub, email, and optionally hd for Google Workspace users. Service account JWTs, by contrast, are self-signed tokens that your server creates using a service account private key and exchanges for a short-lived access token at Google's token endpoint. Because service account JWTs are never sent to end users, they carry no identity claims: only iss, sub (the service account email), and aud pointing at the token endpoint. Distinguishing between these two token types is critical because they serve different roles in the authentication flow, and using the wrong one leads to authorization failures that are difficult to trace.

Key claims explained

Inside a Google ID token, the sub claim is a stable, opaque numeric string tied to the Google Account. Changing the account's email address does not change sub, making it the correct primary key for your user database. The hd claim appears only for Google Workspace accounts and contains the hosted domain: use it server-side to restrict sign-in to your company. Conversely, a consumer Gmail account carries no hd, so an absent hd does not mean the token is invalid. The azp identifies the client that initiated the request.1 The email claim in a Google ID token reflects the user's current email address at the time of sign-in, but you should use sub as the stable identifier in your database because email addresses can change without notice.

Signature verification

Google ID token verification checks the RS256 signature with cached public keys downloaded from Google's public certificate endpoint.3 Match the kid in the token header to the correct key before verifying the signature. Service account JWTs use a different key source: the service account's own public key, accessible through Google's service account metadata endpoints and identified by the kid in the signed JWT header.4 Never skip signature verification; a decoded but unverified Google token provides no security guarantees. Google publishes its public keys at https://www.googleapis.com/oauth2/v3/certs and rotates them periodically, so your verification code must cache the key set and refresh it when an unknown kid appears.

Choosing between user and service tokens

Pick the token by job, not by name. A user ID token belongs to a browser sign-in and proves which Google Account is present. A service account JWT belongs to backend automation and proves which workload is calling Google. Confusing the two creates failures at the token endpoint or in your authorisation checks. Understanding which token type your application expects is the first step in writing correct verification logic, because the claims, issuer, and intended audience differ significantly between user-facing and service-to-service scenarios.

Matching claims to the request

Before trusting a Google token, compare iss, aud, sub, and exp with the flow that produced it. User tokens should point at your client ID and include user identity claims; service account tokens should point at the OAuth token endpoint and carry a service account email. That distinction keeps sign-in debugging separate from API automation. When a service account token arrives at a user-facing endpoint, the sub will contain a service account email rather than a Google Account numeric ID, which is a clear signal to reject the request before processing any downstream logic.

For Google Workspace: restricting sign-in by hd claim

For Google Workspace accounts, the hd claim in the ID token contains the hosted domain: yourcompany.com for example. Check hd server-side after signature verification to restrict sign-in to your organisation without an additional API call.5 Absent hd means a consumer Gmail account; reject these tokens if your application requires Workspace membership. Never check hd client-side, because a client that decodes without verifying the signature cannot trust the hd value.5 Validating the hd claim server-side ensures that only users from your organisation can access protected resources, which is a common requirement for internal tools and enterprise applications.

Using library-level domain validation

Google auth libraries advise validating that the returned ID token hd claim matches the expected domain. Pass your domain string through your verification flow and reject tokens whose hd claim does not match, removing the need for manual claim comparison in your application code. The Google Auth Library for Node.js accepts a hostedDomain parameter in the OAuth2Client constructor, and the Python google-auth package offers a similar verify_id_token option that checks hd automatically during the verification step.

Relying on the library rather than manual string comparison removes a common source of developer error across your codebase. The verification step runs the domain check as part of the same call that validates the signature, so a misconfigured domain fails closed instead of passing silently. Centralising this logic in one place makes future Google Auth Library upgrades safer to apply.

Service account token exchange

Because service account JWTs are self-signed by the service account's private key, your backend exchanges them for a short-lived access token at https://oauth2.googleapis.com/token before calling any Google API.2 The exchange produces a standard OAuth 2.0 access token, not a JWT. Google APIs accept this access token in the Authorization: Bearer header for the duration of the token's lifetime, typically one hour.

Automating the exchange and caching

The google-auth-library (Node.js) and google-auth (Python) automate the exchange and cache the resulting access token until it expires. Your application code calls the library's getRequestHeaders() method and receives a valid Authorization header without managing token lifetimes manually.6 Store the service account JSON key file outside your project repository and load it from an environment variable; never commit key files to source control.

Notes

A Google ID token's iss is always https://accounts.google.com. The sub is a unique numeric string tied to the Google Account (stable across email changes). The hd claim (hosted domain) appears only for Google Workspace accounts. Service account JWTs have iss set to the service account email and are self-signed with the service account's private key: they are exchanged for access tokens at the OAuth token endpoint.

Examples

Google ID token payload

{
  "iss": "https://accounts.google.com",
  "azp": "YOUR_CLIENT_ID.apps.googleusercontent.com",
  "aud": "YOUR_CLIENT_ID.apps.googleusercontent.com",
  "sub": "108512345678901234567",
  "hd": "yourcompany.com",
  "email": "[email protected]",
  "email_verified": true,
  "exp": 1716998400
}

Service account JWT payload

{
  "iss": "[email protected]",
  "sub": "[email protected]",
  "aud": "https://oauth2.googleapis.com/token",
  "iat": 1716994800,
  "exp": 1716998400
}

Service account JWTs are exchanged for access tokens: they are not sent directly to Google APIs.

Verify with the JWT Decoder & Claims Inspector tool.

Google ID token payload

{
  "iss": "https://accounts.google.com",
  "azp": "YOUR_CLIENT_ID.apps.googleusercontent.com",
  "aud": "YOUR_CLIENT_ID.apps.googleusercontent.com",
  "sub": "108512345678901234567",
  "hd": "yourcompany.com",
  "email": "[email protected]",
  "email_verified": true,
  "exp": 1716998400
}
Sources
  1. 1.

    Google for Developers, "OpenID Connect," developers.google.com, accessed June 2026. https://developers.google.com/identity/openid-connect/openid-connect

  2. 2.

    Google for Developers, "Using OAuth 2.0 for Server to Server Applications," developers.google.com, accessed June 2026. https://developers.google.com/identity/protocols/oauth2/service-account

  3. 3.

    Google Cloud, "Class GoogleIdTokenVerifier," docs.cloud.google.com, March 2026. https://docs.cloud.google.com/java/docs/reference/google-api-client/latest/com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier

  4. 4.

    Google Cloud, "Method: projects.serviceAccounts.signJwt," docs.cloud.google.com, May 2025. https://docs.cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/signJwt

  5. 5.

    googleapis, "OAuth2Client source," github.com, accessed June 2026. https://github.com/googleapis/google-auth-library-nodejs/blob/main/src/auth/oauth2client.ts

  6. 6.

    googleapis, "GoogleAuth source," github.com, accessed June 2026. https://github.com/googleapis/google-auth-library-nodejs/blob/main/src/auth/googleauth.ts

FAQ

Keycloak JWT Claims and Role Structure

In Keycloak, role-bearing tokens arrive as standard OpenID Connect JWTs with two role paths that matter for authorisation.1 The access token carries realm-level roles (realm_access) and client-specific roles (resource_access), so your API can decide what a caller may do without another database lookup.2

Token types and structure

Keycloak issues three token types in a standard OAuth 2.0 flow: an access token, an ID token, and a refresh token. The access token carries realm and client roles, enabling authorisation decisions without a database lookup. The ID token holds identity claims: name, email, preferred_username. Building on this, the refresh token (an opaque string, not a JWT) exchanges for fresh access and ID tokens when they expire. Keycloak's access token lifetime is configured in Realm Settings under Tokens, and client Advanced Settings can override the realm option.3 Understanding the distinction between these token types is essential because each serves a different purpose in the authentication flow, and using the wrong one leads to authorization failures.

Key claims explained

Two claims define Keycloak's role structure. realm_access.roles contains an array of roles that apply across the entire realm: think global permissions like offline_access or manage-users. resource_access maps client IDs to their own roles array, supporting per-API authorisation.2 Yet the stable user identifier is sub, which Keycloak maps with its Subject protocol mapper in the basic client scope. preferred_username holds the human-readable username, which Keycloak can map into tokens through protocol mappers. Store sub in your database as the permanent user key and treat preferred_username as display-only data that may shift over time.1 When your application needs to determine which roles a user holds, check both realm_access.roles and resource_access together because permissions can be assigned at either level.

Signature verification

Fetch the current JWKS from https://<keycloak-host>/realms/<realm>/protocol/openid-connect/certs and match the kid to verify the signature.4 Because Keycloak can rotate keys, never hard-code a public key: always resolve it from the JWKS endpoint. The jose (JavaScript) and PyJWT + cryptography (Python) libraries support dynamic JWKS resolution and handle key rotation automatically by refreshing the cache when an unknown kid appears in a token header.5 If your reverse proxy terminates TLS, configure Keycloak's public hostname so tokens, discovery metadata, and redirect URIs use the same frontend URL.

Handling key rotation gracefully

During a rotation event, Keycloak adds the new public key to the JWKS while keeping the old key active. Tokens signed with either key remain verifiable as long as both entries appear in the JWKS simultaneously. Once all tokens signed with the older key have reached their expiry, Keycloak removes the old entry from the set, at which point any new token will carry a kid that matches only the most recently added public key.

Reading role claims in the payload

When debugging authorisation, read roles as arrays rather than single strings. A user can receive several realm roles and several client roles at the same time, and a missing role may simply be in the other claim path. Decode a fresh token after changing role mappings so you inspect the same token your API will receive.

Comparing realm and client roles

Use realm roles for permissions that span the whole realm, then use resource_access roles for permissions tied to one client or API. If your API checks only realm_access.roles, it may reject users who have the right client-specific permission. If it checks only resource_access, it may miss global roles that should apply everywhere. Merging both arrays into a single permission set before testing for required roles ensures that neither claim path is accidentally overlooked when a user has a mix of realm-level and client-specific assignments.

In Protocol Mappers: adding custom claims

In Keycloak's client configuration, Protocol Mappers control which claims appear in access tokens and ID tokens. Navigate to Clients > your-client > Client Scopes > your-client-dedicated > Add Mapper > By Configuration to create a mapper. Choose User Attribute for custom user properties stored in Keycloak, Group Membership for group names, or Hardcoded Claim for a static value. Each mapper includes a Token Claim Name field that sets the exact key the claim uses in the token.3

Confirming mapper output after changes

Mapper changes take effect on the next token issuance; existing tokens carry the old claims until they expire naturally. Confirm the mapper output by requesting a fresh token and decoding it in the tool above. If the expected claim does not appear, check that the mapper targets the correct token type: access token, ID token, or both, since each checkbox is independent.

Testing the mapper against both token types prevents a situation where the claim appears in the ID token but not the access token your API consumes. A quick decode after saving the configuration shows the exact path, so you can point your policy at the correct claim without guessing. Repeat the check whenever you change the mapper target, because stale assumptions cause silent authorization failures.

Checking realm and client roles in application code

Checking Keycloak roles in your application requires reading from two distinct claim paths. Realm roles appear at token.realm_access.roles, an array of strings. Client roles appear at token.resource_access[your-client-id].roles, also an array. Code that checks only one path misses the other: write a helper function that merges both arrays before testing whether a required role is present. In Spring Boot, the Keycloak adapter populates SecurityContextHolder with both role sources when configured correctly, while in Node.js and Python applications, your code reads the raw decoded claims and applies array inclusion checks manually. Centralise role constant strings in a single module to make permission auditing straightforward; scattered literal role name strings across route handlers become difficult to maintain as the number of protected routes grows.

Notes

The iss claim takes the form https://<keycloak-host>/realms/<realm-name>. realm_access.roles contains realm-level roles. resource_access is an object keyed by client ID, each with a roles array. preferred_username is the Keycloak username (not the sub, which is a UUID and the stable identifier). The azp claim is the client ID of the requesting application. If iss does not match the public URL behind your reverse proxy, token validation will fail: set Frontend URL in Keycloak realm settings.

Examples

Keycloak access token payload

{
  "iss": "https://auth.yourapp.com/realms/myrealm",
  "sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "azp": "my-frontend-client",
  "preferred_username": "jane.smith",
  "realm_access": {
    "roles": ["user", "offline_access"]
  },
  "resource_access": {
    "my-api": {
      "roles": ["read", "write"]
    }
  },
  "exp": 1716998400
}

Verify with the JWT Decoder & Claims Inspector tool.

Keycloak access token payload

{
  "iss": "https://auth.yourapp.com/realms/myrealm",
  "sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "azp": "my-frontend-client",
  "preferred_username": "jane.smith",
  "realm_access": {
    "roles": ["user", "offline_access"]
  },
  "resource_access": {
    "my-api": {
      "roles": ["read", "write"]
    }
  },
  "exp": 1716998400
}
Sources
  1. 1.

    OpenID Foundation, "OpenID Connect Core 1.0," openid.net, December 2023. https://openid.net/specs/openid-connect-core-1_0.txt

  2. 2.

    keycloak, "Role mappings in the token," github.com, accessed June 2026. https://github.com/keycloak/keycloak/blob/main/docs/documentation/server_admin/topics/clients/oidc/con-token-role-mappings.adoc

  3. 3.

    Keycloak, "Server Administration Guide," keycloak.org, accessed June 2026. https://www.keycloak.org/docs/latest/server_admin/

  4. 4.

    keycloak, "Keycloak server OIDC URI endpoints," github.com, accessed June 2026. https://github.com/keycloak/keycloak/blob/main/docs/documentation/server_admin/topics/sso-protocols/con-server-oidc-uri-endpoints.adoc

  5. 5.

    Keycloak, "Configuring the hostname (v2)," keycloak.org, accessed June 2026. https://www.keycloak.org/server/hostname

FAQ

Auth0 sub Claim Format and Connection Prefixes

When an Auth0 token reaches your API, the sub claim is the first value worth parsing. It combines a connection-type prefix with a provider-specific user ID, separated by a pipe character.1 That prefix tells you which identity provider authenticated the user, which matters in multi-provider tenants where Google, GitHub, and email/password users coexist. The sub value is stable: an Auth0 user's sub does not change when they update email or link another social account. Paste any Auth0 token above to read the raw sub claim without sending the token to any server.

The prefix before the pipe follows Auth0's connection naming convention. Social connections use provider-specific strings: google-oauth2, facebook, github, twitter, apple. Database connections use auth0 as the prefix regardless of what you name the connection. Enterprise connections for SAML or ADFS use the connection name you configured in the Auth0 dashboard. Knowing the prefix lets you route users to provider-specific logic without an additional profile API call.

Token types and structure

Auth0 sub appears in both the ID token and the access token, making it the universal identifier across token types. The ID token sub identifies the user to your front-end; the access token sub carries the same value, letting your API server resolve user identity without inspecting the ID token. Yet the two tokens serve different audiences: the ID token travels only to the browser, while the access token goes to your API in the Authorization header. Both share the same sub value, ensuring consistent identity across the token pair. When debugging authentication issues, comparing the sub claim across both token types helps you confirm that the same user identity flows through the entire request chain.

Key claims explained

Building on the sub format, each prefix encodes the authentication method precisely. google-oauth2|<id> identifies a Google sign-in, where the numeric suffix is the Google Account ID. auth0|<alphanumeric> identifies a user in Auth0's built-in database, with the suffix being Auth0's internal user ID. github|<numeric> uses GitHub's integer user ID as the suffix, which remains stable even if the GitHub username changes. Enterprise SAML connections use the connection name directly as the prefix: for example, my-saml-conn|[email protected]: making the source obvious from the sub value alone. Understanding these prefix patterns lets you route users to provider-specific logic without making an additional profile API call, because the connection type encoded before the pipe character tells you exactly which identity provider handled the authentication.

Recognising M2M sub values

Machine-to-machine tokens use the client credentials flow, so sub contains the application client_id instead of a user identifier. If your API expects a pipe-separated user sub and receives a plain client_id, treat it as a service caller and apply a different permission path. Checking whether sub contains a pipe character is the fastest way to distinguish human sessions from machine callers in middleware that handles both token types.

Signature verification

Auth0 signs all production tokens with RS256, using asymmetric key pairs managed per tenant. Retrieve the signing key set from https://<tenant>.auth0.com/.well-known/jwks.json and select the key matching the kid claim in the token header. Consequently, you never need to distribute a shared secret: the public JWKS endpoint is sufficient for all verification.2 For Node.js, the jose library resolves JWKS and verifies RS256 signatures in a single API call, which means your middleware can validate every incoming Auth0 token without managing key material manually.3 Always verify signatures server-side; client-side decoding of the sub claim alone provides no authentication guarantee because the decoded payload has no cryptographic proof that Auth0 actually issued it.45

Account linking and sub claim stability across providers

Account linking in Auth0 connects two separate user records into a single identity under one primary sub. When a user signs in with both Google and GitHub, their primary identity's sub remains the token sub on every subsequent sign-in, regardless of which provider they authenticate with. The secondary identity's sub becomes inaccessible in the token; access it only through the Auth0 Management API's user identities array.

Sub stability in the database after linking

Sub stability after linking means your database records require no updates. The primary sub you stored at first sign-in continues to identify the user even after they link additional providers. Unlinking removes the secondary provider but does not change the primary sub. Test linking behavior in your development tenant before relying on sub as a foreign key across Auth0's linking flows; Management API calls are required for linking and cannot be performed from the browser.

Audit logging with provider prefix

For audit logging, store both the primary sub and the provider prefix you parsed from it. The primary sub remains the stable foreign key, while the prefix helps you explain which provider authenticated the account. This small distinction prevents support confusion when a user links another identity and the secondary provider no longer appears in the token. Logging the provider prefix alongside each authentication event also helps you detect when a user switches their primary sign-in method, which can indicate account linking activity or a potential security concern worth investigating.

Capturing the prefix at the same time as the sub means future audits can filter by identity provider without re-decoding historical tokens. The prefix alone explains whether a session used Google, GitHub, or a database connection, which is useful during incident reviews. Store both values together so that linking activity stays visible even after a user merges multiple accounts.

Notes

The sub claim format is |. Examples: google-oauth2|108512345 (Google), auth0|64a1b2c3d4 (database), github|9876543 (GitHub). The iss is always https://<tenant>.auth0.com/. The azp identifies the requesting client. For M2M tokens using Auth0's client credentials flow, sub is the application's client_id rather than a user identifier: the format switches from | to a plain alphanumeric client_id string.

Examples

Database user sub claim

{
  "sub": "auth0|64a1b2c3d4e5f6789",
  "iss": "https://yourapp.auth0.com/",
  "aud": "YOUR_CLIENT_ID",
  "exp": 1748736000,
  "iat": 1748732400
}

Social login sub claims by provider

// Google
"sub": "google-oauth2|108512345678901234567"

// GitHub
"sub": "github|9876543"

// Apple
"sub": "apple|001234.abcdef"

// M2M (client credentials)
"sub": "YOUR_CLIENT_ID"

M2M tokens issued via the Client Credentials flow set sub to the application client_id, not a user ID.

Verify with the JWT Decoder & Claims Inspector tool.

Database user sub claim

{
  "sub": "auth0|64a1b2c3d4e5f6789",
  "iss": "https://yourapp.auth0.com/",
  "aud": "YOUR_CLIENT_ID",
  "exp": 1748736000,
  "iat": 1748732400
}
Sources
  1. 1.

    Auth0, "Identify Users," auth0.com, accessed June 2026. https://auth0.com/docs/manage-users/user-accounts/identify-users

  2. 2.

    Auth0, "Validate JSON Web Tokens," auth0.com, accessed June 2026. https://auth0.com/docs/secure/tokens/json-web-tokens/validate-json-web-tokens

  3. 3.

    panva, "jose," github.com, accessed June 2026. https://github.com/panva/jose

  4. 4.

    OWASP Foundation, "JSON Web Token Cheat Sheet for Java," cheatsheetseries.owasp.org, accessed June 2026. https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html

  5. 5.

    M. Jones, J. Bradley, and N. Sakimura, "JSON Web Token (JWT)," RFC 7519, IETF, May 2015. https://datatracker.ietf.org/doc/html/rfc7519

FAQ

AWS Cognito JWT Claims and Token Types

If your API receives a Cognito token, token_use is the first gate to check. Cognito sends ID tokens and access tokens with different claim sets, and they are not interchangeable. The ID token carries user identity claims such as username, family name, and email; the access token carries scopes for Cognito user self-service API operations, third-party APIs, and the userInfo endpoint. The access token uses client_id where an ID token uses aud, and Cognito requires token_use to be either access or id during validation.1 Paste either token into the decoder above to read its full claim set without transmitting the token to any external service.

Cognito signs tokens with RS256 and exposes the user pool signing keys through a JWKS URI.2 Match the kid in the token header to the correct public key before verifying, and refresh your key cache periodically because Cognito might rotate signing keys. Always verify signatures server-side rather than relying on client-side decoding.

Token types and structure

AWS Cognito distinguishes its two JWT types through the token_use claim, not through different endpoints or headers. An ID token carries token_use: id alongside identity claims such as username, family name, email, user pool issuer, and app client audience. An access token carries token_use: access alongside OAuth 2.0 scopes, group membership, user pool issuer, and app client as client_id.3 Consequently, your authorizer should read token_use first and reject any token that does not match what you expect. Accepting an ID token where an access token is required exposes user attributes to services that should only see permissions, which creates a data-leakage vector in architectures where internal APIs share the same Cognito user pool as the public frontend.

Key claims explained

Inside a Cognito access token, cognito:groups contains the user's group membership. The sub claim identifies the user subject, and aws-jwt-verify can validate groups and scope on access tokens.3 In the ID token, client_id shifts to the aud claim, reverting to standard JWT semantics: a JWT represents claims as a JSON object in the token payload.4

Separating username from user ID

Do not treat cognito:username as the permanent database key. It is useful for display and debugging, but the stable subject value is sub. Store sub from the verified token and keep username as mutable profile data, which prevents account confusion when a user changes their display name or email. Cognito lets users update their preferred username through the self-service profile API, so any code that relies on cognito:username as a lookup key will break the moment a user edits that field.

Signature verification

Cognito signs tokens with RS256 using two per-user-pool RSA key pairs, one signing access tokens and the other signing ID tokens, with the public keys published at https://cognito-idp.<region>.amazonaws.com/<userPoolId>/.well-known/jwks.json. Match the kid in the token header to the correct key in this endpoint before verifying, then compare exp, iss, aud or client_id, and token_use before trusting the claims.1 The aws-jwt-verify library from AWS Labs handles all of these steps in a single function call, caching the JWKS response and automatically refreshing it when a new kid appears, which removes the need to manage key rotation logic in your own middleware code. For access tokens, remember that Cognito replaces aud with client_id, so your verification code must check client_id rather than aud when validating access tokens, while ID tokens retain the standard aud claim containing your app client ID.

Using cognito:groups for role-based access in Lambda authorizers

Lambda authorizers receive the full access token and must verify it before reading claims. After verifying the RS256 signature using the Cognito JWKS, read the cognito:groups array from the verified claims and compare it against the required groups for the route. Return an IAM policy with Effect: Allow when the user belongs to at least one required group, and Effect: Deny otherwise. The Lambda authorizer result caches at the API Gateway layer for the duration you configure.

JWKS caching in Lambda execution environments

Validate token_use: access before reading cognito:groups; ID tokens do not carry group memberships and should not authenticate API Gateway routes. Cache the JWKS response in the Lambda execution environment using a module-level variable: Lambda may retain an execution environment after a function finishes, which lets initialization code and cached data persist between warm invocations, but reuse is not guaranteed.5 Cold starts always fetch a fresh JWKS, so warm-start caching is the optimization that matters at scale.

Group membership timing and token lifetimes

Group names in cognito:groups reflect the groups assigned at token issuance. If an admin changes group membership while an access token is still valid, your authorizer will not see the new membership until the client obtains a fresh token. Pair short access-token lifetimes with Lambda authorizer caching that matches your security needs, especially for admin routes where membership changes must take effect quickly.

Combining a short access token lifetime with a brief authorizer cache gives you near real-time revocation without paying for a cache refresh on every request. When an admin changes group membership, the next token issuance reflects it and the cache expires the stale entry within minutes. Tune the cache duration to match how fast your security team needs membership changes to take effect.

Notes

The token_use claim (id or access) identifies which token type you have received. cognito:groups lists group memberships. cognito:username is the human-readable username (access token only). The access token uses client_id where an ID token would use aud. JWKS endpoint: https://cognito-idp.<region>.amazonaws.com/<userPoolId>/.well-known/jwks.json. origin_jti ties the access token to its originating refresh token, used for token revocation checks.

Examples

Cognito ID token payload

{
  "sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "aud": "YOUR_APP_CLIENT_ID",
  "token_use": "id",
  "cognito:username": "jane.smith",
  "email": "[email protected]",
  "email_verified": true,
  "exp": 1748736000
}

Cognito access token payload

{
  "sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "client_id": "YOUR_APP_CLIENT_ID",
  "token_use": "access",
  "cognito:username": "jane.smith",
  "cognito:groups": ["Admins", "Users"],
  "scope": "aws.cognito.signin.user.admin",
  "origin_jti": "refresh-token-jti-here",
  "exp": 1748736000
}

Access tokens use client_id instead of aud. Always check token_use before processing claims.

Verify with the JWT Decoder & Claims Inspector tool.

Cognito ID token payload

{
  "sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "aud": "YOUR_APP_CLIENT_ID",
  "token_use": "id",
  "cognito:username": "jane.smith",
  "email": "[email protected]",
  "email_verified": true,
  "exp": 1748736000
}
Sources
  1. 1.

    Amazon Web Services, "Verifying JSON web tokens," docs.aws.amazon.com, accessed June 2026. https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html

  2. 2.

    M. Jones, "JSON Web Key (JWK)," RFC 7517, IETF, May 2015. https://datatracker.ietf.org/doc/html/rfc7517

  3. 3.

    AWS Labs, "aws-jwt-verify," github.com, accessed June 2026. https://github.com/awslabs/aws-jwt-verify

  4. 4.

    M. Jones, J. Bradley, and N. Sakimura, "JSON Web Token (JWT)," RFC 7519, IETF, May 2015. https://datatracker.ietf.org/doc/html/rfc7519

  5. 5.

    Amazon Web Services, "Understanding the Lambda execution environment lifecycle," docs.aws.amazon.com, accessed June 2026. https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html

FAQ

Okta JWT Claims and Access Token Structure

A rejected API request often starts with a simple claim mismatch. In Okta, access tokens carry granted scopes in the scp claim array, which lists every OAuth 2.0 scope the user or application was authorized to receive.1 Okta issues both ID tokens (for the application) and access tokens (for resource servers), each with a different claim set. The decoder above reads both types offline, revealing the full structure including the uid, ver, and groups claims that make Okta tokens immediately recognizable.

Okta Authorization Servers are the source of all tokens. The default server lives at https://<org>.okta.com/oauth2/default; custom servers have their own issuer URL. The iss claim in every token identifies which authorization server issued it, enabling multi-server validation logic. Custom claims can be added through Okta's claim mapping policies, but groups only appear if explicitly added via a Groups claim in the authorization server configuration: it is not present by default.2

Token types and structure

Okta separates identity from authorization across two token types. The ID token carries the user's profile: email, name, preferred_username, and the Okta UID in sub. The access token carries granted scopes in scp and the internal Okta user ID in uid: note that uid in the access token is different from sub in the ID token despite referring to the same user. Consequently, your API should read uid from the access token for user resolution rather than parsing the ID token, since the uid value resolves to the Okta user record while sub may contain the user's email address. Both tokens include a ver claim set to 1, confirming the token format version that Okta issued.1

Key claims explained

Inside an Okta access token, scp contains an array of granted OAuth 2.0 scopes: for example, ['openid', 'profile', 'read:reports']. The uid claim holds Okta's internal user ID, distinct from the sub claim used in the ID token. Building on this, the groups claim lists group memberships, but only if you added a Groups claim in your authorization server policy; it does not appear automatically. Custom claims added through Okta's policy engine appear alongside standard claims with no special prefix, so namespace them to avoid collisions with future Okta claims.3

Avoiding claim collisions

Namespacing keeps your custom claims clear when Okta adds new standard fields later. Use a URL-like namespace or a product-specific prefix, then document the expected value type. A custom claim should answer one authorization question; if it needs several data points, keep the token small and fetch the rest server-side. Okta rejects un-namespaced custom claim names at the authorization server level, so a name like role fails while https://yourapp.com/role is accepted, enforcing the pattern before the token ever reaches your API.

Signature verification

Okta signs tokens with a JSON Web Key using the RS256 algorithm and publishes the current key set at https://<org>.okta.com/oauth2/default/v1/keys for the default authorization server. Match the kid in the token header to the correct entry before verifying the signature. A JWK Set is a JSON object with a keys array, and each JWK can carry members such as alg and kid so the verifier can select the right public key.45 The @okta/jwt-verifier package wraps this entire flow into a single verifyAccessToken call that handles JWKS fetching, caching, and key rotation automatically, which means your Node.js middleware can validate Okta tokens without writing any key-resolution logic by hand.

Configuring scopes and custom claims in Okta Authorization Servers

When you create a custom Authorization Server in Okta, the Claims tab controls which data appears in access tokens and ID tokens. Add a custom claim by specifying the name, the value expression (which can reference user profile attributes using the Okta Expression Language), and whether the claim appears in access tokens, ID tokens, or both. Claims configured here appear in every token issued by that server, making them available for access decisions without additional API calls.

Linking scopes to claims

Scopes limit the claims a client can request. Define a custom scope in the Scopes tab, then associate the scope with one or more claims: a claim only appears in the token when the client requested the scope that includes it. This design lets you issue minimal claims for read-only clients and richer claims for administrative clients using the same Authorization Server.

Requesting only the scopes a client needs keeps the token small and limits the data exposed if the token is intercepted. A read-only client receives a minimal claim set while an admin client requests the richer scopes it requires. This least-privilege approach also simplifies audits because the claims present in a token directly reflect the permissions the client was granted.

Testing claim configuration

When you test a new claim, request a fresh token after saving the configuration. Existing tokens keep the old claim set until they expire, so a dashboard change can look broken even when the configuration is correct. Use the decoder above to confirm the claim name, then confirm the API reads the verified token rather than a stale development token.2

Notes

Okta ID token iss: https://<org>.okta.com/oauth2/default. The scp claim in access tokens is an array of granted scopes. uid contains the Okta internal user ID (not the same value as sub). ver is always 1. The groups claim only appears when explicitly configured in the authorization server. Custom claims must be namespaced to avoid conflicts. JWKS: https://<org>.okta.com/oauth2/default/v1/keys.

Examples

Okta access token payload

{
  "ver": 1,
  "iss": "https://yourorg.okta.com/oauth2/default",
  "uid": "00u1a2b3c4d5E6F7H8",
  "sub": "[email protected]",
  "scp": ["openid", "profile", "read:reports"],
  "groups": ["Everyone", "Admins"],
  "exp": 1748736000
}

Okta ID token payload

{
  "ver": 1,
  "iss": "https://yourorg.okta.com/oauth2/default",
  "sub": "[email protected]",
  "email": "[email protected]",
  "preferred_username": "[email protected]",
  "name": "Jane Smith",
  "exp": 1748736000
}

The ID token sub is the user's login (email). The access token uid is Okta's internal user ID.

Verify with the JWT Decoder & Claims Inspector tool.

Okta access token payload

{
  "ver": 1,
  "iss": "https://yourorg.okta.com/oauth2/default",
  "uid": "00u1a2b3c4d5E6F7H8",
  "sub": "[email protected]",
  "scp": ["openid", "profile", "read:reports"],
  "groups": ["Everyone", "Admins"],
  "exp": 1748736000
}
Sources
  1. 1.

    Okta, "okta-jwt-verifier-js README," github.com, accessed June 2026. https://github.com/okta/okta-jwt-verifier-js/blob/master/README.md

  2. 2.

    Okta, "Customize tokens returned from Okta with custom claims," developer.okta.com, accessed June 2026. https://developer.okta.com/docs/guides/customize-tokens-returned-from-okta/main/

  3. 3.

    M. Jones, J. Bradley, and N. Sakimura, "JSON Web Token (JWT)," RFC 7519, IETF, May 2015. https://datatracker.ietf.org/doc/html/rfc7519

  4. 4.

    Okta, "/keys for Org Authorization Server," developer.okta.com, accessed June 2026. https://developer.okta.com/docs/api/openapi/okta-oauth/oauth/orgas/oauthkeys.md

  5. 5.

    M. Jones, "JSON Web Key (JWK)," RFC 7517, IETF, May 2015. https://datatracker.ietf.org/doc/html/rfc7517

FAQ

Azure AD JWT Claims and Microsoft Entra ID Tokens

For Microsoft Entra ID APIs, a valid token is only the starting point. The token carries the tenant ID (tid), the user's immutable object ID (oid), and either delegated scopes (scp) or application roles (roles), depending on whether a user or a service principal called your API.1 Paste any Azure AD or Microsoft Entra ID token above to read its full claim structure.

Microsoft Entra ID recommends the v2.0 endpoint for all new applications. Tokens from this endpoint carry a ver claim of '2.0' and include groups only when the group count is 200 or fewer: above that threshold, an overage indicator replaces the full list.1 The tid claim lets multi-tenant APIs restrict sign-in to approved tenants by comparing it against an allowlist. Understanding tid and oid together prevents the cross-tenant token acceptance vulnerability where a valid token from a different tenant grants access.

Token types and structure

Azure AD issues access tokens for your APIs and ID tokens for your applications, and the v2.0 endpoint handles both. Access tokens carry scp (delegated permissions) or roles (app permissions) but omit profile claims like name or email.2 ID tokens carry profile claims but lack scp.3 Consequently, your API should accept only access tokens: passing an ID token to an API is a common integration error that grants the calling application more access than intended.3 The ver claim tells you which endpoint issued the token: '1.0' for the legacy endpoint and '2.0' for the modern Microsoft Entra endpoint, which is the recommended choice for all new application registrations.

Key claims explained

Inside an Azure AD token, tid identifies the tenant: a UUID that lets multi-tenant APIs confirm the caller belongs to an approved organisation. The oid claim is the immutable object ID of the user within Azure AD; unlike upn or email, oid never changes even when a user moves between tenants or changes their email, which makes it the only reliable primary key for user records in any application that accepts tokens from Azure AD.

Building on this, scp contains space-separated delegated permission names for user-delegated access, while roles contains an array of application role names for app-to-app access. The groups claim lists Azure AD group memberships, but only when the user belongs to 200 or fewer groups; above that threshold, an overage indicator replaces the full list and your application must call the Microsoft Graph API to retrieve the complete set.4

Choosing scp or roles in your API

Read scp when a signed-in user delegated permission to your API. Read roles when a service principal or managed identity called your API with application permissions. Reject the token if the expected permission path is absent, because a valid signature does not mean the caller has the right permission. A token issued through the on-behalf-of flow may carry both scp and roles simultaneously, so your authorization logic should check the permission model that matches the calling context rather than assuming only one path will ever be present.

Signature verification

Azure AD signs tokens with RS256 and publishes its JWKS at https://login.microsoftonline.com/<tenantId>/discovery/v2.0/keys. For single-tenant apps, use the tenant-specific endpoint; for multi-tenant apps, use the common endpoint at https://login.microsoftonline.com/common/discovery/v2.0/keys. Microsoft rotates signing keys periodically, so always resolve the JWKS dynamically using the kid in the token header.5 The microsoft-authentication-library (MSAL) handles verification automatically in .NET and JavaScript applications, fetching the JWKS, caching the key set, and refreshing it on rotation without any manual configuration in your startup code.

Verify iss and tid before trusting any claims: an Azure AD token from an unexpected tenant is a security violation even if the signature is valid, because a cryptographically valid token from the wrong organisation must never grant access to your API. Combining MSAL verification with an explicit tid allowlist ensures that only tokens from your approved tenant pool can reach your business logic, which is the recommended pattern for multi-tenant SaaS applications that must isolate customer data.

Validating Azure AD tokens in ASP.NET Core with Microsoft.Identity.Web

Microsoft.Identity.Web is the recommended library for validating Azure AD tokens in ASP.NET Core. Add it via NuGet with dotnet add package Microsoft.Identity.Web, then call builder.Services.AddMicrosoftIdentityWebApi(builder.Configuration) in Program.cs. The library reads your AzureAd configuration section, fetches the JWKS from Microsoft's discovery endpoint, and configures TokenValidationParameters to enforce iss, aud, and tid automatically.6

Multi-tenant tid validation

For multi-tenant APIs that accept tokens from any Azure AD tenant, set ValidateIssuer to false in TokenValidationParameters and manually validate tid in a policy handler that compares it against your allowlist. Microsoft.Identity.Web exposes verified claims via HttpContext.User.Claims after the middleware runs; read tid with User.FindFirst("tid")?.Value. Never skip tid validation in multi-tenant applications: a valid token from an unintended tenant passes signature and iss checks if you accept the common endpoint.

Building a tenant allowlist

For tenant allowlists, load permitted tid values from configuration and compare them after Microsoft.Identity.Web verifies the token. Do not accept the common endpoint and skip this check, because a valid token from another tenant can still pass signature validation. Store the allowlist in deployment settings, log rejected tids, and test both single-tenant and multi-tenant tokens before release. Rotating the allowlist through environment variables or a secrets management service means you can add or revoke tenant access without redeploying application code, which is critical when responding to a compromised tenant that must be blocked immediately.

Loading the allowlist from deployment configuration means security teams can block a compromised tenant without waiting for an application release. A log entry per rejected tid gives you an audit trail of blocked attempts and helps detect patterns of cross-tenant probing. Treat the allowlist as secret-adjacent configuration and review changes through the same process you use for access control policy.

Notes

tid: Azure AD tenant ID UUID. oid: immutable user object ID: use as primary key, not upn. scp: space-separated delegated permission names. roles: array of app role names. Groups claim appears only when user is in ≤200 groups; otherwise an overage indicator replaces it. v2.0 endpoint recommended for new apps (ver: '2.0'). JWKS: https://login.microsoftonline.com/<tenantId>/discovery/v2.0/keys.

Examples

Azure AD v2.0 access token payload

{
  "ver": "2.0",
  "iss": "https://login.microsoftonline.com/<tenantId>/v2.0",
  "tid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "oid": "f1e2d3c4-b5a6-7890-abcd-ef0987654321",
  "scp": "User.Read Mail.Read",
  "roles": ["Reports.Read"],
  "exp": 1748736000
}

Azure AD groups overage indicator

{
  "_claim_names": {
    "groups": "src1"
  },
  "_claim_sources": {
    "src1": {
      "endpoint": "https://graph.microsoft.com/v1.0/users/<oid>/getMemberObjects"
    }
  }
}

When a user belongs to more than 200 groups, the groups claim is replaced by an overage indicator pointing to the Graph API endpoint.

Verify with the JWT Decoder & Claims Inspector tool.

Azure AD v2.0 access token payload

{
  "ver": "2.0",
  "iss": "https://login.microsoftonline.com/<tenantId>/v2.0",
  "tid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "oid": "f1e2d3c4-b5a6-7890-abcd-ef0987654321",
  "scp": "User.Read Mail.Read",
  "roles": ["Reports.Read"],
  "exp": 1748736000
}
Sources
  1. 1.

    Microsoft, "Access token claims reference," learn.microsoft.com, accessed June 2026. https://learn.microsoft.com/en-us/entra/identity-platform/access-token-claims-reference

  2. 2.

    Auth0, "Sample Use Cases: Scopes and Claims," auth0.com, accessed June 2026. https://auth0.com/docs/get-started/apis/scopes/sample-use-cases-scopes-and-claims

  3. 3.

    N. Sakimura, J. Bradley, M. Jones, B. de Medeiros, and C. Mortimore, "OpenID Connect Core 1.0 incorporating errata set 2," openid.net, December 2023. https://openid.net/specs/openid-connect-core-1_0.html

  4. 4.

    Auth0, "Choose a Connection Type for Azure AD," auth0.com, accessed June 2026. https://auth0.com/docs/authenticate/identity-providers/enterprise-identity-providers/choose-a-connection-type-for-azure-ad

  5. 5.

    Microsoft, "Signing Key Rollover in Microsoft identity platform," learn.microsoft.com, accessed June 2026. https://learn.microsoft.com/en-us/entra/identity-platform/signing-key-rollover

  6. 6.

    AzureAD Microsoft Identity Web contributors, "Web APIs," github.com, accessed June 2026. https://github.com/AzureAD/microsoft-identity-web/wiki/Web-APIs

FAQ

Supabase Auth JWT Claims and Row-Level Security

When Supabase row-level security blocks a query, the decoded JWT often explains why. User-facing Supabase JWTs commonly carry authenticated for signed-in users or anon for anonymous access, while service_role is reserved for server-side administration; aal, session_id, and metadata claims describe authentication strength, the auth session, and server-controlled or user-editable profile data.1 Paste any Supabase token above to inspect its full structure, including aal, session_id, and metadata claims, without sending the token to any external service or logging it locally.

Beyond the role claim, Supabase JWTs carry authentication assurance level in the aal claim. aal1 indicates single-factor authentication; aal2 indicates that MFA was successfully completed. Supabase supports legacy HS256 JWT secret signing and newer asymmetric signing keys; the legacy JWT secret is no longer recommended, and asymmetric signing publishes public keys through Supabase's JWKS discovery endpoint.

Token types and structure

Supabase Auth issues user-facing JWTs for signed-in users and anonymous access. Authenticated users receive role: authenticated, while anonymous public access uses role: anon; your RLS policies should restrict anonymous callers to public-read access only, because anonymous tokens carry no user identity and should never write data to tables that require an authenticated sub.

Building on this, the is_anonymous boolean claim appears when a user signs in via Supabase's anonymous sign-in feature, distinguishing anonymous-auth sessions from completely unauthenticated public API calls. This distinction matters when your application allows anonymous browsing but requires a conversion step to create permanent user records, since the sub value persists across the upgrade from anonymous to authenticated.

Key claims explained

Inside a Supabase token, aal (Authentication Assurance Level) communicates the strength of authentication. aal1 means the user provided a single factor (password or magic link); aal2 means they passed MFA. session_id is a UUID identifying the specific auth session, exp marks the access-token expiry timestamp, and is_anonymous appears for anonymous sign-in sessions.1

The app_metadata object carries server-controlled data like the authentication provider and provider user ID; your database or edge functions can read this but a user cannot modify it, which makes it the correct place to store subscription tiers, role assignments, or other authorization data that must remain under server control. Conversely, user_metadata holds data the user can update directly through the Supabase client SDK, such as display names, avatar URLs, or other profile fields that the user owns.

Matching claims to RLS checks

When a policy checks auth.uid(), the decoded sub must be the same UUID your row stores. When a policy checks app_metadata.plan or user_metadata.role, the decoded metadata object must contain the exact key and value. Small spelling differences in those paths are enough to make a valid token fail your policy. A policy that references auth.jwt() with a path like app_metadata.plan will return null instead of throwing an error when the key is misspelled, which silently blocks access and makes the root cause difficult to spot without decoding the token first.

Signature verification

Supabase supports legacy HS256 JWT secret signing and newer asymmetric signing keys; the legacy JWT secret is no longer recommended because any service that holds the shared symmetric key can forge arbitrary tokens, which eliminates the ability to trust claim values from external callers.2 For asymmetric signing, Supabase publishes public keys through the JWKS discovery endpoint at https://<ref>.supabase.co/auth/v1/.well-known/jwks.json, enabling external services to verify tokens without direct access to the private key.2 Always verify tokens on the server side, because client-side decoding provides no cryptographic proof that the claims were actually issued by Supabase. Migrating from HS256 to asymmetric signing is a one-time configuration change in the Supabase dashboard that immediately improves your security posture by ensuring that only the Supabase Auth service can sign new tokens while any number of downstream services can verify them.

Debugging row-level security policies with decoded tokens

Debugging Supabase RLS policies requires matching the decoded token claims to the PostgreSQL policy conditions. Paste the active token into the decoder above to read the exact sub value and role claim your current session carries, because any mismatch between the token payload and the policy expression will silently reject the query without an error message. Your RLS policy selects rows based on these values: auth.uid() returns the caller's user ID, and auth.jwt() exposes the full JWT claims for policy checks against app_metadata, user_metadata, and any custom fields the token carries.3 When a policy blocks access unexpectedly, confirm that the decoded sub matches the owner column value and that role is authenticated rather than anon before investigating expression logic.

Simulating requests in the SQL console

For new policies, test them in the Supabase dashboard's Table Editor SQL console by running SET request.jwt.claims = ''; then your SELECT query. This simulates controlled JWT claims while you debug the policy.3 Policies that look correct in isolation sometimes fail because the JSON path in the policy does not match the exact nesting of your app_metadata or user_metadata claims.

Diagnosing JSON path issues in policies

When a policy still fails after the sub value looks right, inspect the JSON path in the policy. Supabase policies often reference app_metadata or user_metadata with exact nesting, and a missing key behaves differently from a false value. Adjust the decoded payload simulation first, then update the policy so the path matches the claim structure your application actually writes. Running the simulation with a deliberately broken path confirms whether the policy fails open or fail closed, which tells you whether the issue is a missing path or a logic error in how the policy evaluates the returned value.

Decoding the simulated payload before editing the policy shows you exactly which key the policy expression will receive at runtime. A small mismatch between the expected nesting and the actual claim object is the most common cause of policies that silently block access. Re-running the simulation after each change keeps the test path aligned with the live token shape.

Notes

role claim: authenticated for signed-in users, anon for anonymous/unauthenticated. aal claim: aal1 = single factor, aal2 = MFA completed. session_id: UUID for this auth session. app_metadata: server-controlled (provider, provider_id): not modifiable by the user. user_metadata: user-editable. is_anonymous: boolean, present on anonymous sign-in sessions. JWKS (asymmetric signing mode): https://<ref>.supabase.co/auth/v1/.well-known/jwks.json.

Examples

Supabase authenticated user token

{
  "iss": "https://<ref>.supabase.co/auth/v1",
  "sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "role": "authenticated",
  "aal": "aal1",
  "session_id": "b2c3d4e5-f6a7-8901-bcde-f01234567890",
  "app_metadata": {
    "provider": "email",
    "providers": ["email"]
  },
  "user_metadata": { "name": "Jane Smith" },
  "exp": 1748736000
}

Supabase anonymous user token

{
  "role": "authenticated",
  "aal": "aal1",
  "is_anonymous": true,
  "session_id": "c3d4e5f6-a7b8-9012-cdef-012345678901",
  "sub": "d4e5f6a7-b8c9-0123-defa-123456789012",
  "exp": 1748736000
}

Anonymous sign-in tokens have role: 'authenticated' but also carry is_anonymous: true. Row-level security policies can use is_anonymous to apply separate access rules.

Verify with the JWT Decoder & Claims Inspector tool.

Supabase authenticated user token

{
  "iss": "https://<ref>.supabase.co/auth/v1",
  "sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "role": "authenticated",
  "aal": "aal1",
  "session_id": "b2c3d4e5-f6a7-8901-bcde-f01234567890",
  "app_metadata": {
    "provider": "email",
    "providers": ["email"]
  },
  "user_metadata": { "name": "Jane Smith" },
  "exp": 1748736000
}
Sources
  1. 1.

    Supabase, "JWT Claims Reference," supabase.com, accessed June 2026. https://supabase.com/docs/guides/auth/jwt-fields

  2. 2.

    Supabase, "JWT Signing Keys," supabase.com, accessed June 2026. https://supabase.com/docs/guides/auth/signing-keys

  3. 3.

    Supabase contributors, "Token Security and Row Level Security," github.com, accessed June 2026. https://github.com/supabase/supabase/blob/master/apps/docs/content/guides/auth/oauth-server/token-security.mdx

FAQ

Clerk JWT Token Format and Session Claims

A Clerk authorization failure often comes down to one missing or misread claim. Clerk session tokens are short-lived JWTs whose default claims include azp, exp, iat, iss, jti, nbf, sid, and sub; custom session claims or custom JWT templates can add organization or user data only when you configure them.1 Clerk M2M JWTs identify the machine subject, while interactive session tokens identify the current user, so sub helps distinguish human sessions from machine callers when the token format is available.2 Paste any Clerk token above to inspect the claim set offline; decoded claims are not verified claims, so verify tokens with Clerk SDK/backend flows before trusting them in production.3

Every Clerk session token can carry azp (authorized party), identifying the Origin that requested the token. Checking azp helps prevent cross-origin token reuse, but M2M JWTs and some privacy-sensitive session tokens may omit it. The sub claim holds the Clerk user ID for regular users and the machine subject for M2M JWTs. By default, Clerk tokens carry no role or groups claim: these must be explicitly added through custom claims or templates before they appear.

Token types and structure

Clerk issues session tokens for interactive users, and it can create M2M tokens for service-to-service communication in either opaque or JWT format. Session tokens identify the current user in sub, while M2M JWTs identify the machine subject without the user_ prefix. Because M2M tokens can be opaque by default, use the token format together with the sub shape when classifying callers rather than relying on sub alone, since the same sub field points to either a human user or a machine depending on the token type.2 Understanding this distinction is critical for middleware that handles both human sessions and machine-to-machine calls, because applying user-specific authorization logic to a service caller can grant unintended access or reject valid automated requests.

Key claims explained

Inside a Clerk session token, the default claims include azp, exp, iat, iss, jti, nbf, sid, and sub. azp contains the Origin that requested the token and can be omitted in privacy-sensitive cases; exp, iat, nbf, jti, sid, and sub describe expiry, issue time, validity window, token ID, session ID, and user subject. Custom session claims or JWT templates can add organization data, user metadata, email, or other values, but those claims only appear when your Clerk configuration explicitly includes them through the dashboard or API.

Distinguishing session and M2M subjects

Clerk session tokens usually identify a human account with sub values that start with user_. M2M JWTs can use the machine client ID instead, so the same field points to a different kind of actor. Check the surrounding token context before mapping sub to a user profile. A Clerk M2M JWT omits the azp claim entirely and uses a machine identifier in sub, so the absence of azp combined with a non-user_ prefix is a reliable signal that the caller is a service rather than a human session.

Reading session claims during debugging

When a Clerk-protected route rejects a request, decode the current session token before changing route code. Look for sid first, because it ties the token to a specific session and helps you confirm you are inspecting the active login. Then compare sub with the user ID shown in your dashboard or logs; user_ values identify human sessions, while machine subjects point toward M2M access.

Confirming the expected org claim

Organization data appears only when your Clerk configuration adds it through a custom claim or JWT template. If your API expects org_id or org_role, decode a token after the user has selected an organization and confirm the value appears exactly as your policy checks it. Missing org data usually means the token was issued before the template changed, so force a fresh session and try again.

Signature verification

A JWT decoder can read the encoded header and payload, but it cannot prove that an issuer signed the token or that the claims are still authorized. Treat decoded Clerk claims as debugging data until your backend verifies the token with Clerk's SDK or backend authentication flow. For session tokens, Clerk's SDK middleware can validate tokens automatically on protected routes, checking the signature against Clerk's published keys and rejecting any token that has been tampered with or that originates from a different Clerk instance.3 The verification step also checks the token's nbf and exp claims, ensuring that tokens are only accepted within their intended validity window, which prevents replay attacks using tokens that were captured after their expiry time.

Verifying Clerk session tokens in Next.js API routes

In Next.js App Router routes, use auth() from @clerk/nextjs/server to access the verified Auth object instead of trusting raw decoded claims. Clerk's session token documentation shows this route-handler pattern: await auth(), read isAuthenticated and userId, then reject the response when the user is not signed in. The same principle applies to Pages Router and other frameworks: let the Clerk SDK or backend flow perform verification, then use the decoded payload only to understand what the verified token contained.1 For middleware that runs on every request, Clerk's clerkMiddleware function handles session verification and token refresh automatically, which means your route handlers can trust that any request reaching them has already passed cryptographic validation.

Debugging M2M and custom claim flows

M2M tokens and custom session claims deserve separate checks. An M2M caller may be valid even when user_ sub and azp are absent, because those fields belong to interactive sessions. For custom claims, decode a fresh token after saving the template; existing sessions can continue to carry the previous claim set until refresh. This sequence keeps template mistakes separate from SDK verification problems.

Keeping decoded data out of trust decisions

Use decoded Clerk claims to choose the next backend check, not to make the check itself. If the payload suggests an org role, pass the request through Clerk verification and your own authorization code. If the payload suggests an M2M caller, validate the client credentials flow and application permissions. The decoder gives you context; your backend decides trust. Treating the decoded payload as a hint rather than a verdict keeps your authorization logic centralized in the Clerk SDK, which means a configuration change on the Clerk side immediately applies to every protected route without touching application code.

Keeping authorization in the Clerk SDK means a single verified path governs every route instead of scattered manual checks that drift over time. The decoded payload remains a debugging aid that tells you which verification step to run next, not a substitute for it. Route handlers stay simple because they trust the already-verified request context rather than re-reading raw claims.

Notes

Default session claims: azp, exp, iat, iss, jti, nbf, sid, sub. sub format: user_<id> for users. M2M JWTs: sub is the machine subject. No role or groups claim by default: add via custom claims or templates. iss: Clerk Frontend API URL. azp: Origin that requested the token; may be omitted.

Examples

Clerk session token payload

{
  "azp": "https://yourapp.com",
  "exp": 1748732460,
  "iat": 1748732400,
  "iss": "https://clerk.yourapp.com",
  "jti": "unique-token-id-abc123",
  "nbf": 1748732390,
  "sub": "user_2abc123def456"
}

Clerk session token with JWT Template claims

{
  "azp": "https://yourapp.com",
  "sub": "user_2abc123def456",
  "email": "[email protected]",
  "org_id": "org_abc123",
  "org_role": "admin",
  "exp": 1748732460
}

org_id and org_role appear only when a JWT Template includes them and the user is a member of an organization.

Verify with the JWT Decoder & Claims Inspector tool.

Clerk session token payload

{
  "azp": "https://yourapp.com",
  "exp": 1748732460,
  "iat": 1748732400,
  "iss": "https://clerk.yourapp.com",
  "jti": "unique-token-id-abc123",
  "nbf": 1748732390,
  "sub": "user_2abc123def456"
}
Sources
  1. 1.

    Clerk, "Session tokens," clerk.com, accessed June 2026. https://clerk.com/docs/guides/sessions/session-tokens

  2. 2.

    Clerk, "Using M2M tokens," clerk.com, accessed June 2026. https://clerk.com/docs/guides/development/machine-auth/m2m-tokens

  3. 3.

    IETF, "JSON Web Token (JWT)," rfc-editor.org, accessed June 2026. https://www.rfc-editor.org/rfc/rfc7519

FAQ