JWT Decoder Reference

Every JWT term and claim covered by the JWT Decoder & Claims Inspector, collected on one page. Pick a term from the list to see its definition and how it shows up in a decoded token.

ZERO UPLOAD · ALL LOCAL

What Is a JSON Web Token (JWT)?

Because JSON Web Tokens encode identity and permissions in a compact, URL-safe format, they have become the dominant mechanism for stateless authentication in modern web APIs. Understanding the three-part structure reveals why JWTs are readable but cryptographically bound, and why decoding them without verifying the signature is not a security operation.

What is a JSON Web Token?

A JSON Web Token is a digitally signed, Base64Url-encoded string that carries claims about a subject.1 Its three dot-separated parts: header, payload, and signature: encode the signing algorithm, the claim set, and the cryptographic proof that binds them. Because the payload is only encoded and not encrypted, any party with the raw token can read the claims; the signature ensures that no party can alter them without detection.

Token structure explained

A JWT consists of three Base64Url-encoded sections separated by dots.1 The header section encodes a JSON object specifying the signing algorithm (alg) and token type (typ: JWT). The payload section encodes the claims: user identity, expiry timestamps, roles, and any custom fields the issuer added. The signature section is a cryptographic hash of the encoded header and payload, computed using the key specified in the header's alg field. Consequently, altering a single character in the header or payload invalidates the signature, making token tampering detectable without the signing key.

Reading the three sections in order

Start with the header to learn the algorithm and key ID, then inspect the payload to understand the claims, and finally treat the signature as proof that the first two sections were not changed. This order matters because a decoded payload is useful debugging data, but it is not trusted data until your server verifies the signature with the issuer's key. A JWT decoder shows the shape quickly; your validation code decides whether the token can be accepted.

Claims and encoding

Base64Url encoding differs from standard Base64 in two characters: + becomes - and / becomes _, and padding = characters are omitted, which makes the token safe to include in HTTP headers, query strings, and cookies without requiring additional percent-encoding that would inflate the token size. The payload JSON is not compressed or encrypted; it is only encoded, meaning any party that intercepts the raw token string can read every claim value without needing a decryption key. Consequently, never include sensitive secrets, passwords, or private keys in a JWT payload, and treat the payload as a public document that is cryptographically signed to prevent tampering but not sealed to prevent reading.

Stateless authentication

The JWT's self-contained structure eliminates the need for server-side session storage. A server that issues a JWT embeds all the authorisation information directly in the token. Downstream services verify the signature and read claims directly, without querying a shared session database. Building on this, stateless authentication scales horizontally: any instance of your API can verify any JWT without coordination. Yet the trade-off is revocation complexity: once issued, a JWT remains valid until its exp, unless you maintain a server-side blocklist of revoked tokens.

For debugging, that trade-off is helpful because you can inspect claims without knowing the issuer's private key. For production, it is a reminder that decoding alone is not authentication. A valid-looking token may still be expired, intended for another audience, issued by the wrong provider, or revoked after issuance. Your backend should combine signature verification with exp, iss, aud, and any provider-specific checks before granting access.

Across authentication systems: where JWTs appear

Across modern authentication systems, JWTs carry identity and permission data in several distinct contexts. OAuth 2.0 access tokens are frequently JWTs, allowing your API to verify permissions without a database call on every request. OpenID Connect mandates JWTs for ID tokens: the format is a specification requirement, not a convention.2 HTTP Authorization headers carry JWTs in the Bearer scheme: Authorization: Bearer eyJ....3 Understanding where JWTs appear helps you decode the right token type when debugging authentication failures, because a decoding error often means the wrong token format was pasted into the inspector.

Cookie and WebSocket transport

Cookies store JWTs for web applications that use the HttpOnly and SameSite attributes to prevent script access and CSRF misuse. WebSocket upgrade requests can include a JWT in a query parameter during the initial handshake before the protocol switches; avoid query-string token placement in production because web server logs capture URL parameters and may retain them longer than the token's intended lifetime.

JWTs versus opaque tokens

Opaque tokens are random strings that the server maps to session records in a database, and only the issuing server can resolve the token back to the associated session data. JWTs are self-contained structures that allow any recipient server to read all claims directly from the token payload without performing a database lookup. The fundamental trade-off is revocation: revoking an opaque token means deleting the database record instantly and the token becomes useless immediately, whereas revoking a JWT requires maintaining a server-side blocklist of revoked token identifiers or simply waiting for the exp claim to pass.

Choosing the right approach for your use case

Choose opaque tokens for applications where immediate revocation is critical: financial services, healthcare portals, or any session that must terminate on logout without any revocation window. Choose JWTs for high-throughput APIs where stateless verification eliminates database round-trips and scales horizontally to any number of server instances. Many production systems use both approaches together: opaque refresh tokens stored server-side for revocation control, plus short-lived JWT access tokens for API calls where stateless verification provides the performance and scalability benefit.

Refresh tokens give you instant revocation for the parts of the system that need it, while access tokens keep the API path stateless and fast. The access token lifetime can stay short because the refresh token reissues it without bothering the user. This combination is why most modern stacks separate the two token types instead of using one token for everything.

Try in the tool

What this page covers

  • Header encodes the signing algorithm (alg) and token type (typ)
  • Payload encodes the claims: identity, expiry, roles, and any custom fields
  • Signature a cryptographic hash of the header and payload; detects tampering, does not hide the content

Open the JWT Decoder & Claims Inspector tool to try this yourself.

