What the JWT decoder does
The decoder reads a JSON Web Token: it splits it into its parts, decodes the header and the payload from base64url and shows them as JSON, and lists every claim with its value as written. Times like exp get their dates in UTC and in your time zone, and a status says whether the token is active, expired or not yet valid, counting every second. Notes point out an unsigned token, headers that name their own keys, secrets in the payload and lifetimes of more than 30 days. It never checks the signature, and it says so on every token.
How to decode a JWT
- Paste the token. A
Bearerprefix, quotes around it and line breaks from a wrapped token are set aside. - Read the status beside Signature not verified: Active, Expired, Not yet valid or No expiry.
- Look at the header and the payload, and at Claims for each claim's meaning and date. Time display shows dates in UTC only, or in your time zone too.
- Copy the header, the payload or a single claim. If Problems lists one, select it to see its place in the token.
Examples
Input
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJpYXQiOjE2OTk5OTY0MDAsImV4cCI6MTcwMDAwMDAwMH0.VV63pGQEZPvcrCE-T8RwBleef2XGSfFqwDeHK9qOOIY
Output
Header: {"alg":"HS256","typ":"JWT"}
Payload: {"sub":"123","iat":1699996400,"exp":1700000000}
iat: 2023-11-14T21:13:20Z
exp: 2023-11-14T22:13:20Z
Status: Expired · Signature not verified
Input
eyJhbGciOiJub25lIn0.eyJzdWIiOiIxMjMifQ.
Output
Header: {"alg":"none"}
Payload: {"sub":"123"}
Status: No expiry · Signature not verified
Input
eyJhbGciOiJIUzI1NiJ9.eyJzdWIi*fQ.c2ln
Problem reported
Line 1, column 30: * is not a base64url character, in the payload. base64url has A–Z, a–z, 0–9, - and _ only. (JWT_BAD_BASE64URL)
How a JWT is built
A signed JWT, a JWS in the terms of RFC 7515 (opens in a new tab), has three parts joined by dots: header.payload.signature. The header is a JSON object that names the signing algorithm in alg, often with typ: "JWT" and a key ID in kid. The payload is a JSON object of claims, statements about the user or the session. Both are written in base64url, the URL-safe Base64 without = padding; the Base64 Encoder & Decoder shows how that alphabet works.
The signature is computed over the first two parts exactly as they are written, header.payload, with the issuer's secret or private key. Change one character of either and the signature no longer matches. That is the only protection a JWS has: its contents are readable by anyone.
An encrypted JWT, a JWE (RFC 7516 (opens in a new tab)), has five parts: a header, an encrypted key, an initialization vector, the ciphertext and an authentication tag. Its header can be read; its claims open only with the recipient's key, so the decoder shows the header and the enc algorithm and stops there.
Registered claims and NumericDate
RFC 7519 (opens in a new tab) defines seven claims with fixed meanings. iss names the issuer, sub the subject, usually a user ID, and aud the audience, the service or services the token is for: a string or a list. exp is when the token expires, nbf when it starts to be valid and iat when it was issued. jti is a unique ID, which lets a server refuse a token it has seen before. Any other claim, like scope or email, is up to the issuer.
The three times are NumericDates: seconds since 1970-01-01T00:00:00Z, without leap seconds, possibly with a fraction. A common mistake is to write them in milliseconds, as JavaScript's Date.now() returns them. 1700000000000 read as seconds lies in the year 55840, so the decoder flags it and shows what it would be as milliseconds. The Unix Timestamp Converter converts such values both ways.
The decoder compares exp and nbf with your clock to the second, with no tolerance. Servers usually allow a little clock skew, a few seconds up to a few minutes, so a token that has just expired here may still pass there, and one issued by a server whose clock runs ahead may look not yet valid for a moment.
Decoding is not verifying
Anyone can decode a JWT: the header and the payload are only base64url, not encrypted. What makes a token trustworthy is its signature, and checking it needs the key: the shared secret for HMAC algorithms like HS256, or the issuer's public key for RSA, RSA-PSS, ECDSA and EdDSA. That check belongs on the server that accepts the token, with a maintained JWT library.
A verifier should also decide which algorithms it accepts instead of taking alg from the token, and check iss, aud, exp and nbf as well as the signature. A token can be perfectly readable here and still be rejected there for any of these reasons. This decoder never verifies a signature, which is why every token, valid or not, shows Signature not verified.
Security pitfalls
RFC 8725 (opens in a new tab), the best current practice for JWTs, lists the mistakes that keep recurring, and the decoder notes the ones a token itself reveals:
alg: nonedeclares an unsigned token. Libraries that once accepted it let anyone forge tokens; a verifier must refuse it.- Key headers:
jkuandx5upoint to URLs to fetch keys from,jwkandx5ccarry a key or a certificate inside the token. A verifier that trusts them lets the token choose the key it is checked with. - Secrets in the payload: a claim named
password,api_keyorssnis readable by everyone who has the token, including logs and browser history. - Long lifetimes: a token valid for weeks stays usable long after a leak. Access tokens usually live minutes or hours, renewed with a refresh token.
Options
| Option | Values | Default | When to change it |
|---|---|---|---|
| Time display | Local and UTC, UTC only | Local and UTC | UTC only when you compare with server logs, or share a screenshot that should not reveal your time zone. |
Common errors and fixes
| Code | What it means | How to fix it |
|---|---|---|
JWT_Error | The text does not have 3 parts, or 5 for a JWE, or its header or payload is empty: often a token cut short when it was copied. | Copy the whole token again; it ends after the signature, before any space or quote. |
JWT_Error | A part has a character that base64url does not use, like = padding, + and / from standard Base64, or a space; or a length that cannot be base64url. | Copy the token again; the marked character shows where it broke. |
JWT_Error | The header or the payload decodes to something that is not valid JSON. | The token was changed or built by hand; ask its issuer for a new one. |
JWT_Error | The header or the payload is valid JSON, but a string, a number or a list instead of an object. | A JWT's header and payload are always objects; the token was not made by a JWT library. |
JWT_Warning | The token is encrypted (five parts): its claims need the recipient's key. | Decrypt it in the service that holds the key; only the header can be read here. |
JWT_Warning | The header says the token is not signed. | Never accept it in production; make the verifier require the algorithm it expects. |
JWT_Warning | The current time is past exp. | Get a new token, or renew it with a refresh token. |
JWT_Warning | The current time is before nbf. | Wait until nbf, or check the issuer's clock. |
JWT_Warning | The payload has no exp claim, so the token never expires. | Have the issuer add exp; a token without it stays valid until its key changes. |
JWT_Warning | exp, nbf or iat is not a NumericDate: a string, milliseconds instead of seconds, a negative value or one outside every date. | Write the time as a number of seconds since 1970-01-01T00:00:00Z. |
JWT_Note | The token is valid for more than 30 days between its iat and its exp. | Shorten the lifetime and renew tokens more often. |
JWT_Note | The header has jku, x5u, jwk or x5c: a key source chosen by the token itself. | Verify with keys the server already trusts, never with keys the token brings. |
JWT_Warning | The payload has a claim named like a secret, such as password or api_key. | Keep secrets out of tokens; send an ID and look the data up on the server. |
JSON_Warning | The header or the payload has the same claim twice; libraries disagree on which one counts. | Remove the duplicate where the token is made; two readings of one token are a known attack. |
JSON_Note | A claim is an integer larger than 2^53 − 1, which JSON.parse rounds. | Nothing to fix here: the decoder shows it exactly. Issuers often write such IDs as strings. |