How to Decode a JWT Token Online: Claims, Expiry and Common Auth Failures

Decoding a JWT turns the dense `eyJhbGci…` string into two readable JSON objects: the header, which says how the token was signed, and the payload, which says who it is for and until when. Most authentication bugs become obvious once you can read those fields: an expired token, the wrong audience, a token from the staging identity provider. Decoding takes seconds; the useful part is knowing what to look for afterwards.

Decode a JWT in three steps

Paste the token into a JWT decoder. The tool splits it at the two dots, Base64url-decodes the first two parts and shows them as JSON. The third part, the signature, is binary and is not decoded into anything readable.

If the tool reports an invalid token, check what you pasted: a leading Bearer , surrounding quotes from a JSON value, or a trailing newline are the usual reasons. A token with four dots, five parts, is an encrypted JWE and cannot be decoded without the key.

Remember that a token is a working credential until it expires. Use a decoder that runs in your browser without uploading the token, or decode it locally as shown below, especially for production tokens.

"JWT decrypt" is a common search, but a normal JWT is not encrypted. Its contents are only encoded, which is exactly why anyone can decode it, and why sensitive data does not belong in it.

Reading the header

{
  "alg": "RS256",     // how it is signed: RS256/ES256 = public-key, HS256 = shared secret
  "typ": "JWT",       // "at+jwt" marks an OAuth access token (RFC 9068)
  "kid": "a1b2c3"     // which key in the issuer's JWKS signed it
}

alg tells you what the verifier needs: a public key for RS256, PS256, ES256 or EdDSA, or the shared secret for HS256. kid is what to look for in the issuer's JWKS document. If verification fails with "no matching key", compare this kid with the keys the verifier has loaded. A mismatch usually means the token came from a different environment, or the provider rotated keys and the verifier's cache is stale.

alg: none in a token you received is a red flag. Legitimate systems do not issue unsigned tokens.

Reading the payload

{
  "iss": "https://login.example.com/",       // issuer: which identity provider
  "sub": "auth0|64f1c2a9",                   // subject: the user or client ID
  "aud": ["https://api.example.com"],        // audience: which API it is for
  "iat": 1790000000,                         // issued at   (seconds since 1970)
  "exp": 1790003600,                         // expires at  (one hour later)
  "scope": "orders:read orders:write",
  "azp": "spa-client-id"                     // authorised party: the client app
}

The time fields are Unix timestamps in seconds. Convert them to dates to see the token's lifetime at a glance: new Date(exp * 1000) in JavaScript, date -u -d @1790003600 on Linux or date -u -r 1790003600 on macOS. A decoder that shows "expires in 4 minutes" or "expired 2 hours ago" saves the arithmetic.

The rest are application claims. scope or scp lists OAuth permissions, roles or groups carry authorisation data, and ID tokens from OpenID Connect add email, name and nonce.

The checks that explain most auth failures

Once the token is decoded, go through the claims in this order. One of them explains most 401 and 403 responses.

Expired: exp is in the past. Compare with the server's clock rather than your laptop's, because clock skew of a minute or two causes intermittent failures right at expiry.

Not yet valid: nbf or iat is in the future, which usually points to clock skew between the issuer and the API server.

Wrong audience: aud does not contain the identifier the API expects. This happens when a front end sends an ID token, whose aud is the client ID, instead of an access token, whose aud is the API. It also happens when the audience or resource parameter was omitted from the authorisation request.

Wrong issuer: iss points to another tenant or environment (dev- versus prod-), or differs by a trailing slash from the configured value.

Missing permissions: the token is valid, so the error is a 403, not a 401, but scope lacks what the endpoint requires. Check which scopes the client requested.

Wrong token type: typ, token_use (AWS Cognito) or the presence of nonce and email shows an ID token where an access token is expected.

Decoding in code and on the command line

// Node.js / browser with the jose library: decode WITHOUT verifying
import { decodeJwt, decodeProtectedHeader } from 'jose';
const header = decodeProtectedHeader(token);
const claims = decodeJwt(token);

// Node.js without dependencies
const claims2 = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString('utf8'));
import jwt   # PyJWT
jwt.get_unverified_header(token)
jwt.decode(token, options={'verify_signature': False})   # inspection only
# Shell: payload as formatted JSON (tolerates missing padding)
printf '%s' "$TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq .

These functions deliberately skip signature verification, which is fine for debugging and display and wrong for any authorisation decision. In request handling, use the library's verify function with the expected algorithm, issuer and audience.

When the token will not decode

Not every access token is a JWT. Some providers issue opaque tokens, random strings with no dots, which only the provider can interpret through its introspection endpoint (RFC 7662). Microsoft Graph access tokens are JWTs, but their format is not a contract, and Microsoft advises clients not to decode them. Treat tokens meant for another API as opaque even when you can read them.

If the payload decodes to garbage, the token may be compressed or encrypted. JWE tokens have five parts and a header with enc. Some systems also use a zip: DEF header to deflate the payload before encryption.

Sources and further reading

RFC 7519 defines the JWT claims (iss, sub, aud, exp, nbf, iat, jti). RFC 7515 defines the header parameters and the compact serialisation. RFC 9068 defines the JWT access-token profile, OpenID Connect Core defines ID tokens, and RFC 7662 defines token introspection. The jose and PyJWT documentation describe their decode-without-verify functions.

Frequently asked questions

How do I decode a JWT token?

Split it at the dots, Base64url-decode the first two parts and read them as JSON. A JWT decoder does this for you; in code use `decodeJwt()` from `jose` or `jwt.decode(token, options={'verify_signature': False})` in PyJWT for inspection.

Can I decode a JWT without the secret?

Yes. The header and payload are only Base64url-encoded, so no key is needed to read them. The secret or public key is only needed to verify the signature.

Can a JWT be decrypted?

A normal signed JWT is not encrypted, so there is nothing to decrypt; decoding reveals its contents. Only an encrypted JWE, which has five parts, needs a key to read.

How do I check if a JWT is expired?

Decode the payload and compare `exp`, in seconds since 1970, with the current time: `Date.now() / 1000 > exp` means expired. Allow a small tolerance for clock skew.

Why does my decoded JWT show the wrong audience?

Often the client sent an ID token, whose audience is the client ID, instead of an access token for the API, or requested the token without the API's audience or resource parameter.

Is it safe to decode a JWT online?

Decoding itself is harmless, but the token is a live credential. Use a decoder that works entirely in your browser, or decode locally, and never paste production tokens into tools that send input to a server.