Open the tool →
Sources
  1. 1.

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

  2. 2.

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

  3. 3.

    M. Jones and D. Hardt, "The OAuth 2.0 Authorization Framework: Bearer Token Usage," RFC 6750, IETF, October 2012. https://datatracker.ietf.org/doc/html/rfc6750

FAQ

What Is a JWT Claim?

In a JSON Web Token, every key-value pair in the payload has a specific name: a claim. Claims encode identity (sub), permissions (roles), expiry (exp), and any custom data the issuer adds. Understanding which claims are standardised and which are provider-specific is the first step in reading any decoded token correctly.

What is a JWT claim?

A JWT claim is a key-value pair in the token payload that asserts a fact about the subject or the token itself. RFC 7519 defines seven registered claims with standard names and semantics.1 Public claims can be registered with IANA to avoid collisions. Private claims are any non-registered key-value pairs the issuer and consumer agree upon: they have no collision protection unless namespaced with a URI or reverse-domain prefix.

Registered claims

The seven registered claims from RFC 7519 carry the same meaning in every compliant JWT: iss (issuer), sub (subject), aud (audience), exp (expiration), nbf (not before), iat (issued at), and jti (JWT ID).1 Libraries automatically validate exp and nbf during token verification; iss and aud require explicit configuration. Because these claim names are three characters long, JWT payloads remain compact even when all seven are present. Not all tokens include every registered claim: only exp is consistently present across most providers, since many issuers omit jti and nbf.

Reading registered claims first

When you inspect a decoded token, read registered claims before private claims. iss tells you who issued the token, sub tells you which subject it represents, aud tells you which API should accept it, and exp tells you when it stops being valid. That sequence turns a long JSON object into a checklist: issuer, subject, audience, and time. Private claims then explain what the issuer wanted your application to do with that subject.

Private claims

Private claims are any claim outside the RFC 7519 registered set. AWS Cognito adds cognito:groups and token_use. Keycloak adds realm_access and resource_access. Supabase adds role and aal. These provider-specific claims enable application logic without additional database lookups. Yet without namespacing, a private claim named role might collide if a future RFC standardises the same name with different semantics. Consequently, the RFC recommends using URI-namespaced claim names: for example https://yourapp.com/role: for any private claim you introduce in your own tokens.

Before trusting a private claim, confirm the issuer and audience first. A role claim from the wrong tenant can grant access to the wrong data, and a custom permission claim from a test environment can slip into production if your validation configuration is too broad. Treat private claims as useful only after the registered claims prove the token belongs to your system.

Claim validation

Reading a claim is fundamentally different from validating it, because decoding only shows the raw value while validation confirms the signature is intact, the token is not expired, and the iss and aud match expected values.2 Building on this distinction, a claim read from an unverified token is untrusted data that tells you what the token asserts but not whether the assertion is genuine, which is why production systems must verify the cryptographic signature before acting on any claim value. Only after successful signature verification does a claim carry the authorisation weight the issuer intended, since the signature is the only mechanism that proves the issuer actually created the token.

In practice: reading claims across languages

In Python, jwt.decode() returns a plain dict where you should use payload.get('sub') to avoid KeyError when a claim is absent, while in JavaScript, jose's decodeJwt() returns a plain object where accessing an absent key returns undefined rather than throwing an exception. In Go, claims parsed into jwt.MapClaims require a type assertion with the ok idiom: sub, ok := claims['sub'].(string), because the JSON decoder stores all values as interface{} that must be explicitly converted. Each language presents the same underlying claim name and value, but the access pattern differs significantly, so always check the specific library documentation when porting JWT handling code between languages.

Provider-specific claim names across languages

Provider-specific claims follow the same access pattern as registered claims regardless of the programming language you use. Cognito's cognito:groups arrives as a JSON array in all three languages; your code receives it as a list in Python, an array in JavaScript, or a slice in Go, and the claim name including any colon separator in names like cognito:groups is the exact string key you pass to the accessor.

Using the exact claim key avoids a class of bugs where code reads a slightly different name and receives undefined instead of the real value. The colon in names like cognito:groups is part of the key, not a separator your accessor splits on. Print the decoded claim map once during integration so you can copy the precise string into your access code.

Adding custom claims to tokens you issue

Adding custom claims to tokens your application issues follows the same key-value structure as registered claims, and the specific API varies by library. In PyJWT, any extra key in the payload dict passed to jwt.encode() automatically encodes as a claim; in jose (JavaScript), pass the full claims object including custom fields to new SignJWT(claims) and call .sign() to produce the token; in JJWT (Java), call .claim(fieldName, value) on the Jwts builder for each custom field before compacting. Understanding these patterns makes it straightforward to enrich tokens with application-specific data that downstream services can read without an additional API call.

Namespace requirements for third-party tokens

Namespace custom claims when your token may be accepted by third-party services, because a bare key like role risks collision if a future RFC 7519 revision adds a registered claim with the same name but different semantics. Using https://yourapp.com/role as the claim key guarantees global uniqueness across all systems. Auth0 and Okta enforce namespacing for custom claims through their dashboard configuration and reject un-namespaced keys before issuing the token, which catches naming conflicts at configuration time rather than at verification time.3

Try in the tool

What this page covers

  • iss, sub, aud issuer, subject, and audience: the seven RFC 7519 registered claims include these three identity/routing fields
  • exp, nbf, iat the three NumericDate time claims that define a token's validity window
  • jti a unique token ID, used for replay-attack prevention via a short-lived seen-cache

