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 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]"
} Open in the tool → - 1.
Auth0, "Tokens," auth0.com, accessed June 2026. https://auth0.com/docs/secure/tokens
- 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.
M. Jones, J. Bradley, and N. Sakimura, "JSON Web Token (JWT)," RFC 7519, IETF, May 2015. https://www.rfc-editor.org/rfc/rfc7519
- 4.
M. Jones, "JSON Web Key (JWK)," RFC 7517, IETF, May 2015. https://www.rfc-editor.org/rfc/rfc7517
- 5.
Auth0, "Validate JSON Web Tokens," auth0.com, accessed June 2026. https://auth0.com/docs/secure/tokens/json-web-tokens/validate-json-web-tokens
The ID token is for your application and contains identity claims (name, email, picture). The access token is for your API and contains authorisation claims (scope, audience). The ID token is not intended to be sent to APIs.
It follows the format <connection>|<provider_id>. Examples: google-oauth2|1085123456 for a Google login, auth0|64a1b2c3d4e5f6 for a username/password user, github|9876543 for a GitHub social login.
When you request an access token for an API, Auth0 adds both your API identifier and the /userinfo endpoint to the aud array. An ID token always has aud as a single string: the application client_id.
Fetch the JWKS from https://<tenant>.auth0.com/.well-known/jwks.json and use the kid in the token header to select the correct key. Libraries like jose (JavaScript) or python-jose (Python) handle this.
Yes. CapyToolkit's Auth0 JWT decoder keeps the token local to your browser while you inspect claims. No token data is transmitted. You can disconnect from the internet after loading the page and the decoder still works.
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"
}
} Open in the tool → - 1.
Google, "DecodedIdToken interface," firebase.google.com, July 2022. https://firebase.google.com/docs/reference/admin/node/firebase-admin.auth.decodedidtoken
- 2.
Firebase, "FirebaseTokenGenerator source," github.com, accessed June 2026. https://github.com/firebase/firebase-admin-node/blob/main/src/auth/token-generator.ts
- 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.
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.
Firebase, "Control Access with Custom Claims and Security Rules," firebase.google.com, June 2026. https://firebase.google.com/docs/auth/admin/custom-claims
- 6.
Auth0, "Validate JSON Web Tokens," auth0.com, accessed June 2026. https://auth0.com/docs/secure/tokens/json-web-tokens/validate-json-web-tokens
A Firebase ID token is what the client SDK receives after sign-in and sends to your backend for verification. A custom token is minted by your server and sent to the client, which exchanges it for an ID token via signInWithCustomToken(). They are different stages of the same flow.
In an ID token, the UID appears in both sub and user_id: they are always identical. In a custom token it appears in the uid field at the top level.
Use the Firebase Admin SDK's auth.verifyIdToken(token) on your server. It fetches Google's public keys automatically and caches them. Never verify a Firebase token client-side.
It shows how the user authenticated: google.com for Google sign-in, password for email/password, phone for phone auth, anonymous for anonymous sessions, custom for custom token exchange.
Yes. Use auth.setCustomUserClaims(uid, { admin: true }) in the Admin SDK. Custom claims appear in the ID token after the next refresh (up to 1 hour). They are limited to 1000 bytes total.
Yes. CapyToolkit's JWT decoder runs in your browser and decodes the token locally. Paste only tokens you are allowed to inspect, then use your server and Firebase Admin SDK for verification.
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
} Open in the tool → - 1.
Google for Developers, "OpenID Connect," developers.google.com, accessed June 2026. https://developers.google.com/identity/openid-connect/openid-connect
- 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.
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.
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.
googleapis, "OAuth2Client source," github.com, accessed June 2026. https://github.com/googleapis/google-auth-library-nodejs/blob/main/src/auth/oauth2client.ts
- 6.
googleapis, "GoogleAuth source," github.com, accessed June 2026. https://github.com/googleapis/google-auth-library-nodejs/blob/main/src/auth/googleauth.ts
Yes. The sub is a unique opaque identifier tied to the Google Account itself, not the email address. Always use sub as the primary identifier in your database, not email.
hd stands for hosted domain. It only appears for Google Workspace accounts and contains the organisation domain. A consumer Gmail account has no hd claim. Verify hd server-side to restrict sign-in to your company domain.
Fetch public keys from https://www.googleapis.com/oauth2/v3/certs and verify the signature using the key matching the kid in the token header. Libraries like google-auth-library (Node.js) or google-auth (Python) handle this automatically.
aud identifies who the token is for (your app client ID). azp (authorised party) identifies who requested it. For a standard sign-in they are equal, but can differ when using service account delegation.
Google ID tokens expire after 1 hour. Use the refresh token to obtain a new ID token via the OAuth 2.0 token endpoint after expiry.
Yes. CapyToolkit's JWT decoder reads the token locally in your browser, which helps you inspect claims before server-side verification. Use the decoded payload for debugging, not as proof of trust.
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
} Open in the tool → - 1.
OpenID Foundation, "OpenID Connect Core 1.0," openid.net, December 2023. https://openid.net/specs/openid-connect-core-1_0.txt
- 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.
Keycloak, "Server Administration Guide," keycloak.org, accessed June 2026. https://www.keycloak.org/docs/latest/server_admin/
- 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.
Keycloak, "Configuring the hostname (v2)," keycloak.org, accessed June 2026. https://www.keycloak.org/server/hostname
realm_access.roles contains roles that apply across the entire realm. resource_access.<client-id>.roles contains roles specific to a particular client (your API or service). For per-API authorisation, check resource_access.<your-api>.roles.
The Keycloak username: typically an email or human-readable identifier. It can change if an admin updates the username. The sub (a UUID) is the stable permanent identifier; use it as the primary key in your database.
Fetch the JWKS from https://<keycloak-host>/realms/<realm>/protocol/openid-connect/certs and verify using the key matching the kid header. Keycloak rotates keys periodically: do not cache the JWKS indefinitely.
The iss is the full public URL Keycloak uses for the realm. If Keycloak is behind a reverse proxy, the iss must match the public-facing URL configured in Frontend URL settings. A mismatch causes validation failures.
Yes. Use Protocol Mappers in the client or realm settings to add custom claims from user attributes, hardcoded values, or JavaScript mappers. Custom claims appear immediately after the next sign-in.
Yes. CapyToolkit's JWT decoder reads the token locally in your browser, which is useful for checking claim paths like realm_access and resource_access. Keep signature verification on your server.
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 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 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
} Open in the tool → - 1.
Auth0, "Identify Users," auth0.com, accessed June 2026. https://auth0.com/docs/manage-users/user-accounts/identify-users
- 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.
panva, "jose," github.com, accessed June 2026. https://github.com/panva/jose
- 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.
M. Jones, J. Bradley, and N. Sakimura, "JSON Web Token (JWT)," RFC 7519, IETF, May 2015. https://datatracker.ietf.org/doc/html/rfc7519
The prefix identifies the connection type. google-oauth2 means a Google social login, auth0 means a database connection (username/password), github means GitHub OAuth, and enterprise connection names appear as configured in your Auth0 dashboard. Parsing this prefix lets you apply provider-specific logic without an extra API call.
No. The sub is an immutable identifier tied to the Auth0 user record, not to the email address. Changing an email, linking a new social account, or resetting a password all leave the sub unchanged. Store sub as your primary key rather than email.
No. Each linked identity keeps its own sub format. The user's primary identity determines the sub in the token. Auth0's Identity Linking feature merges accounts under one user record, but the sub reflects the primary connection that was used at sign-in.
Only for debugging. CapyToolkit's JWT decoder shows the raw sub without confirming the token came from Auth0. In production, always verify the signature server-side: only then can you trust the sub claim to identify a user.
M2M tokens issued via the Client Credentials flow set sub to the application's client_id: a plain alphanumeric string with no pipe separator. This distinguishes M2M tokens from user tokens and lets your API detect machine callers by checking whether sub contains a pipe.
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
} Open in the tool → - 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.
M. Jones, "JSON Web Key (JWK)," RFC 7517, IETF, May 2015. https://datatracker.ietf.org/doc/html/rfc7517
- 3.
AWS Labs, "aws-jwt-verify," github.com, accessed June 2026. https://github.com/awslabs/aws-jwt-verify
- 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.
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
Cognito replaced aud with client_id in access tokens to align with older OIDC client validation patterns. Your backend must check client_id instead of aud when validating access tokens. ID tokens retain the standard aud claim containing your app client ID.
Read cognito:groups in the access token. It contains an array of the group names the user belongs to. You must explicitly request groups in your token configuration; they do not appear by default in all setups.
Check the token_use claim first. It contains the string id for ID tokens and access for access tokens. Reject any token where token_use does not match what your endpoint expects before reading any other claims.
AWS warns that Cognito might rotate signing keys, so resolve the JWKS dynamically rather than hard-coding any key. Cache the public keys using the kid as the cache key and refresh the cache periodically. If you receive a token with the correct issuer but an unfamiliar kid, re-fetch the JWKS before rejecting it.
Yes. CapyToolkit's Cognito JWT decoder keeps a production token on your device while you inspect token_use, client_id, and groups. No token data leaves your device, making it safe to paste tokens for debugging.
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
} Open in the tool → - 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.
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.
M. Jones, J. Bradley, and N. Sakimura, "JSON Web Token (JWT)," RFC 7519, IETF, May 2015. https://datatracker.ietf.org/doc/html/rfc7519
- 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.
M. Jones, "JSON Web Key (JWK)," RFC 7517, IETF, May 2015. https://datatracker.ietf.org/doc/html/rfc7517
uid is Okta's internal user ID: an alphanumeric string starting with 00u. It differs from sub, which in the access token contains the user's login (usually their email). Use uid to look up users in Okta's API and sub to identify them in your application database.
Configure a Groups claim in your Okta Authorization Server under the Claims tab. Without this configuration the groups claim does not appear in tokens, even when users belong to groups. Once added, Okta injects the array of group names on every token issued by that server.
Cache the JWKS aggressively: Okta provides Cache-Control headers on the JWKS endpoint. Refresh the cache only when you encounter an unknown kid, which signals a key rotation. Libraries like @okta/jwt-verifier handle this automatically using a stale-while-revalidate pattern.
Yes. Custom authorization servers have their own issuer URL in the format https://<org>.okta.com/oauth2/<authServerId>. Tokens from a custom server have a different iss and JWKS endpoint. Ensure your application validates iss against the correct server for each token type.
Yes. CapyToolkit's JWT decoder reads the token's Base64Url-encoded payload locally in your browser with no network calls. Use it for inspection, then verify signatures and issuer server-side before trusting the token.
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
} Open in the tool → - 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.
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.
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.
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.
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.
AzureAD Microsoft Identity Web contributors, "Web APIs," github.com, accessed June 2026. https://github.com/AzureAD/microsoft-identity-web/wiki/Web-APIs
Store oid. The user principal name (upn) changes when a user renames their account or moves between tenants. The oid is an immutable UUID assigned to the user object in Azure AD and never changes throughout the account's lifetime.
Read the tid claim from the verified token and compare it against your allowlist of permitted tenant IDs. Reject any token whose tid does not match. An Azure AD token from an unexpected tenant is cryptographically valid but should not grant access to your API.
scp (space-separated string) holds delegated permissions granted by a user consent. roles (array) holds application permissions granted by admin consent without a user. Checking both covers all permission models. A token can contain scp, roles, or both depending on the grant type.
The groups claim is replaced by an overage indicator containing _claim_names and _claim_sources with a URL pointing to the Microsoft Graph getMemberObjects endpoint. Your application must call this endpoint to retrieve the full group list. Build your group-checking logic to handle this case.
Yes. CapyToolkit decodes tokens entirely in your browser. No token data is sent anywhere. You can disconnect from the internet after loading the page and decoding still works.
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 = '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.4 Adjust the decoded payload simulation first, then update the policy so the path matches the claim structure your application actually writes.5 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
} Open in the tool → - 1.
Supabase, "JWT Claims Reference," supabase.com, accessed June 2026. https://supabase.com/docs/guides/auth/jwt-fields
- 2.
Supabase, "JWT Signing Keys," supabase.com, accessed June 2026. https://supabase.com/docs/guides/auth/signing-keys
- 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
- 4.
PostgreSQL Global Development Group, "9.16. JSON Functions and Operators," postgresql.org, accessed September 2026. https://www.postgresql.org/docs/current/functions-json.html
- 5.
PostgreSQL Global Development Group, "5.9. Row Security Policies," postgresql.org, accessed September 2026. https://www.postgresql.org/docs/current/ddl-rowsecurity.html
Supabase sets a PostgreSQL configuration parameter (request.jwt.claims) containing the full JWT payload for each request. Your RLS policies can call auth.role() (which reads role from this parameter) to check whether the caller is authenticated or anon. You can also call auth.uid() to get the sub claim as a UUID.
No. When a user converts an anonymous session to a permanent account by linking an email or OAuth provider, Supabase preserves the original sub UUID. Rows created during the anonymous session remain accessible after the upgrade.
Never share the JWT secret. It is a symmetric key: anyone who holds it can forge any JWT claim, including role: authenticated with any sub value. Treat it like a database password. The Supabase service key provides admin-level API access and should remain server-side only.
aal2 indicates the user completed a second authentication factor: typically a TOTP authenticator app: in addition to their primary login. Supabase Authenticator Assurance Level 2 requires explicit MFA configuration in your project settings. Use it to gate sensitive operations behind a stronger authentication requirement.
Yes. CapyToolkit's Supabase JWT decoder lets you safely paste a Supabase JWT while keeping claims local in your browser. No token data is transmitted to any server. Supabase JWTs do not contain database credentials: they contain user identity and metadata, which is safe to inspect offline.
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. The client credentials grant behind these tokens is the standard machine-to-machine pattern in OAuth 2.0, where the client authenticates directly with the authorization server rather than acting on a resource owner's behalf.4
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. OWASP guidance makes the same point about session tokens: once authenticated, a session ID is temporarily equivalent to the strongest authentication method the application used, so disclosure, capture, or prediction of it leads directly to session hijacking.5
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"
} Open in the tool → - 1.
Clerk, "Session tokens," clerk.com, accessed June 2026. https://clerk.com/docs/guides/sessions/session-tokens
- 2.
Clerk, "Using M2M tokens," clerk.com, accessed June 2026. https://clerk.com/docs/guides/development/machine-auth/m2m-tokens
- 3.
IETF, "JSON Web Token (JWT)," rfc-editor.org, accessed June 2026. https://www.rfc-editor.org/rfc/rfc7519
- 4.
D. Hardt, "The OAuth 2.0 Authorization Framework," RFC 6749, IETF, October 2012. https://www.rfc-editor.org/rfc/rfc6749
- 5.
OWASP Cheat Sheet Series, "Session Management," cheatsheetseries.owasp.org, accessed September 2026. https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
azp identifies the frontend domain that requested the token. Your API should verify azp matches your expected frontend origin to prevent a malicious site from reusing a user's token. Clerk sets azp to the full origin URL: for example, https://yourapp.com: not just the hostname.
Create a JWT Template in the Clerk dashboard and add a custom claim that maps to your organization role or a custom metadata field. Without a JWT Template, no role claim appears. Clerk does not include any authorization claims by default: identity only.
Clerk's frontend SDK manages token refresh for active sessions. Your application code should read the current token from the SDK at request time instead of caching a decoded token for longer than needed.
M2M tokens use the machine client ID in sub rather than a user_<id> string, have no azp claim, and are issued via a client credentials-style flow. Your API can detect M2M callers by checking whether sub starts with user_.
Yes. CapyToolkit's JWT decoder runs in your browser with no network calls. Clerk session tokens contain user identity claims but no session secrets, making them safe to inspect for debugging.
Decode a JWT in Python with PyJWT
In Python projects, PyJWT is the standard library for JWT decoding and verification, with hundreds of millions of monthly PyPI downloads. Version 2.13.0, released May 2026, is the current stable release. A basic install: pip install PyJWT: has no required runtime dependencies for HMAC algorithms, making it lightweight for decode-only use cases.1 Paste any token into the decoder above for instant offline inspection without writing a single line of Python, or follow the patterns below to integrate token reading into your application.
The safer choice for server-side token handling is always offline browser inspection during debugging. Pasting a JWT into a server-backed tool transmits it to a third party. Because the decoder above runs entirely in your browser, your production tokens never leave your machine. For application code, PyJWT handles both the decode-without-verify pattern for debugging and full JWKS-based verification for production: you choose which mode to call based on whether you need security guarantees.
What to look for
- PyJWT version referenced 2.13.0
- Minimum Python version 3.9+
The cryptography extra is only needed for RS256/ES256 verification, not for HMAC algorithms.
Opens the JWT Decoder & Claims Inspector with this section's reference values shown at the top of the tool.
Open in the tool →Library and setup
Install PyJWT with pip install PyJWT. For full JWKS-based signature verification, add the cryptography extra: pip install PyJWT[cryptography]. The cryptography package provides the RSA and EC key handling that RS256 and ES256 require. PyJWT follows semantic versioning: pin to a major version (PyJWT>=2,<3) in your requirements.txt to avoid breaking changes. Importing the library requires import jwt; no submodule import is needed for basic use. The jwt namespace provides all the functions you need for both decode and verify operations, including decode, get_unverified_header, and the PyJWKClient class that handles JWKS fetching and key rotation automatically.2
Keeping decode and verify separate
Use decode-only calls when you are diagnosing a token shape in development. Use verified decode in middleware, background jobs, and any code that grants access. The same library supports both modes, but the security boundary changes as soon as you remove verify_signature. A decode-only call in production middleware that grants access based on unverified claims effectively turns your API into an open endpoint, because any client can craft a JWT with arbitrary sub, iss, and role values that your server would accept without checking the cryptographic proof.
Decoding without verification
Reading a JWT payload without checking the signature uses jwt.decode() with a special options dict. Calling jwt.decode(token, options={'verify_signature': False}) returns the payload as a Python dict with all claims accessible by key. To read only the header before decoding the payload, call jwt.get_unverified_header(token): it returns the alg, kid, and typ fields without touching the payload.2 Consequently, reading the kid first lets you select the correct verification key from a JWKS before performing the full decode and verify call, which is the recommended two-step pattern when your middleware needs to log the algorithm and key ID before committing to the more expensive cryptographic verification step.
Reading specific claims
After decoding, access claims by key like any Python dict: payload['sub'] for the subject, payload.get('exp') for expiry, payload.get('email') for the email claim. Using .get() instead of direct key access avoids KeyError when a claim is absent, which is especially important for optional claims like email_verified or azp that some providers omit entirely. Furthermore, PyJWT's verify path auto-converts exp to a datetime and raises DecodeError if the token is expired: you get claim reading and expiry validation in one call. For custom or provider-specific claims, access them directly from the payload dict using whatever key the issuer used, always wrapping the access in a .get() call or a try/except block so that a missing custom key does not crash your request handler.
Full JWKS-based verification with PyJWT
For full RS256 verification using a JWKS endpoint, install PyJWT with the cryptography extra and use the built-in PyJWKClient class. Instantiate the client with your provider's JWKS URI, then call get_signing_key_from_jwt(token) to retrieve the matching signing key. Pass signing_key.key to jwt.decode() alongside your expected algorithm list, audience, and issuer to perform complete verification, which gives you cryptographic proof that the token was actually issued by your configured provider rather than constructed by an attacker.
ES256 and JWKS cache refresh
PyJWKClient caches the JWKS response and refreshes it automatically when it encounters an unknown kid in a token header.3 For ES256 tokens from providers that use ECDSA signing, change the algorithms parameter from RS256 to ES256; the same PyJWKClient approach handles key selection and caching without further code changes. ES256 signatures are substantially smaller than RS256 at equivalent security levels, which reduces token size and improves throughput in high-volume APIs that process thousands of authenticated requests per second, making it the preferred choice for latency-sensitive microservice architectures.4
Handling PyJWT exceptions in production code
Because PyJWT raises distinct exception classes for each failure mode, your exception handler can return the correct HTTP status for each case.5 Catching jwt.ExpiredSignatureError signals an expired token: return a 401 response and prompt the client to refresh. Catching jwt.InvalidAudienceError signals an aud mismatch: return a 403. Catching jwt.DecodeError covers malformed tokens that are not valid JWTs: return a 400 to indicate a client-side formatting issue. Mapping each exception to a specific HTTP status code ensures that well-behaved clients can implement the correct recovery strategy automatically rather than treating every 401 as a permanent failure.
Logging exception context without raw token data
Catch all three exception types separately rather than using a broad except Exception block. Separate handlers let you log each type at the correct severity: expired tokens are normal and expected; malformed tokens at high volume may indicate a client bug or a probing attempt. Log the exception class and the token's iss and sub alongside the error to aid debugging without logging the full raw token string, which keeps your log storage compliant with data-retention policies that restrict how long credential-adjacent data can persist.
Separating the exception types also helps your monitoring system alert on the right severity without manual triage. Expired tokens are routine and need no page, while a sudden spike in malformed tokens can indicate an attacker probing your endpoints. Before you add that logging, paste the failing token into the decoder for PyJWT exception handling patterns, to see exactly which claim triggered the exception, without writing it to a log file at all. Keeping the raw token out of the log line protects users and satisfies auditors who review what your systems retain.
When to use this
Use PyJWT when your Python backend needs to read or verify JWTs from any provider. The decode-without-verification pattern suits debugging and testing; the full verify path suits middleware that protects API routes.
Examples
Decode JWT payload without signature verification
import jwt
token = "eyJ..."
payload = jwt.decode(
token,
options={"verify_signature": False}
)
print(payload["sub"])
print(payload.get("email")) Useful for reading claims during development or test setup without needing the signing key.
Read only the JWT header (no payload decode)
import jwt
token = "eyJ..."
header = jwt.get_unverified_header(token)
print(header["alg"]) # e.g. RS256
print(header.get("kid")) # key ID for JWKS lookup Use get_unverified_header to select the correct JWKS key before full verification.
- 1.
PyPI, "PyJWT 2.13.0," pypi.org, May 2026. https://pypi.org/project/PyJWT/
- 2.
PyJWT, "API Reference," pyjwt.readthedocs.io, accessed June 2026. https://pyjwt.readthedocs.io/en/latest/api.html
- 3.
jpadilla/pyjwt, "jwt/jwks_client.py," github.com, accessed June 2026. https://github.com/jpadilla/pyjwt/blob/master/jwt/jwks_client.py
- 4.
M. Jones, "JSON Web Algorithms (JWA)," RFC 7518, IETF, May 2015. https://datatracker.ietf.org/doc/html/rfc7518
- 5.
jpadilla/pyjwt, "jwt/exceptions.py," github.com, accessed June 2026. https://github.com/jpadilla/pyjwt/blob/master/jwt/exceptions.py
jwt.decode processes the payload and optionally verifies the signature. jwt.get_unverified_header reads only the header section without touching the payload. Use get_unverified_header when you need the kid or alg value to select a verification key before calling decode.
PyJWT with the cryptography extra handles RS256 and ES256 verification once you supply the public key. For automatic JWKS fetching and key rotation, combine PyJWT with the jwcrypto or python-jose package, or use a provider-specific SDK that manages JWKS internally.
Yes. Disabling signature verification also disables all other validation: exp, nbf, aud, and iss checks are skipped. Use this pattern only in development or testing contexts where you control the token source. Production code must verify the signature before trusting any claim.
Wrap the decode call in a try/except block catching jwt.ExpiredSignatureError for expired tokens and jwt.DecodeError for malformed tokens. ExpiredSignatureError is a subclass of DecodeError, so catching DecodeError covers both. Check the exception message to distinguish the two causes.
PyJWT 2.13.0 requires Python 3.9 and above. Versions 2.10.0 and later dropped Python 3.8 support. The library has no external runtime dependencies for HMAC algorithms: the cryptography extra is only needed for RSA and EC key types used by RS256, ES256, and similar asymmetric algorithms.
Yes. CapyToolkit's JWT decoder reads the token locally in your browser, so it is useful for checking claim names before writing Python. Keep PyJWT in your backend for verified decode and production validation.
Decode a JWT in JavaScript and Node.js
When debugging in JavaScript, the right decode path depends on where the code runs. jose is the modern ESM-compatible choice for browsers, Deno, and edge runtimes;1 jsonwebtoken is the established CommonJS library for Node.js backends.2 Both provide a decode-only path that returns raw payload claims without signature verification. The decoder above inspects any token offline in your browser: no library installation needed. For programmatic use in your application, the patterns below cover both libraries.
Pasting a JWT into a server-backed tool transmits the full token to a third party. Browser-based decoding eliminates this risk entirely. Because jose's decodeJwt() runs in the browser with zero network calls, your production tokens never leave your machine during debugging. For Node.js backends, jsonwebtoken's jwt.decode() with complete: true returns header, payload, and signature fields in one object: useful for building middleware that inspects claims before forwarding to a handler.
What this page covers
- jose decodeJwt() browser/ESM-friendly, no key required for reading claims
- jsonwebtoken jwt.decode() Node.js only; pass { complete: true } to also read the header
Opens the JWT Decoder & Claims Inspector with this section's checklist shown at the top of the tool.
Open in the tool →Library and setup
Install jose with npm install jose for browser and Deno projects, or use it in Node.js via ESM imports. For CommonJS Node.js code, install jsonwebtoken with npm install jsonwebtoken. jose exports named ESM functions: import { decodeJwt, decodeProtectedHeader } from 'jose': making tree-shaking effective in bundled applications.3 jsonwebtoken exports a default object: const jwt = require('jsonwebtoken'): following the CommonJS pattern.2 Both libraries are well-maintained; jose is the actively recommended choice for new projects, while jsonwebtoken remains widely deployed in existing codebases.
Choosing the runtime before the API
Pick the library by runtime first. Browser and edge code should use jose because it ships as ESM and avoids Node-specific APIs. Existing CommonJS backends can keep jsonwebtoken where it already works, but new ESM services should treat jose as the default. This prevents accidental imports that break bundling or serverless builds. A common mistake is importing jsonwebtoken into a Next.js edge middleware file, where Node.js-specific modules like crypto and buffer are unavailable at runtime, causing cryptic build failures that take significant time to trace back to the wrong library choice.
Decoding without verification
In jose, calling decodeJwt(token) returns the payload claims as a plain JavaScript object. For the header, call decodeProtectedHeader(token) to read alg, kid, and typ separately.3 In jsonwebtoken, jwt.decode(token, { complete: true }) returns an object with header, payload, and signature properties: the complete option is required to access the header.4 Neither call makes a network request or checks the signature. Consequently, both are safe for use during debugging and testing, but neither should gate access control decisions without a subsequent signature verification step, because a decoded payload without a verified signature is indistinguishable from one that an attacker constructed locally with arbitrary claims.
Reading specific claims
After decoding, access claims directly by property name. In jose, decodeJwt(token).sub gives the subject claim; decodeJwt(token).exp returns the expiry as a Unix timestamp in seconds. In jsonwebtoken's complete mode, payload.sub and payload.exp follow the same pattern. Building on this, the exp timestamp is always a number representing seconds since the Unix epoch, so you must multiply by 1000 to compare against Date.now() in milliseconds, which is the single most common cause of JWT expiry bugs in JavaScript applications.5 Custom claims from provider-specific fields like cognito:groups or realm_access appear in the decoded object using the same dot-notation access pattern. For nested objects, inspect the full payload before assuming a flat claim path.
Full signature verification with jose in Node.js
For full RS256 verification in Node.js, import jwtVerify and createRemoteJWKSet from jose. Create the JWKS resolver by calling createRemoteJWKSet with your provider's JWKS URL, then call jwtVerify(token, jwkSet, { issuer, audience }) to verify the signature and validate registered claims in one step. The function returns the decoded payload only on success; any failure throws an error you catch and translate into a 401 response.
JWKS caching and key rotation
Pass your expected issuer and audience strings to the jwtVerify options object. Providing these parameters removes the need for manual claim checking after verification. The jose library caches the JWKS response internally and refreshes automatically when it encounters an unknown kid, absorbing key rotation without any additional configuration in your middleware.6 During a rotation event, the library serves stale cached keys for existing tokens while fetching the new key set in the background, which means zero downtime for your API even when the provider rotates keys during peak traffic.
Keeping decode and verify paths separate
In middleware, keep the decode-only path separate from the verified path: decode the token first when you need to log claims or choose a tenant, then run jwtVerify before you trust the value. This split keeps debugging fast while preserving the security boundary that your API route must enforce, and keeping this helper near your auth middleware ensures future changes do not reintroduce decode-only access checks.
This separation also makes testing easier because you can exercise the decode path in unit tests without standing up a full verification backend. The verified result then flows into your existing authorization code that already expects a trusted token. Documenting the boundary next to the middleware prevents a future contributor from collapsing the two steps back into one insecure call. You can confirm a claim shape in decoding JWTs with jose or jsonwebtoken before writing either library's code.
When to use this
Use jose when building browser extensions, Next.js edge middleware, or Deno services that decode tokens client-side. Use jsonwebtoken when maintaining an existing Node.js codebase that already imports it. The decode-only approach matches jose's decodeJwt(), so you can confirm a claim shape in the browser before writing either library's code.
Examples
Decode JWT payload in the browser with jose
import { decodeJwt, decodeProtectedHeader } from "jose";
const payload = decodeJwt(token);
console.log(payload.sub);
console.log(payload.exp);
const header = decodeProtectedHeader(token);
console.log(header.alg); // e.g. "RS256"
console.log(header.kid); decodeJwt and decodeProtectedHeader never verify the signature. For verification, use jose's jwtVerify().
Decode JWT with header in Node.js (jsonwebtoken)
const jwt = require("jsonwebtoken");
const decoded = jwt.decode(token, { complete: true });
console.log(decoded.header.alg); // e.g. "RS256"
console.log(decoded.payload.sub);
console.log(decoded.payload.exp); The complete: true option is required to access the header. Without it, jwt.decode returns only the payload.
- 1.
npm, "jose 6.2.3," npmjs.com, accessed June 2026. https://www.npmjs.com/package/jose
- 2.
auth0, "node-jsonwebtoken," github.com, accessed June 2026. https://github.com/auth0/node-jsonwebtoken
- 3.
panva, "jose," github.com, accessed June 2026. https://github.com/panva/jose
- 4.
auth0, "jsonwebtoken," npmjs.com, accessed June 2026. https://www.npmjs.com/package/jsonwebtoken
- 5.
MDN, "Date.now() - JavaScript," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now
- 6.
M. Jones et al., "JSON Web Token (JWT)," RFC 7519, IETF, May 2015. https://datatracker.ietf.org/doc/html/rfc7519
decodeJwt reads the payload without checking the signature: no key required. jwtVerify fetches or accepts a key, verifies the RS256 or HS256 signature, and also validates exp, nbf, and aud. Use decodeJwt for debugging; use jwtVerify for production middleware.
Use jose. It is ESM-native, supports all modern algorithms, runs in browsers and edge runtimes, and receives active maintenance. jsonwebtoken is CommonJS-only and primarily maintained for backward compatibility. For any new Node.js project using ESM, jose is the standard recommendation.
Multiply the exp value by 1000: const isExpired = Date.now() > payload.exp * 1000. Date.now() returns milliseconds; exp is seconds. This off-by-1000 mistake is the most common JavaScript JWT expiry bug.
No. jwt.decode returns null for a malformed or non-JWT string. Always null-check the return value before accessing decoded.payload. jwt.verify does throw on invalid tokens: use it when you need error handling for malformed inputs.
Yes. CapyToolkit's JWT decoder reads the Base64Url-encoded JSON payload locally in your browser with no dependency. A manual atob() expression can do the same for a single claim, but the decoder is safer for inspecting the full payload.
Decode a JWT in Go
If a Go HTTP middleware rejects a JWT, ParseUnverified can show which claim the server later failed to trust. Go's standard JWT library is github.com/golang-jwt/jwt/v5, the current major version with Go 1.21+ support.1 It provides ParseUnverified for payload inspection without a key, ParseWithClaims for full signature verification, and a typed claims interface for structured access. Add it with go get github.com/golang-jwt/jwt/v5 and import it as jwt in your source file. Paste any token into the decoder above for instant offline inspection without setting up a Go module first.
Pasting tokens into server-backed tools exposes them to third-party servers. Browser-based inspection eliminates this risk entirely with no network calls. For application code, Go's golang-jwt/jwt library uses an idiomatic key function pattern: you provide a func(*jwt.Token) (interface{}, error) that selects the correct verification key based on the token's header claims. This pattern supports multi-provider key selection and dynamic JWKS resolution without coupling your verification logic to a specific issuer.
What to look for
- Recommended package golang-jwt/jwt v5
- Known algorithm-confusion CVE CVE-2020-26160
Use jwt.NewParser().ParseUnverified() to decode without verifying, and a typed claims struct to read provider-specific fields.
Opens the JWT Decoder & Claims Inspector with this section's reference values shown at the top of the tool.
Open in the tool →Library and setup
Install the library with go get github.com/golang-jwt/jwt/v5 from your module root, using the import path github.com/golang-jwt/jwt/v5 with the package name jwt, and note that this v5 major version replaced the legacy github.com/dgrijalva/jwt-go and github.com/form3tech-oss/jwt-go packages that are both unmaintained and contain known security vulnerabilities including CVE-2020-26160.2 jwt.MapClaims provides a map[string]interface{} for dynamic claim access when you need to read arbitrary keys without declaring struct fields.3 Embedding jwt.RegisteredClaims in a struct gives you typed fields for standard claims like Sub, ExpiresAt, and Issuer with compile-time safety that prevents typos in claim-name strings.4
Choosing MapClaims or a claims struct
Use jwt.MapClaims when you are exploring an unfamiliar token and need every key without declaring Go fields first, then migrate to a typed claims struct once the expected claim names are stable. Starting with MapClaims in tests and moving to a struct in production middleware keeps debugging fast without weakening validation, and a typed claims struct with json tags also serves as documentation for which claims your handler expects.
Decoding without verification
Inspecting a JWT without verifying its signature uses jwt.NewParser().ParseUnverified(token, jwt.MapClaims{}), which returns a *jwt.Token with the Claims field populated from the payload and a Valid field set to false since no cryptographic verification occurred.1 Read claims using the MapClaims type where claims["sub"] returns the subject as interface{} that you assert to string with the ok idiom, and this type-assertion pattern is necessary for every claim read from MapClaims because the JSON decoder stores all values as interface{}.3 A simpler debugging approach embeds jwt.RegisteredClaims in a struct and passes it to ParseUnverified, giving you typed fields like claims.Subject and claims.ExpiresAt without the assertion noise.
Reading specific claims
For structured claim access, define a struct embedding jwt.RegisteredClaims and add custom fields, then pass a pointer to this struct as the claims argument to ParseUnverified or ParseWithClaims so the library populates all fields via JSON unmarshalling. Building on this, jwt.RegisteredClaims provides ExpiresAt (a *jwt.NumericDate), IssuedAt, Issuer, Subject, Audience, and ID as typed Go fields, which means you get compile-time checking for the most common claim names while retaining the flexibility to add provider-specific fields alongside them.5
For provider-specific claims, like cognito:groups or realm_access, add them as struct fields with json tags matching the claim name exactly as it appears in the token payload. Field names containing colons require the json:"cognito:groups" tag form; arrays use []string and objects use map[string]interface{}, so printing the raw parsed claims first helps you choose the correct Go type for each nested structure.
Combining golang-jwt with lestrrat-go/jwx for JWKS resolution
For JWKS-based RS256 verification in Go, add lestrrat-go/jwx alongside golang-jwt/jwt, installing with go get github.com/lestrrat-go/jwx/v2 and importing the jwk and jws subpackages for key-set management. Create a cached key set with jwk.NewCachedSet(ctx, jwksURL) then call jws.Verify([]byte(token), jws.WithKeySet(ks)) to verify the signature without managing key rotation manually, because the cached set refreshes automatically when a token arrives with an unknown kid that is not present in the current cache.6
Complete claim validation after signature verification
For complete claim validation after signature verification, parse the verified payload into your claims struct using json.Unmarshal or pass the token to jwt.ParseWithClaims with a key function that resolves from the same jwk cache. This two-library pattern is the standard approach in Go microservices where dynamic JWKS resolution and strict claim validation are both required at the gateway layer, handling all key rotation logic internally so your application code only needs to focus on validating the expected iss, aud, and exp values.
Keeping ParseUnverified out of production routes
Keep ParseUnverified out of production route guards because it reads the payload without verifying the signature or enforcing exp, which means any caller can construct a token with arbitrary claims that your server would accept. Use it only to read the header and kid during debugging, then send the original token through the verified ParseWithClaims path before your handler makes an access decision. A grep for ParseUnverified across your codebase during code review is a quick way to catch accidental usage in route handlers.
Adding this check to your continuous integration pipeline catches accidental introduction of insecure parsing before it reaches production. A single ParseUnverified call in a hot path can become an open door if the surrounding code starts trusting its output. Treat the grep as a guardrail that complements, rather than replaces, the verified parsing you use at request time, and reach for Go JWT parsing without module setup instead when you just need to confirm a claim before writing any code, since it gives you the same view as ParseUnverified minus the module setup.
When to use this
Use golang-jwt/jwt in Go HTTP servers and Lambda functions that receive JWTs from any standard provider. ParseUnverified suits middleware debugging; ParseWithClaims with a key function suits production route guards.
Examples
Inspect JWT payload without verification in Go
import "github.com/golang-jwt/jwt/v5"
token := "eyJ..."
p, _, err := jwt.NewParser().ParseUnverified(
token,
jwt.MapClaims{},
)
if claims, ok := p.Claims.(jwt.MapClaims); ok {
fmt.Println(claims["sub"])
fmt.Println(claims["exp"])
} ParseUnverified never returns an error for a well-formed JWT, even if the signature is wrong or the token is expired.
Typed claims struct for provider-specific fields
type MyClaims struct {
jwt.RegisteredClaims
// Keycloak roles
RealmAccess map[string][]string `json:"realm_access"`
// Cognito groups
Groups []string `json:"cognito:groups"`
}
var claims MyClaims
jwt.NewParser().ParseUnverified(token, &claims)
fmt.Println(claims.Subject)
fmt.Println(claims.Groups) JSON tags must exactly match the claim name, including colons in provider-specific claim names like cognito:groups.
- 1.
golang-jwt, "jwt/v5," pkg.go.dev, accessed June 2026. https://pkg.go.dev/github.com/golang-jwt/jwt/[email protected]
- 2.
NVD, "CVE-2020-26160," nvd.nist.gov, September 2020. https://nvd.nist.gov/vuln/detail/CVE-2020-26160
- 3.
golang-jwt, "map_claims.go," github.com, accessed June 2026. https://github.com/golang-jwt/jwt/blob/main/map_claims.go
- 4.
golang-jwt, "registered_claims.go," github.com, accessed June 2026. https://github.com/golang-jwt/jwt/blob/main/registered_claims.go
- 5.
M. Jones et al., "JSON Web Token (JWT)," RFC 7519, IETF, May 2015. https://datatracker.ietf.org/doc/html/rfc7519
- 6.
lestrrat-go, "jwx v2 — jwk.Cache," pkg.go.dev, accessed June 2026. https://pkg.go.dev/github.com/lestrrat-go/jwx/v2/jwk#Cache
ParseUnverified reads the claims without checking the signature: it always sets Valid to false. Parse requires a key function and verifies the signature, setting Valid to true only if verification succeeds. Use ParseUnverified for inspection; use Parse for production authentication.
The key function has the signature func(*jwt.Token) (interface{}, error). The *jwt.Token argument gives you access to the header claims (including alg and kid) so you can select the correct verification key dynamically. Return the key as interface{}: for RS256, return an *rsa.PublicKey; for HS256, return a []byte.
Use a type assertion with the ok idiom: sub, ok := claims["sub"].(string). If ok is false, the claim is absent or has an unexpected type. For numeric claims like exp, the JSON decoder produces float64: convert with int64(claims["exp"].(float64)) to get a Unix timestamp.
Yes. Pass *rsa.PublicKey for RS256 and RS384/RS512; pass *ecdsa.PublicKey for ES256 and ES384/ES512. The library selects the verifier based on the alg claim in the token header. For JWKS-based key resolution, combine golang-jwt/jwt with the lestrrat-go/jwx library.
No. dgrijalva/jwt-go is archived and has a known security vulnerability (CVE-2020-26160). Use github.com/golang-jwt/jwt/v5, the community fork that fixed this issue and continues active maintenance. Migrate by updating the import path and adjusting for the v5 API changes.
Yes. CapyToolkit's JWT decoder reads the payload locally in your browser, which helps you confirm claim names before choosing MapClaims or a typed claims struct. Use ParseWithClaims in your Go backend when trust matters.
Decode a JWT in Java with JJWT
If a Java route returns a token parsing error, JJWT is often the library that pinpoints the failure. JJWT is the most widely used JWT library for Java and Android, published under io.jsonwebtoken on Maven Central. It provides a fluent builder API for both token creation and parsing, covering decode-without-verification and full cryptographic verification paths. Adding it to a Gradle project requires two lines: jjwt-api and jjwt-impl.1 Paste any token into the decoder above for instant offline inspection without writing Java application code, or follow the patterns below for application integration.
The safer debugging approach uses browser-based inspection rather than pasting tokens into a server-backed tool. Because every character of a JWT is transmitted in server-backed decoders, production tokens become a liability. Offline inspection in your browser reveals all claims: sub, exp, realm_access, or any custom field: with no token transmission risk. For Java middleware that must read claims programmatically, JJWT's parser handles both the unsigned inspection case and the fully verified case with equal conciseness.
What this page covers
- Jwts.parser().parseClaimsJwt() reads claims without verifying, using JJWT's builder API
- claims.get(name, Type.class) reads a custom claim with an explicit type instead of a raw Map cast
Opens the JWT Decoder & Claims Inspector with this section's checklist shown at the top of the tool.
Open in the tool →Library and setup
Add JJWT to your Maven pom.xml with two dependencies: io.jsonwebtoken:jjwt-api (the interface layer) and io.jsonwebtoken:jjwt-impl (the runtime implementation)2; for RS256 or ES256 verification, also add io.jsonwebtoken:jjwt-jackson for JSON parsing if your project does not already include Jackson. In Gradle, add the same two artifacts to the dependencies block, where the api artifact provides the public interfaces while impl provides the runtime implementations that handle the actual cryptographic operations. Importing io.jsonwebtoken.Jwts is the only import needed for most parsing operations, and the fluent builder pattern that JJWT uses makes it straightforward to chain configuration calls for signing keys, required claims, and expiration checks.
Keeping unsigned parsing out of production
The unsigned parser is useful when you inspect test fixtures or reproduce a customer report because it reads the payload without requiring a signing key. It should not live in route guards, because it accepts tokens without issuer trust, which means an unsigned parser in a production route guard effectively disables authentication entirely and allows any client to construct a JWT with arbitrary claims that the server will accept as long as the Base64Url encoding is structurally valid.
Decoding without verification
Parsing a JWT payload without signature verification requires removing the signature section first, because the unsigned parser rejects any token that still carries a signature. Strip everything after the second dot: String unsignedToken = token.substring(0, token.lastIndexOf('.') + 1). Then call Jwts.parser().build().parseClaimsJwt(unsignedToken) to get the Claims object3, which works for any token regardless of the algorithm specified in the header.
Reading claims from the result uses typed accessor methods: getSubject() for sub, getExpiration() for exp, or get('custom', String.class) for provider-specific claims like realm_access or cognito:groups. The Claims interface extends java.util.Map, so you can also iterate over all entries when you need to inspect every claim in a token from an unfamiliar provider.
Reading specific claims
The Claims object returned by parseClaimsJwt exposes typed accessors for all registered JWT claims.3 getSubject() returns the sub claim as a String; getExpiration() returns a java.util.Date. Building on this, getIssuedAt() returns the iat timestamp, getIssuer() returns iss, and getAudience() returns the aud claim as a Set<String>, which means checking audience validity is a simple contains() call rather than a string comparison.
For custom or provider-specific claims, like realm_access in Keycloak tokens, use get('realm_access', Map.class) with the expected Java type to avoid ClassCastException at runtime. Always print the full Claims object before selecting individual paths from an unfamiliar provider, because nested objects require exact type declarations that are difficult to guess without seeing the actual JSON structure first.
Full signature verification with JJWT using RS256 keys
JJWT handles RS256 verification by accepting a PublicKey or JWK object as the signing key in the parser. Fetch the public key from your provider's JWKS endpoint, deserialize it using JJWT's JWKSet parser, then call Jwts.parser().verifyWith(publicKey).requireIssuer(expectedIss).requireAudience(expectedAud).build().parseSignedClaims(token) to perform full verified parsing that checks the cryptographic signature, expiry, issuer, and audience in a single call. The method throws JwtException on any failure, giving your error handler enough context to return the correct HTTP status code for each specific validation failure.
Dynamic key selection with Locator
For dynamic JWKS key selection by kid, use Jwts.parser().keyLocator(locator) where locator implements Locator<Key>. Override locate(JwsHeader header) to read header.getKeyId() and return the matching public key from your JWKS cache. JJWT calls this method once per verification, giving your locator full access to the token header for key selection before the signature check runs. Caching the JWKS response inside the locator and only refreshing when an unknown kid appears keeps verification fast while still handling key rotation without manual intervention or application restarts.
Placing the cache inside the locator keeps key selection fast even during an active rotation event when two keys are valid at once. The verifier asks the locator for the correct key on every call, so new tokens verify against the fresh key while old tokens still match the previous one. This design removes manual rotation handling from your route code entirely.
Keeping unsigned parsing out of route guards
Keep unsigned parsing out of route guards because it accepts tokens without issuer trust and effectively disables authentication for any client that can construct a valid Base64Url-encoded payload. The signature is the only element preventing an attacker from forging a token or editing the claims of an existing one, which is exactly what unsigned parsing discards.4 In production, pass the original signed token to parseSignedClaims and let JJWT reject the request whenever the signature, expiry, issuer, or audience fails validation. A common code-review check is to search the codebase for all calls to parseClaimsJwt and verify that none of them sit inside a filter, interceptor, or any other component that runs before the request reaches your business logic.5 For a quick look at a token's shape before you write any Java, reading JWT claims without a JVM renders the same header and claims that parseClaimsJwt would return.
When to use this
Use JJWT when your Java or Android backend reads JWTs from any standard provider. The unsigned parse path suits test fixtures and debugging; the signed parse path suits production API middleware.
Examples
Decode JWT payload without verification in Java
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
String token = "eyJ...";
// Strip signature for unsigned parsing
String unsigned = token.substring(0, token.lastIndexOf('.') + 1);
Claims claims = Jwts.parser()
.build()
.parseClaimsJwt(unsigned)
.getBody();
System.out.println(claims.getSubject());
System.out.println(claims.getExpiration()); Remove the signature section (third JWT part) before calling parseClaimsJwt: the unsigned parser rejects signed tokens.
Read a custom claim from a parsed JWT
Claims claims = Jwts.parser()
.build()
.parseClaimsJwt(unsigned)
.getBody();
// Standard claim
String sub = claims.getSubject();
// Custom claim with type
String role = claims.get("role", String.class);
Map<?, ?> realmAccess = claims.get("realm_access", Map.class); Always specify the expected Java type in get() to avoid raw Map casting.
- 1.
jwtk/jjwt, "Java JWT: JSON Web Token for Java and Android," github.com, accessed June 2026. https://github.com/jwtk/jjwt
- 2.
Maven Central, "io.jsonwebtoken:jjwt-api," central.sonatype.com, accessed June 2026. https://central.sonatype.com/artifact/io.jsonwebtoken/jjwt-api
- 3.
JJWT, "Claims," javadoc.io, accessed June 2026. https://javadoc.io/static/io.jsonwebtoken/jjwt-api/0.13.0/io/jsonwebtoken/Claims.html
- 4.
OWASP Cheat Sheet Series, "JSON Web Token," cheatsheetseries.owasp.org, accessed September 2026. https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_Cheat_Sheet.html
- 5.
JJWT, "Jwts," javadoc.io, accessed June 2026. https://javadoc.io/static/io.jsonwebtoken/jjwt-api/0.13.0/io/jsonwebtoken/Jwts.html
parseClaimsJwt is designed for unsigned (unsecured) JWTs: it fails if a signature is present. To inspect a signed token's claims without verifying, remove the third section (after the second dot). parseClaimsJws is the method for signed tokens and requires a key.
Add implementation 'io.jsonwebtoken:jjwt-api:0.12.x' and runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.x'. The api artifact compiles against; the impl artifact provides the runtime classes. For RS256 support, also add runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.x'.
Yes. Call claims.entrySet() to iterate over all key-value pairs as Map.Entry<String, Object>. This is useful when you need to log all claims or forward them to another system without knowing every claim name in advance.
parseClaimsJwt handles unsigned (unsecured) JWTs: it fails if a signature is present. parseClaimsJws handles signed JWTs: it requires a signing key and verifies the signature. Use parseClaimsJwt only for tokens you have deliberately stripped of their signature for inspection purposes.
Yes, JJWT is compatible with Android API 26 and above. On Android, replace jjwt-impl with the android-compatible variant or use jjwt-jackson. The core API and claims interfaces work identically across Java and Android targets.
Yes. CapyToolkit's JWT decoder reads the payload locally in your browser, which helps you confirm claim names before writing Java. Use JJWT in the backend when you need verified parsing.
Decode a JWT in C# / .NET
When ASP.NET Core rejects a bearer token, the first question is usually which claim caused the rejection. System.IdentityModel.Tokens.Jwt is distributed as a NuGet package: install it with dotnet add package System.IdentityModel.Tokens.Jwt.1 The JwtSecurityTokenHandler and JwtSecurityToken classes provide both a read-without-verify path and a full TokenValidationParameters-based validation path. Adding Microsoft.IdentityModel.JsonWebTokens via NuGet gives you the newer JsonWebToken API, which is preferred for .NET 6 and later. Paste any token into the decoder above for instant offline inspection without writing a line of C# or committing a token to shared server-backed logs.
Pasting production tokens into server-backed decoder tools exposes them to third parties. C# developers can decode any JWT locally in a console project using new JwtSecurityToken(token) without a network call. Because this approach reads the raw encoded payload, it works offline with no signing key. For production middleware in ASP.NET Core, JwtBearerAuthentication's AddAuthentication pipeline handles full validation automatically: but understanding the underlying claims object helps when debugging middleware configuration issues.
What this page covers
- JwtSecurityTokenHandler.ReadJwtToken() reads claims without verifying, from System.IdentityModel.Tokens.Jwt
- jwt.Claims.FirstOrDefault(c => c.Type == "...") matches provider-specific claims by string, since they have no ClaimTypes constant
Opens the JWT Decoder & Claims Inspector with this section's checklist shown at the top of the tool.
Open in the tool →Library and setup
System.IdentityModel.Tokens.Jwt is distributed as a NuGet package: add it with dotnet add package System.IdentityModel.Tokens.Jwt. The JwtSecurityTokenHandler class is the primary entry point for both decoding and validation, providing a unified API surface that handles token parsing, signature verification, and claim extraction. For .NET 6 and later, Microsoft.IdentityModel.JsonWebTokens provides the newer JsonWebToken class with improved performance and async support, and both packages coexist so you can migrate incrementally without breaking existing code.
Choosing ReadJwtToken for debugging
Use ReadJwtToken when you need to inspect fixtures, logs, or a token captured during a failing request, because it gives you the same Claims collection your middleware will eventually read without requiring a signing key. Keep it out of protected endpoints, because it accepts any well-formed JWT regardless of issuer, audience, expiry, or signature, and a ReadJwtToken call inside a controller action that grants access based on the decoded sub or role claims effectively bypasses the entire authentication pipeline.
Decoding without verification
Reading a JWT payload without signature verification uses new JwtSecurityToken(token), which parses the header and payload directly from the Base64Url-encoded string without requiring a signing key, making it suitable for logging, debugging, or test fixture inspection where you trust the token source.2 The resulting JwtSecurityToken object exposes Claims as an IEnumerable<Claim>, the Header dictionary, and the Payload dictionary, giving you multiple ways to access any claim by type name using token.Claims.FirstOrDefault(c => c.Type == "sub")?.Value or by iterating over the full payload dictionary when you need to inspect every field. CapyToolkit's browser-based decoder performs the equivalent operation entirely offline, showing the same header and payload structure without sending your token to any server.
Reading specific claims
The Claims collection on a JwtSecurityToken maps each JWT claim to a .NET Claim object with Type and Value properties. Standard claims like sub, iss, and aud map to ClaimTypes constants in System.Security.Claims, which means your authorization code can reference ClaimTypes.Subject rather than a magic string. Building on this, JwtSecurityToken also exposes strongly typed properties: JwtSecurityToken.Subject (sub), ValidTo (exp as DateTime), IssuedAt (iat as DateTime), and Issuer (iss), so you can read registered claims with compile-time safety rather than string-based lookups.3 For provider-specific claims like realm_access or cognito:groups, access them via the Payload dictionary with payload["realm_access"] or by filtering Claims by type name, inspecting the dictionary first whenever a provider nests values so you choose the correct JSON shape.
Configuring TokenValidationParameters for RS256 in ASP.NET Core
Configuring RS256 token validation in ASP.NET Core uses the TokenValidationParameters object passed to AddJwtBearer, where you set IssuerSigningKeyResolver to a delegate that fetches JWKS from your provider and returns the matching SecurityKey by kid.4 Set ValidIssuer and ValidAudience to your expected values, and set RequireExpirationTime to true to enforce exp validation, ensuring that the middleware calls your resolver on each request and caches the key between calls for performance.
OIDC providers with AddMicrosoftIdentityWebApi
For Azure AD and other OIDC providers, Microsoft.IdentityModel.Protocols.OpenIdConnect handles JWKS discovery automatically. Call AddMicrosoftIdentityWebApi(configuration) instead of AddJwtBearer to configure OIDC-compliant validation in one line. The middleware fetches the JWKS from the provider's discovery document, validates tid, iss, and aud automatically, and refreshes keys on rotation without any manual configuration in your startup code. This single-call configuration replaces what would otherwise require dozens of lines of manual TokenValidationParameters setup, including JWKS endpoint URLs, issuer strings, audience values, and key rotation callbacks that are error-prone to maintain by hand.
Separating debug reads from production validation
Keep the decode-only JwtSecurityTokenHandler path separate from middleware validation, because reading a token without checking the signature is useful for local debugging but should never become the authorization path that protects production data. The signature is the only element that stops an attacker from forging a token or editing the claims of an existing one.5 In ASP.NET Core, let the bearer middleware validate the token, then read HttpContext.User in your endpoint so every protected route follows the same key resolution and claim validation rules. Scattering ReadJwtToken calls across multiple controllers creates an inconsistent security model where some endpoints verify signatures and others trust raw payloads, making it nearly impossible to audit which routes are actually protected.
Centralising token reads through the authenticated middleware means every endpoint honours the same validation rules without per-controller exceptions. A new developer adding a route inherits the secure path by default rather than accidentally introducing a debug read in production. Periodic code review of where claims are read keeps the security model consistent as the codebase grows, and C# JWT claim reading with JwtSecurityToken shows the same header and claims that ReadJwtToken returns before you write any middleware.
When to use this
Use JwtSecurityToken for C# and ASP.NET Core projects that need to inspect JWT claims during middleware development, test fixture setup, or debugging authentication issues without running the full validation pipeline. The same header and claims ReadJwtToken would return are often faster to inspect without spinning up a console project.
Examples
Read JWT claims without verification in C#
using System.IdentityModel.Tokens.Jwt;
var token = "eyJ...";
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(token);
var sub = jwt.Subject;
var exp = jwt.ValidTo;
var issuer = jwt.Issuer;
foreach (var claim in jwt.Claims)
{
Console.WriteLine($"{claim.Type}: {claim.Value}");
} handler.ReadJwtToken does not verify the signature. Use handler.ValidateToken for production validation.
Access a custom claim by type name
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(token);
// Standard claim
var email = jwt.Claims
.FirstOrDefault(c => c.Type == "email")?.Value;
// Provider-specific claim
var role = jwt.Claims
.FirstOrDefault(c => c.Type == "role")?.Value; Provider-specific claim names (like "role" or "cognito:groups") are not mapped to ClaimTypes constants: match them by string.
- 1.
AzureAD, "azure-activedirectory-identitymodel-extensions-for-dotnet," github.com, accessed June 2026. https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet
- 2.
Microsoft, "JwtSecurityTokenHandler.ReadJwtToken Method," learn.microsoft.com, accessed June 2026. https://learn.microsoft.com/en-us/dotnet/api/system.identitymodel.tokens.jwt.jwtsecuritytokenhandler.readjwttoken
- 3.
Microsoft, "JwtSecurityToken Class," learn.microsoft.com, accessed June 2026. https://learn.microsoft.com/en-us/dotnet/api/system.identitymodel.tokens.jwt.jwtsecuritytoken
- 4.
Auth0, "ASP.NET Web API (OWIN): Authorization," auth0.com, accessed June 2026. https://auth0.com/docs/quickstart/backend/webapi-owin
- 5.
OWASP Cheat Sheet Series, "JSON Web Token," cheatsheetseries.owasp.org, accessed September 2026. https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_Cheat_Sheet.html
ReadJwtToken parses the JWT structure and exposes claims without any signature or expiry validation. ValidateToken performs full validation: signature, expiry, issuer, and audience. Use ReadJwtToken for inspection and debugging; use ValidateToken in production middleware that enforces security.
Not directly. AddAuthentication().AddJwtBearer() configures the full validation pipeline automatically. JwtSecurityTokenHandler is useful when you need to inspect tokens manually outside the middleware pipeline: in tests, background jobs, or diagnostic logging.
Compare jwt.ValidTo to DateTime.UtcNow: bool isExpired = DateTime.UtcNow > jwt.ValidTo. The ValidTo property maps the exp Unix timestamp to a UTC DateTime automatically. Note that this comparison trusts the exp claim without verifying the signature.
Yes. JwtSecurityToken parses the structure of any standard JWT regardless of the signing algorithm. Reading claims via ReadJwtToken works for RS256, HS256, ES256, and all other algorithms: the algorithm only matters when you call ValidateToken and need to provide a matching key.
Yes. It targets .NET Standard 2.0 and above, covering .NET Framework 4.6.2+, .NET Core 2.0+, and .NET 5+. The API surface is identical across all targets, though performance is better on .NET 6 and later with the newer Microsoft.IdentityModel.JsonWebTokens package.
Yes. CapyToolkit's JWT decoder reads the token locally in your browser, so you can confirm claim names before writing C#. Use ReadJwtToken or ValidateToken in your backend depending on whether you need inspection or trust.