Open the JWT Decoder & Claims Inspector tool to try this yourself.

Open the tool →
Sources
  1. 1.

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

  2. 2.

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

  3. 3.

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

FAQ

What Is a JWKS (JSON Web Key Set)?

JWT signature verification requires the issuer's public key, so every OpenID Connect provider publishes a JSON Web Key Set: a structured document that lists all current signing keys. Reading the JWKS endpoint gives your application everything it needs to verify tokens without a shared secret, enabling stateless, key-rotation-aware signature verification.

What is a JWKS?

A JWKS (JSON Web Key Set) is a JSON document containing an array of JSON Web Keys: each representing one public signing key. Each key entry includes a kid (key ID) that matches the kid claim in the JWT header, along with the cryptographic parameters needed to reconstruct the public key.1 Providers publish the JWKS at a well-known URL and rotate it when keys change, keeping your application's verification logic current without code changes.

JWKS structure

A JWKS document contains a single keys array where each entry represents one public key with algorithm-specific parameters that your JWT verifier needs to reconstruct the cryptographic key material. For RSA keys (RS256), the parameters are n (modulus, Base64Url-encoded) and e (exponent, usually AQAB for 65537); for EC keys (ES256), the parameters are crv (curve name), x, and y (point coordinates).1 Every key entry also includes kty (key type: RSA or EC), use (sig for signing keys), and kid (key ID), and consequently, matching the token header's kid to the correct JWKS entry selects the right public key for verification.

Matching kid to the token header

The kid value is the bridge between the JWT header and the JWKS document, so your verifier reads the unverified header first to extract the kid, finds the matching public key in the keys array, then performs the cryptographic check against the signature. If kid is missing, your application needs provider-specific key selection logic or must reject the token until the correct key can be determined, and relying on the absence of kid is fragile because a future key rotation will add kid values and break any logic that depends on its absence.

Key rotation

Providers rotate signing keys periodically: typically every 90 days: to limit exposure if a private key is ever compromised.2 During rotation, the old key and the new key coexist in the JWKS simultaneously. Any token signed by the old private key still matches the old public key in the JWKS by kid. Consequently, tokens issued before the rotation remain verifiable until they expire.

Once all old tokens have expired, the provider removes the old key from the JWKS. Never hard-code a public key: always resolve it dynamically from the JWKS endpoint to survive rotation transparently. Your cache should therefore hold multiple keys, not a single key, because a live provider may sign valid tokens with either the current or previous key during the transition window.

Discovery and caching

OpenID Connect providers publish their JWKS URL in a discovery document at <issuer>/.well-known/openid-configuration, and fetching this document at application startup gives you the jwks_uri field that points to the current JWKS endpoint.3 Cache the JWKS response aggressively using HTTP Cache-Control headers because the signing keys rarely change during normal operation, typically rotating only every 90 days or when a key compromise is suspected. Yet always implement a stale-key fallback mechanism: when verification fails due to an unknown kid, fetch the JWKS again before rejecting the token. Using the unknown-kid refresh pattern absorbs key rotation events transparently without requiring downtime or manual key updates in your application configuration, which is why most JWT libraries implement this pattern automatically when you use their built-in JWKS resolver.

Refreshing the JWKS cache on an unknown kid

When a token arrives with a kid not present in your cached JWKS, refresh the key set before rejecting the token. The provider added a new signing key since your last fetch, and the new token uses it. Fetch the JWKS again, check whether the new kid appears, and retry verification once. If the kid is still absent after a fresh fetch, the token is malformed or from the wrong issuer.

Preventing DoS from arbitrary kid values

Limit each cache refresh to one attempt per unknown kid to prevent a denial-of-service condition where an attacker sends tokens with arbitrary kid values. Use a per-kid fetch lock: if a refresh is already in progress for a given kid, queue the verification request and wait for the existing fetch to complete rather than starting a parallel one. Libraries like jose (JavaScript) and PyJWT (Python) implement this stale-while-revalidate pattern internally when you use their built-in JWKS resolvers.

JWKS caching with multiple identity providers

Applications that accept tokens from more than one provider maintain a separate JWKS cache per provider. Keying the cache by iss ensures that an Auth0 token resolves against Auth0's JWKS and an Okta token resolves against Okta's JWKS. Using a single shared cache risks verifying a token from one provider against another provider's keys, which always fails with a misleading signature error.

Rejecting unknown iss values before fetching JWKS

Read the iss claim from the decoded (unverified) header first, then select the JWKS endpoint for that issuer. Verify the signature only after confirming that iss maps to a known provider in your configuration. Reject unknown iss values immediately without any JWKS fetch; this limits unnecessary network calls from tokens your application will never accept and avoids disclosing which providers you support.

Early rejection also protects your infrastructure from a flood of crafted tokens that probe for accepted issuers across the internet. An attacker learns nothing about your provider list because the request is dropped before any key fetch occurs. Combining the iss check with a strict allowlist keeps verification cost low and your external attack surface small even under sustained probing.

Try in the tool

What to look for

  • ~90 days
  • n (modulus) + e (exponent)
  • crv (curve) + x, y (point)

Decode the token above to read its kid, then match it to the correct entry in the provider's JWKS.

Open the JWT Decoder & Claims Inspector tool to try this yourself.

Open the tool →
Sources
  1. 1.

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

  2. 2.

    Auth0, "JSON Web Key Sets," auth0.com, accessed June 2026. https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-key-sets

  3. 3.

    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

FAQ

What Is the JWT iss Claim?

A JWT can come from any provider, so the iss (issuer) claim identifies who created it. Validating iss prevents cross-issuer token acceptance: an Auth0 token from one tenant must not authenticate against a different provider's API. Without iss validation, any party that can forge a structurally valid JWT for any provider can authenticate against your system if you compare only the signature and ignore issuer configuration.

What is the iss claim?

The iss (issuer) claim is a string that identifies the entity that created and signed the JWT. Typically a URL pointing to the authentication server, iss lets the recipient confirm that the token came from an expected source before acting on its claims. JWT libraries that validate iss compare the claim value against an allowlist of expected issuers and reject any token whose iss does not match.1

iss format across providers

Each provider uses a predictable URL pattern for iss.1 Auth0 uses https://<tenant>.auth0.com/, Google uses https://accounts.google.com, Okta uses https://<org>.okta.com/oauth2/default, and Azure AD uses https://login.microsoftonline.com/<tenantId>/v2.0. Supabase uses https://<ref>.supabase.co/auth/v1. Consequently, checking iss against your expected pattern confirms the token came from your configured provider rather than an arbitrary signer. The exact iss value should come from your provider's documentation or discovery endpoint rather than being guessed from the token you received.

Comparing iss before signature checks

Read the issuer value early in the validation path. If iss is unknown, reject the token without fetching a JWKS or spending CPU on signature verification. If iss is expected, use its documented discovery endpoint to locate the correct key set. This keeps multi-provider systems fast and prevents accidental validation against the wrong issuer. Verifying iss before touching the JWKS also reduces the attack surface, because an attacker who sends tokens from an unknown issuer triggers an immediate rejection without your server ever performing a network call to fetch keys or spending CPU on a signature check that would fail anyway.

Multi-tenant security

In multi-tenant applications, iss validation prevents cross-tenant token acceptance, which is a vulnerability that arises when your API accepts tokens from any tenant rather than only from tenants you have explicitly approved. Azure AD includes the tenant ID in iss, so two different companies' Azure AD tokens are visually similar but have different iss values, and verifying iss against your expected tenant's URL rejects tokens from other tenants even when they are cryptographically valid.2 Building on this, Auth0 tenants each have unique iss URLs, and a valid Auth0 token from a different customer's tenant would pass signature verification if verified against that tenant's JWKS, making iss validation the essential backstop that catches this cross-tenant confusion.

Discovery and configuration

The correct iss value for your provider is available in the discovery document at <issuer>/.well-known/openid-configuration, and the issuer field in this document is the exact string that should match the iss claim in every token the provider issues.3 Configuring your JWT validation with this exact string, rejecting any deviation including trailing slashes, prevents subtle iss manipulation attacks where an attacker crafts a plausible-looking but different issuer URL. When onboarding a new provider, copy the discovery issuer into configuration first and test with a real token from that environment, because this catches realm, tenant, and path differences before a production request depends on the setting.

Configuring iss validation in your library

Configuring iss validation means passing the expected issuer string to your JWT library at startup, not deriving it dynamically from each incoming token. In PyJWT, the issuer parameter in jwt.decode() accepts the expected string and raises InvalidIssuerError on mismatch. In jose (JavaScript), jwtVerify() accepts an issuer option. Both libraries use exact string equality for the comparison: a trailing slash present in your config but absent from the token causes a rejection.

Multi-provider validation strategy

For multi-provider applications, run one validator instance per issuer rather than a single validator with a list of acceptable values. Separate instances make the accepted issuer set explicit and prevent a misconfiguration from silently accepting tokens from an unintended provider as you extend the configuration over time. Test the validator with a known-valid token from your configured issuer before deploying to verify the exact iss string is correct.

Trailing slash and case sensitivity in iss

Issuer URL comparison is case-sensitive and character-exact. Auth0 iss values always include a trailing slash: https://yourapp.auth0.com/. Google's iss does not include a trailing slash: https://accounts.google.com. Keycloak iss values also omit the trailing slash. Copy the expected iss directly from a decoded token or from the provider's discovery document rather than typing it from memory.

Case sensitivity in realm and tenant names

Case sensitivity matters for providers whose realm or tenant names include uppercase letters in the URL path. A Keycloak realm named MyRealm produces iss values with uppercase characters in the path; storing the expected iss as myrealm fails the comparison silently. Decode a real token from your environment and paste the raw iss value into your configuration file to avoid transcription mistakes that are difficult to spot in a log.

Treating the decoded value as the source of truth removes guesswork from a setting that must match exactly or every token fails. A trailing slash or a wrong realm name is invisible until a real request hits your middleware and gets rejected. Saving the value directly from a working token eliminates that class of configuration typo entirely.

Try in the tool

What this page covers

  • Auth0 https://<tenant>.auth0.com/
  • Google https://accounts.google.com
  • Okta https://<org>.okta.com/oauth2/default
  • Azure AD https://login.microsoftonline.com/<tenantId>/v2.0
  • Supabase https://<ref>.supabase.co/auth/v1

Open the JWT Decoder & Claims Inspector tool to try this yourself.

Open the tool →
Sources
  1. 1.

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

  2. 2.

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

  3. 3.

    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

FAQ

What Is the JWT sub Claim?

For identity storage, the sub (subject) claim carries the stable identifier for the authenticated user or service. Developers commonly use sub as the primary key for user database records: it is guaranteed stable across email changes, password resets, and profile updates. Reading the sub claim correctly determines the identity behind every authenticated request.

What is the sub claim?

The sub (subject) claim is a string that identifies the principal the JWT represents: typically a user, service account, or machine client. RFC 7519 requires sub to be unique within the context of the issuer and stable over the lifetime of the subject.1 Unlike email or username, sub does not change when a user updates their account information, making it the correct identifier to store in your database as a foreign key.

sub formats by provider

Each provider uses a different sub format that encodes authentication details in a provider-specific pattern. Auth0 encodes connection type and provider user ID as google-oauth2|108512345 or auth0|64a1b2c3.2 Google uses a stable numeric string such as 108512345678901234567 that never changes even when the user updates their email. AWS Cognito uses a UUID like a1b2c3d4-e5f6-7890-abcd-ef1234567890. Clerk uses a prefixed string like user_2abc123def. Keycloak also uses UUIDs. Consequently, the sub format immediately reveals which provider issued the token without needing to read iss, and storing sub alongside the iss value lets your database accommodate users from multiple providers without sub collision across tenants.

Using sub without assuming the provider

The sub value is useful even before you know every provider-specific claim. Decode the token, note the sub shape, and use it to route the request to the correct validation path. A prefixed Clerk sub, an Auth0 connection prefix, and a UUID all require the same stability guarantee but different display and lookup rules. Building a small helper function that parses the sub format based on the iss value keeps provider-specific logic isolated in one place.

Stability guarantee

The sub claim is stable by specification: the same user will have the same sub in every token from the same issuer, forever, and changing email addresses, unlinking social accounts, or resetting passwords does not change sub. Building on this guarantee, using email as a database primary key causes orphaned records when users update their email, while using sub as the primary key survives all profile changes without requiring cascading updates across related tables. The only scenario where sub changes is account deletion and re-creation, which is intentional since a new account represents a new subject.

This stability guarantee is local to the issuer, not global across the internet, so always store iss with sub when multiple providers are possible and never compare sub values from different issuers as if they belong to the same namespace, because that simple pairing prevents rare but confusing account collisions in multi-provider systems.

Machine and service subjects

sub is not exclusively a user identifier, and understanding this distinction is essential for writing authorization logic that correctly handles both human and machine callers. OAuth 2.0 Client Credentials flow tokens use the application's client_id as sub, identifying the machine rather than a human user. Clerk M2M tokens use the machine client ID, and AWS Cognito service tokens use the Cognito app client ID. Consequently, your API should not assume sub always refers to a human user: check the token_use or azp claim alongside sub to determine whether the caller is a person or a service, and always validate the caller type before applying user-specific authorization rules that would grant inappropriate permissions to a machine account.

When your application supports multiple identity providers

When your application accepts tokens from more than one provider, storing sub alone as the user identifier risks collision.3 A Cognito UUID and a Keycloak UUID are both valid UUID strings; two users from different providers could share the same sub value. Store a composite key of iss and sub together, either as two columns in your users table or as a concatenated string in a single indexed column.

Link-on-first-login for multi-provider accounts

Alternatively, assign an internal user ID at first sign-in and link the provider's sub to it in a separate identities table. This approach supports users who later link multiple providers to a single account, which a sub-as-primary-key design makes awkward after the first record is created. Most social login flows benefit from the link-on-first-login pattern rather than treating provider sub values as permanent cross-provider identifiers.

Using sub as a database foreign key

Sub functions as the foreign key that connects a JWT to a user row in your application database. After verifying the token, look up the user: SELECT * FROM users WHERE provider_sub = $1. Create the user row on first sign-in if no match exists, then use the returned internal user ID for all subsequent application logic. Your code reads sub once per request during the lookup and discards it after; internal user IDs drive all downstream queries.

Indexing the sub column for performance

Index the provider_sub column. Every authenticated request triggers this lookup, and a table scan on a large users table adds hundreds of milliseconds to request processing time. A unique index on the sub column, or a composite unique index on iss and sub for multi-provider setups, makes the lookup constant-time regardless of how many users your application has. Without that index, every authenticated request performs a full table scan that grows linearly with your user base, which is a performance regression that often goes unnoticed in development with a small dataset but becomes critical at production scale.

A composite index on iss and sub handles multi-provider setups in a single lookup and avoids the ambiguity of matching sub values across different issuers. Without the index, growth in user count translates directly into slower authentication for every request. Adding the index once during schema setup is far cheaper than diagnosing latency after the table has millions of rows.

Try in the tool

What this page covers

  • Auth0 google-oauth2|108512345 (social) or auth0|64a1b2c3 (database connection)
  • Google a stable numeric string, e.g. 108512345678901234567
  • AWS Cognito / Keycloak a UUID, e.g. a1b2c3d4-e5f6-7890-abcd-ef1234567890
  • Clerk a prefixed string, e.g. user_2abc123def

Open the JWT Decoder & Claims Inspector tool to try this yourself.

Open the tool →
Sources
  1. 1.

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

  2. 2.

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

  3. 3.

    J. Bradley, M. Jones, and H. Tschofenig, "JSON Web Token Best Current Practices," RFC 8725, IETF, February 2020. https://datatracker.ietf.org/doc/html/rfc8725

FAQ

What Is the JWT exp Claim?

JWT expiry starts with the exp (expiration) claim, which sets the exact moment the token must be rejected. Comparing exp against the current time is one of the first checks JWT libraries perform during verification: a token past its exp is invalid regardless of signature validity. Paste any token above to see the exp value as a human-readable timestamp.

What is the exp claim?

The exp (expiration time) claim is a JSON number representing the number of seconds elapsed since January 1, 1970, at 00:00:00 UTC: called a NumericDate in RFC 7519.1 Servers must reject any token whose current time is at or after the exp value. Unlike a timestamp in milliseconds (used by JavaScript's Date.now()), exp is always in seconds: the most common source of expiry-related bugs in JWT integrations.

NumericDate format

RFC 7519 defines NumericDate as an integer or decimal number representing seconds since the Unix epoch: never milliseconds.1 The number 1748736000 represents a specific second in June 2026. Multiplying by 1000 gives the millisecond value expected by JavaScript's new Date(1748736000 * 1000). Consequently, passing the raw exp value to new Date() without the multiplication produces a date in 1970, the classic symptom of the seconds/milliseconds confusion. Libraries that conform to RFC 7519 always treat exp as seconds and convert to platform-native time types automatically.

Converting exp before debugging expiry

When a token appears expired immediately, convert the raw exp value before changing issuer settings. If the decoded date is years away, check the verifying server clock and clock skew tolerance. If the decoded date is in 1970 or a far-future year, compare exp with iat to confirm whether the issuer used seconds or milliseconds. A correctly formed token will have exp a reasonable interval after iat, typically measured in minutes or hours, so a delta of thousands of years between the two timestamps is an immediate signal that the unit conversion went wrong.

Validation timing and clock skew

A server rejects a token when Date.now() / 1000 >= exp, allowing for a configurable clock skew tolerance that absorbs minor clock drift between the issuing server and the verifying server, typically set to 30 to 60 seconds depending on your infrastructure reliability.2 Without any tolerance, a token issued with a 30-second lifetime might be rejected on a server whose clock is just 5 seconds ahead of the issuer, causing intermittent authentication failures that are difficult to reproduce. Building on this, very short-lived tokens such as Clerk's default 60-second session tokens require tighter NTP synchronisation across your entire infrastructure than standard 15-minute access tokens, because even a small clock drift can cause a significant percentage of those short-lived tokens to be rejected prematurely.

Tokens without exp

RFC 7519 makes exp optional. Long-lived service tokens and API keys are sometimes issued without exp for operational simplicity. A token with no exp claim does not expire by time: it remains valid until explicitly revoked or the signing key is rotated. Yet indefinite validity is a security risk: a stolen service token with no exp provides permanent access.

When issuing tokens without exp, implement explicit revocation via a jti blocklist or a server-side token store that you can invalidate. The decoder above displays 'No exp claim' clearly for such tokens. In practice, prefer short-lived service tokens unless you can prove that revocation, monitoring, and key rotation are all enforced consistently.

For access tokens: recommended exp values by use case

For access tokens protecting APIs that handle financial or health data, set exp to 5 to 15 minutes. Sensitive resources tolerate more frequent refresh operations because the shortened revocation window outweighs the latency cost of silent token refresh. For general web application APIs, 60 minutes balances security with user experience; the auth SDK refreshes the token before expiry without user-visible prompts.

Machine-to-machine token exp and refresh tokens

For machine-to-machine tokens not tied to a user session, use shorter lifetimes of 10 to 30 minutes combined with client credential rotation. Refresh tokens carry longer exp values: 7 to 30 days for interactive users and 1 to 24 hours for M2M clients. Access token exp and refresh token exp are configured separately in your identity provider's dashboard; verify which setting you are changing before saving.

When exp validation fails in your middleware

When exp validation fails, your middleware should return a 401 with a WWW-Authenticate header indicating the token has expired.3 Well-behaved clients read this header and attempt a token refresh before retrying the original request. Return a 403 only when the token is valid but the user lacks permission; using 403 for expired tokens confuses client SDKs that treat 403 as a permanent denial and skip the refresh step.

Distinguishing expired from malformed tokens

For server-rendered applications, redirect an expired session to the login page rather than returning a raw 401 JSON body. Distinguish between expired tokens, which are recoverable by refresh, and malformed tokens, which require re-authentication, in your error handling. Both produce exceptions in your JWT library, but only expired tokens are recoverable without user interaction; branching on the exception class produces the correct user experience for each case.

Mapping each exception to the correct client action keeps your application responsive instead of treating every failure as a hard stop. A malformed token should trigger re-authentication while an expired one prompts a silent refresh in the background. Testing both branches with sample tokens ensures the user experience matches the error your library actually raises.

Try in the tool

What to look for

  • NumericDate: seconds since Unix epoch
  • 5-60 minutes
  • 30-60 seconds

A decoded exp landing in 1970 means the issuer sent milliseconds where seconds were expected.

Open the JWT Decoder & Claims Inspector tool to try this yourself.

Open the tool →
Sources
  1. 1.

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

  2. 2.

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

  3. 3.

    OWASP, "Session Management Cheat Sheet," owasp.org, accessed June 2026. https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html

FAQ

What Is the JWT aud Claim?

For API routing, the aud (audience) claim specifies who the token is for. Validating aud prevents cross-service token misuse: a token issued for your analytics API should not authenticate against your billing API, even if both accept tokens from the same provider. Without aud validation, any service behind your auth provider becomes accessible with any valid token.

What is the aud claim?

The aud (audience) claim identifies the recipients the JWT is intended for: typically the API, service, or client application that should accept it. It can be a single string or an array of strings.1 A recipient that receives a JWT must verify that its own identifier appears in the aud claim. If aud is present and does not include the recipient's identifier, the token must be rejected even if the signature is valid.

aud formats and values

Providers use different conventions for the aud value, and understanding these conventions is essential for writing correct audience validation logic. Auth0 uses the API audience URL you configure, such as https://api.yourapp.com/, or the client_id for ID tokens. Azure AD uses the application (client) ID GUID. AWS Cognito uses the app client ID for ID tokens and omits aud from access tokens entirely, substituting client_id in its place. Consequently, the expected aud value for each token type should come from your provider's configuration dashboard rather than being guessed, because mismatching the aud causes immediate rejection even with a perfectly valid signature.

Matching aud to token type

Read the token type before choosing the expected audience, because ID tokens target the application client while access tokens target the API resource, and comparing against the wrong one causes a valid token to fail. Decoding the token in the tool above before writing any validation code lets you see the exact aud value the issuer included, which prevents the common mistake of guessing the expected audience string and getting the format slightly wrong.

String vs array

RFC 7519 allows aud to be either a single string when the token has one audience or a JSON array of strings when multiple audiences are intended, and your verification code must handle both formats correctly.1 Auth0 access tokens that request multiple API audiences use an array, so verifying aud in the array case requires checking whether your service identifier appears anywhere in the array rather than comparing the whole array to a single string. Building on this, some JWT libraries default to strict string comparison and must be explicitly configured to handle the array case, so always check your library's documentation for the array-aware audience validation setting to avoid false rejections of valid multi-audience tokens.

Multi-audience and security

Including multiple audiences in one token enables a single access token to work across several APIs. Yet this convenience weakens isolation: any compromised API can forward a received token to another API in the audience list. Consequently, the narrower the aud, the stronger the isolation. For high-security APIs, issue tokens with a single aud matching only the target service, and reject any token whose aud includes additional services. The extra token exchange is a security investment, not unnecessary friction.

When you design a token for a gateway plus downstream services, decide whether each service needs its own audience or whether the gateway alone should be the audience. If downstream services receive the same token, they inherit the same trust boundary. If they need independent access control, issue or exchange a narrower token at the gateway layer.

When your API receives a token with the wrong aud

When your API receives a token whose aud does not include your service identifier, reject it with a 401 before reading any other claim.2 Return 401 rather than 403 at this stage: the token does not address your API and should be treated as a missing credential, not as an access denial for a valid user. Log the aud value you received alongside the value you expected to make debugging straightforward.

Keeping rejection messages generic

Keep rejection error messages generic in API responses: Invalid token rather than Audience mismatch. Detailed aud-related error messages help an attacker understand which audience values your API accepts. Log the full rejection detail internally with a unique request ID so your team can investigate without exposing the information in the response body. Including the expected aud value alongside the received aud value in your internal logs turns a cryptic mismatch into a one-line diagnosis, which is especially valuable when debugging issues across multiple microservices that each have different audience configurations.

API gateway enforcement of aud

API gateways such as AWS API Gateway JWT authorizers and Kong's jwt plugin validate aud at the infrastructure layer before your application code runs.3 Configuring the expected audience in the gateway eliminates the need to check aud in individual route handlers. In AWS API Gateway, set the audience field in the JWT authorizer configuration; the gateway rejects non-matching tokens before they reach your Lambda or container.

Delegating aud enforcement to Kong and API Gateway

For Kong, configure the claims_to_verify option with aud and your expected value. Both gateways cache JWKS keys and handle rotation automatically, removing signature and audience verification from your application entirely. When the gateway handles aud validation, your application code receives only validated, trusted claims; it does not need to re-verify the token on every route. This separation of concerns also means that rotating JWKS keys or updating the expected audience requires a gateway configuration change rather than a full application redeploy, which simplifies operations in environments where gateway and application release cycles are managed by different teams.

Gateway-level enforcement also means a new service joins the trusted set by updating gateway config rather than editing application code across teams. The application receives only tokens that already passed the audience check, so route handlers stay focused on business logic. This split keeps security policy consistent even when the gateway and the application ship on independent release schedules.

Try in the tool

What this page covers

  • Auth0 API audience URL (access token) or client_id (ID token)
  • Azure AD the application (client) ID GUID
  • AWS Cognito omits aud from access tokens entirely; substitutes client_id instead

Open the JWT Decoder & Claims Inspector tool to try this yourself.

Open the tool →
Sources
  1. 1.

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

  2. 2.

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

  3. 3.

    J. Bradley, M. Jones, and H. Tschofenig, "JSON Web Token Best Current Practices," RFC 8725, IETF, February 2020. https://datatracker.ietf.org/doc/html/rfc8725

FAQ

What Is a Bearer Token?

HTTP requires an explicit convention for passing credentials, so RFC 6750 defines the Bearer token scheme for the Authorization header.1 Any party that presents a bearer token receives the access it grants: no identity proof beyond possession of the token is required. JWTs are the most common bearer token format in modern APIs, but any opaque string can serve as a bearer token.

What is a Bearer token?

A Bearer token is any token that grants access to a resource simply by being presented: no cryptographic proof of identity is required beyond possession of the token itself. The term comes from the HTTP Authorization header scheme: Authorization: Bearer .1 Unlike mutual TLS or signature-based schemes, bearer tokens require only HTTPS to protect the token from interception. Any party who obtains the token: legitimately or through theft: can use it until it expires.

The Authorization header format

RFC 6750 specifies the exact header format: Authorization: Bearer followed by a space and the token string. The token occupies the remainder of the header value without quotation marks or encoding. HTTP parsers split on the first space, making Bearer the scheme identifier and everything after the credential. Consequently, all standard JWT libraries and API gateways expect this exact format. Tokens passed in query parameters (?access_token=) or custom headers are also valid under RFC 6750 but are less preferred because query parameters appear in server logs.

Splitting the header safely

Treat the header as a scheme plus a credential. First confirm the value starts with Bearer and contains a space, then use the substring after that space as the token. Reject missing headers, empty credentials, or unexpected schemes with a 401 response. This keeps parsing errors separate from authentication failures and avoids null-reference bugs in route handlers. A token with a trailing newline or carriage return copied from a log file will fail signature verification silently, so trimming whitespace after extracting the credential is a defensive step that prevents an entire class of hard-to-diagnose authentication failures.

Bearer vs other authorization schemes

Bearer tokens differ from the Basic and Digest schemes in that they assert access without embedding credentials that prove the caller's identity through a cryptographic challenge. Basic authentication sends a username and password in every request, while Digest sends a challenge response that proves password knowledge through a hash-based handshake; bearer tokens skip all of this by making the token itself the sole proof of authorization. Building on this simplified model, Bearer tokens work with any underlying token format including JWTs, opaque strings, or provider-specific formats, and replacing the token type does not require changing the HTTP header format, which makes Bearer a flexible transport layer that your API can support regardless of which identity provider issued the credential.

Security implications

The 'bearer' name reflects the security model: whoever bears the token gets the access it represents, with no further challenge. Consequently, HTTPS is not optional for bearer token APIs: HTTP would expose the token to any observer on the network path. Stolen bearer tokens grant full access until expiry, making short token lifetimes and refresh token rotation the primary mitigation.2 Furthermore, tokens should never appear in URL query parameters in production, since web server access logs capture URLs and may retain them longer than the token's intended lifetime.3

For browser applications, keep access tokens out of persistent storage. In-memory storage is not perfect, but it reduces exposure compared with localStorage because the token disappears on reload and is not exposed to every tag manager script. Pair short access-token lifetimes with HttpOnly refresh cookies when your application needs both usability and a smaller attack surface.

Attaching bearer tokens in HTTP client code

Passing a bearer token in a web framework typically uses a request interceptor that reads the current token and attaches it to every outgoing request. In Axios, set the Authorization header in a request interceptor by modifying config.headers.Authorization. In the native Fetch API, pass the header directly in the request options object. Both approaches centralise token attachment in one place rather than adding header logic to each individual API call.

Extracting tokens in Express.js and Next.js routes

In Express.js and Next.js API routes, extract the bearer token from the incoming request using req.headers.authorization and splitting on the space after Bearer. Always confirm the header exists and the prefix matches before splitting; a missing Authorization header or an unexpected scheme should produce a 401 immediately rather than a runtime error on an undefined value. Wrapping the extraction in a reusable middleware function means every route gets consistent parsing behavior, and you can unit test the edge cases like missing headers, lowercase bearer prefixes, and multi-space gaps between the scheme and the token.

Unit testing these edge cases prevents a brittle parser from rejecting valid tokens or accepting malformed ones in production. A lowercase scheme or an extra space is common in hand-built requests and proxies, so normalising the prefix before comparison avoids surprising failures. Centralising extraction also means the same trusted logic runs on every route instead of being reimplemented per handler.

Storing bearer tokens safely in the browser

Single-page applications should store access tokens in memory: a JavaScript variable or a state management store that clears on page reload. In-memory storage prevents XSS attacks from reading tokens because a script running in a third-party context cannot access another module's internal variables. Avoid localStorage and sessionStorage for token storage; both are accessible to any script on the page, including analytics or chat widgets loaded by tag managers.

HttpOnly cookies for refresh token storage

Store refresh tokens in HttpOnly cookies set by your server.4 An HttpOnly cookie is invisible to JavaScript; document.cookie cannot read it. The browser attaches the cookie automatically on matching domain requests. Restrict the cookie's path to your token-refresh endpoint, such as /auth/refresh, to prevent the browser from sending the refresh token with every API call and limiting its exposure if a single endpoint is compromised.

Try in the tool

What this page covers

  • Authorization: Bearer <token> the exact RFC 6750 header format; parsers split on the first space
  • Access token storage in memory only, never localStorage or sessionStorage
  • Refresh token storage an HttpOnly, Secure, SameSite cookie scoped to the refresh endpoint

Open the JWT Decoder & Claims Inspector tool to try this yourself.

Open the tool →
Sources
  1. 1.

    D. Hardt, "The OAuth 2.0 Authorization Framework: Bearer Token Usage," RFC 6750, IETF, October 2012. https://www.rfc-editor.org/rfc/rfc6750

  2. 2.

    J. Bradley, A. Labunets, and D. Fett, "Best Current Practice for OAuth 2.0 Security," RFC 9700, IETF, January 2025. https://datatracker.ietf.org/doc/rfc9700/

  3. 3.

    OWASP Foundation, "REST Security Cheat Sheet," owasp.org, accessed June 2026. https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html

  4. 4.

    Mozilla Developer Network, "Using HTTP cookies," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies

FAQ