Decoding a JWT only reads it: the signature and the time decide whether to trust it
A JWT (JSON Web Token) is a string made of three parts separated by two dots: a header, a payload and a signature. The first two parts are JSON written as Base64url, so anyone can decode and read them without a key. That is why seeing the content after decoding tells you neither whether the token is genuine nor whether it can be used now. A server accepts a token only after it checks, in order, that the signature is valid, that the expiry time (exp) has not passed, and that the issuer (iss) and audience (aud) are the expected values.
The tool above decodes the token you paste right away and shows the values and meanings of the header and payload in a table. It then follows the same four checkpoints (format, signature, time, issuer and audience) and marks where the token gets stopped. If you add a secret or a public key (PEM, JWK, JWKS), it actually checks the signature, and the time left until exp counts down by the second on the timeline. The token and key are processed only in the browser and are not sent anywhere.
When you need to decode a JWT yourself
- When an API returns 401 Unauthorized: the reason is usually expiry (exp), a token meant for another service (aud) or a key rotation (kid). Paste the token and you can see at which of the four checkpoints it is stopped.
- When logins end sooner than expected: the gap between iat and exp is the token's lifetime. Access tokens often live only 15 minutes or an hour, so the lifetime and time left shown on screen let you check the setting.
- When you add OpenID Connect login: check that the ID token's iss, aud, nonce, email_verified and similar values arrive as expected. If you paste the JSON from the provider's jwks_uri as is, the tool checks the signature with the public key whose kid matches.
- When you are finding out why a permission is missing: claims such as scope, roles and groups are split into chips so you can see whether the needed permission is there. The service defines what the values mean, so compare them with the server configuration.
- When the server clock and your device clock differ: if you see "not valid yet" or "expired" right after issue, a clock difference may be the cause. Switch the tolerance between 0 s, 60 s and 5 min and watch how the verdict changes.
- When you need a test token: in "Edit JSON", change the payload and sign it again with a secret to get a token with the values you want. If you edit without the key, you can also see right away that the signature no longer matches.
How to use it
- Add the token: paste the JWT into the token field. It finds only the token even when it is mixed with other text, such as an
Authorization: Bearer …header, a token response JSON ({"access_token": "…", "id_token": "…"}) or#id_token=…in a callback URL. If there are several tokens, pick one with the chips. - Check the parts: the token text is colored by header, payload and signature, and the bar below shows the bytes of each part. Hover over a table row and the characters of the token that hold that value are highlighted.
- Check the time: the validity section shows the time left until expiry, iat, nbf and exp converted to your device time zone, and the token's lifetime. Choose the clock tolerance that matches the library your server uses.
- Check the signature: if alg is an HS algorithm such as HS256, enter the secret. For RS, PS, ES and EdDSA, enter the public key. Enter the issuer and audience your server expects and the fourth checkpoint is checked too.
- Edit: choose "Edit JSON" under "Content" to change the header and payload yourself. If the signature check section has a secret or private key, the tool builds a token signed again with the edited content.
Three parts separated by two dots
The header and payload are UTF-8 JSON converted to Base64url. Base64url uses - and _ instead of the + and / of ordinary Base64 and drops the trailing = (RFC 7515 section 2), so it can go straight into a URL or an HTTP header. The signature is the bytes computed with a key over the whole text "header.payload", written the same way, so changing even one character of the header or payload makes it stop matching. Below is an HS256 token made with the secret demo-secret-for-roberin-jwt-2026, split into its parts.
| Part | Token text | Decoded | Characters | Bytes |
|---|---|---|---|---|
| Header | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 | {"alg":"HS256","typ":"JWT"} | 36 | 27 |
| Payload | eyJzdWIiOiJ1c2VyLTEwMjQiLCJpYXQiOjE3MDQwNjcyMDAsImV4cCI6MTcwNDA2ODEwMH0 | {"sub":"user-1024","iat":1704067200,"exp":1704068100} | 71 | 53 |
| Signature | 5YZB1gnh7IFicIbrPtT-mJoFb3uuYFcYTk84hPIA7Ro | HMAC-SHA256 value (not JSON) | 43 | 32 |
Every 3 bytes become 4 characters, so a Base64url part is about one third longer than the original JSON. The length of the signature part depends on the algorithm: HS256 gives 32 bytes (43 characters), RS256 gives the size of the RSA key (a 2048-bit key means 256 bytes, 342 characters) and ES256 gives 64 bytes (86 characters). If there are five parts, the token is an encrypted JWE (RFC 7516). The header of a JWE is readable, but the other four parts cannot be decoded without the recipient's key.
Common claims and what to check
Each item in the payload is called a claim. The seven claims registered by RFC 7519 and the names defined by OpenID Connect have fixed meanings, and any other name is a value the issuing service defines on its own (a Private Claim). The "What to check" column in the table lists what a server that receives the token has to do.
| Claim | Meaning | What to check |
|---|---|---|
iss | Issuer | It must equal the trusted issuer value character for character. A single trailing / makes it a different value. |
sub | Subject (user ID) | It is unique only within the issuer, so combine it with iss to tell users apart. |
aud | Audience | A string or an array. Reject the token if your own name is not in it (RFC 7519 4.1.3). |
exp | Expiration time | The token is expired when the current time is equal to or later than exp. You may allow a tolerance of a few minutes at most for clock differences. |
nbf | Not before | If the current time is earlier than nbf, the token must not be accepted yet. |
iat | Issued at | Used to see how old the token is. It does not by itself decide expiry. |
jti | Token ID | If you record the jti values already used, you can stop the same token from being reused. |
nonce | Request check value (OIDC) | It must equal the value sent in the login request. If it differs, the token belongs to a different login. |
azp | App that received the token (OIDC) | If present, check that it equals your own client_id. |
scope | Permission scope | A list separated by spaces. Check that it contains the scope the requested action needs. |
Reading exp, nbf and iat, and the clock tolerance
All three times are seconds counted from 00:00 UTC on January 1, 1970 (NumericDate in RFC 7519). Written in New York time, the same moment is 5 hours earlier in winter and 4 hours earlier during daylight saving time. A value written in milliseconds has 13 digits, and read as seconds it points tens of thousands of years ahead, so the token effectively never ends. The tool adds a warning when it finds such a value.
| Value | UTC | New York (ET) | Note |
|---|---|---|---|
1704067200 | 2024-01-01 00:00:00 | 2023-12-31 19:00:00 (UTC−05:00) | iat of the example token above |
1704068100 | 2024-01-01 00:15:00 | 2023-12-31 19:15:00 (UTC−05:00) | exp of the example token, lifetime 900 seconds = 15 minutes |
2000000000 | 2033-05-18 03:33:20 | 2033-05-17 23:33:20 (UTC−04:00, DST) | A ten-digit seconds value |
1704067200000 | (read as seconds: year 55969) | - | Wrongly written in milliseconds, must be divided by 1000 |
The clock of your server and the clock of the issuing server can differ by anything from a few seconds to a few minutes. RFC 7519 says that when checking exp and nbf you may allow a small tolerance, "usually no more than a few minutes". The actual default differs by library, so the same token can be accepted by one server and rejected by another.
| Library | Setting name | Default |
|---|---|---|
| PyJWT (Python) | leeway | 0 s |
| jsonwebtoken (Node.js) | clockTolerance | 0 s |
| Spring Security (Java) | JwtTimestampValidator clockSkew | 60 s |
| Microsoft.IdentityModel (.NET) | TokenValidationParameters.ClockSkew | 5 min (300 s) |
The tool's verdict follows jsonwebtoken. If the current time (rounded down to the second) is equal to or later than exp plus the tolerance, the token is expired, and if nbf is later than the current time plus the tolerance, it is not valid yet. When the expiry time has passed but is still within the tolerance, the time checkpoint passes and the remaining margin is shown.
Checking the signature: the key each algorithm needs
The alg in the header is only the name of the method the token says it was signed with. The recipient must not simply trust it, so a server should decide the algorithms it allows in advance and reject anything else (RFC 8725 3.1). The tool tells you which kind of key fits alg, and gives the reason when you enter a key that does not fit.
| alg | Key used to check | Accepted formats | Rule |
|---|---|---|---|
HS256·HS384·HS512 | The same secret as the creator | Plain text, Base64 value, JWK (kty oct) | The key must be at least as long as the hash (256 bits for HS256, RFC 7518 3.2) |
RS256·RS384·RS512 | RSA public key | PEM public key, PKCS#1, X.509 certificate, JWK, JWKS | At least 2048 bits (RFC 7518 3.3) |
PS256·PS384·PS512 | RSA public key | Same as the RS family | RSA-PSS, salt length = hash length (RFC 7518 3.5) |
ES256 | P-256 public key | PEM, certificate, JWK (kty EC), JWKS | The signature is R and S joined, 64 bytes (RFC 7518 3.4) |
ES384 | P-384 public key | Same as ES256 | Signature of 96 bytes |
ES512 | P-521 public key | Same as ES256 | Signature of 132 bytes (the curve name is P-521) |
EdDSA·Ed25519 | Ed25519 public key | PEM, JWK (kty OKP) | RFC 9864 (October 2025) says to use the name Ed25519 instead of EdDSA |
If you enter a private key, the tool extracts the public key and checks with it, but a public key is all that checking needs. A line break is often added at the end when a secret is copied, so if the secret as entered does not match but matches with the leading and trailing spaces removed, the tool says so. An ES token whose signature is in DER format (starting with 0x30) was built wrongly by another library, so the tool marks that cause separately.
Why the checkpoints follow the order a server uses
Section 7.2 of RFC 7519 describes the order for receiving a JWT: split the parts, decode the Base64url and JSON, and check the signature. If any step fails, the token must be rejected. On top of that, RFC 8725 requires checking that the signing key belongs to the issuer when iss is present (3.8), and, when one issuer serves several applications, rejecting a token whose aud is missing or not yours (3.9). The tool's four checkpoints follow this order exactly and write the reason for the first checkpoint that stops the token on the verdict line. While the signature has not been checked, the other checkpoints stay marked "not checked yet" even if they pass.
Whether the permission is right, whether the session has already logged out, and whether the same jti was used again cannot be known from the token alone. A server checks those conditions with its own records and settings, so passing all four checkpoints only means that "the conditions of the token itself are met".
Editing JSON and signing again
When you change the header or payload under "Edit JSON", the tool builds a new token and puts it in the token field. If the signature check section has an HS secret or a private key for RS, PS, ES or Ed25519 (PKCS#8, PKCS#1, an EC private key, or a JWK with d), the token is signed again with that key. If there is only a public key or no key, the original signature is attached unchanged, so the signature checkpoint turns to a mismatch. The "Edited token" example is exactly this case. The role value was changed from user to admin while the signature stayed the same, so checking with the secret exposes it immediately.
The buttons "Set iat to now and exp to 15 min, 1 hr or 1 day" change the times of a test token in one step. If nbf is present, it is set to now as well. The edited JSON only has its spaces removed to make one line, so the key order and number notation stay as you wrote them.
Common mistakes and known attacks
- A server that accepts alg none: anyone can create a token without a signature. Some libraries even accepted None and NONE, which differ only in case, so block it with an allowed list (RFC 8725 2.1).
- Swapping an RS256 token for HS256: the attacker changes alg to HS256 and signs using the server's public key as the HMAC secret. Using one key for one algorithm only prevents it. The tool reports this form when you enter a public key for an HS token.
- A secret that is easy to remember: with just one HS256 token in hand, the secret can be found by a dictionary attack. Generate the secret randomly (RFC 8725 3.5) and make it at least as long as the hash output, which is 32 bytes for HS256 (RFC 7518 3.2).
- A server that does not check aud: it accepts a token the same issuer gave to another service. Using an ID token for API access is a mistake that has to be blocked for the same reason.
- Key lookup that uses kid or jku as is: if you put kid straight into a file path or SQL, or fetch keys from the jku address in the header, the check ends up using a key the attacker chose (RFC 8725 3.10).
- Personal data in the payload: a signed JWT is not encrypted. If you put an email or phone number in it, it can be read in any log or browser the token passes through.
- Extra characters from copying: if Bearer, quotes or line breaks come along, the server cannot read the token. The tool removes these characters before reading and tells you what it removed.
Limits and cautions
- For a JWE (five parts), the tool reads only the header and does not decrypt. It also does not read nested JWTs, tokens with b64 set to false (RFC 7797) or JWS in JSON serialization.
- Signature checking uses the browser's Web Crypto. ES256K and Ed448 cannot be checked, and on older browsers that do not support Ed25519, the tool says it cannot check.
- It does not fetch keys from a jku or x5u address or from a provider's jwks_uri. You have to paste the public key yourself, a choice made so that the token is never sent outside.
- The time verdict is based on this device's clock. If the device clock is wrong, the time left and the verdict are wrong by the same amount.
- Even when all four checkpoints pass, only the server knows about permissions, logout and reuse. Avoid pasting production secrets and real user tokens where you can, and check with test keys and tokens.
Basis: the JWT, JWS, JWE, JWK and JWA rules come from RFC 7519, 7515, 7516, 7517 and 7518, the security recommendations from RFC 8725, the Ed25519 name from RFC 9864, ID token claims from OpenID Connect Core 1.0, and the default tolerances from each library's source code, checked on October 22, 2026. The tokens and times in the tables were confirmed to give the same values in this tool and in Python PyJWT 2.13.
Sources
- 01IETF — RFC 7519: JSON Web Token (JWT)Section 2 NumericDate, section 4 unique claim names, 4.1 registered claims (iss, sub, aud, exp, nbf, iat, jti), 4.1.4 exp tolerance, 7.2 validation order
- 02IETF — RFC 7515: JSON Web Signature (JWS)Section 2 Base64url (trailing = omitted), section 4 unique header names, 4.1 registered headers (alg, kid, jku, x5u, typ, cty, crit)
- 03IETF — RFC 7518: JSON Web Algorithms (JWA)3.2 HMAC key length, 3.3 and 3.5 RSA 2048 bits and PSS salt length, 3.4 ECDSA signature R‖S
- 04IETF — RFC 8725: JSON Web Token Best Current Practices2.1 none and algorithm swapping, 2.2 weak symmetric keys, 3.1 allowed algorithms, 3.8 key belongs to the iss issuer, 3.9 aud checks, 3.10 kid, jku and x5u, 3.11 typ
- 05IETF — RFC 7516: JSON Web Encryption (JWE)7.1 Compact serialization with five parts
- 06IETF — RFC 7517: JSON Web Key (JWK)JWK and JWK Set (keys array), kid, use and alg parameters
- 07IETF — RFC 9864: Fully-Specified Algorithms for JOSE and COSEOctober 2025, registers Ed25519 and Ed448, marks EdDSA as Deprecated
- 08OpenID Foundation — OpenID Connect Core 1.0Section 2 ID token (auth_time, nonce, acr, amr, azp), 5.1 standard claims (name, email, email_verified and others)
- 09IETF — RFC 9068: JWT Profile for OAuth 2.0 Access Tokenstyp at+jwt, client_id and scope claims
- 10PyJWT — jwt/api_jwt.pyDefault of leeway in decode is 0
- 11auth0/node-jsonwebtoken — verify.jsIf clockTolerance is absent it is 0; expired when now ≥ exp + tolerance
- 12Spring Security — JwtTimestampValidatorDEFAULT_MAX_CLOCK_SKEW of 60 seconds
- 13Microsoft.IdentityModel.Tokens — TokenValidationParametersDefaultClockSkew of 300 seconds (5 minutes)
- 14MDN — SubtleCrypto.verify()The Web Crypto API that checks HMAC, RSASSA-PKCS1-v1_5, RSA-PSS, ECDSA and Ed25519 signatures in the browser
Frequently asked questions
Can I tell whether a JWT is valid just by decoding it?
No. The header and payload are Base64url, so anyone can decode them, and seeing the content only means the format is right. To know whether it is valid you must also check that the signature matches, that exp has not passed, and that iss and aud are the expected values. The four checkpoints in the tool above follow exactly this order.
Is a JWT payload encrypted?
A signed JWT (two dots, three parts) is not encrypted. The signature only guarantees the content was not changed and does not hide it. Do not put values others must not see, such as an email or phone number, in the payload, or use an encrypted JWE instead.
Why do I get a 401 when exp has not passed yet?
The token is rejected when the signing key was rotated and no public key matches the kid, when aud differs from the API's name, or when iss differs from the server configuration by even one character. If the server clock is fast, a token close to expiry may be judged expired early. If the server revoked the token separately, for example after a logout, you cannot tell from the token alone.
I entered the HS256 secret but get a signature mismatch.
First check whether the secret in the server configuration is plain text or a Base64 value, and switch between 'Plain text' and 'Base64 value'. The tool removes a line break added by copying and checks again. If it still fails, the key belongs to another environment (development or production), or the header or payload was changed after signing.
Where do I get the public key to verify an RS256 token?
For an OpenID Connect provider, the list of public keys (JWKS) is at the jwks_uri in the document at the issuer address plus /.well-known/openid-configuration. Paste that JSON as is and the tool picks the key with the same kid as the token header and checks with it. You can also enter a PEM public key or an X.509 certificate.
Is it safe to paste a token here?
The token and key are processed only in this browser and are never sent or stored. Still, a production access token is a working key until it expires, so be careful when you share or capture your screen, and check with a test token where you can.
What unit is the exp number in?
Seconds counted from 00:00 UTC on January 1, 1970. 1704067200 is 00:00 UTC on January 1, 2024, which is 7 p.m. on December 31, 2023 in New York. A 13-digit value was wrongly written in milliseconds, and read as seconds it points tens of thousands of years ahead, so divide it by 1000.
What is a token with four dots (five parts)?
It is an encrypted JWE. Only the first part, the header, can be read, and the rest must be decrypted with the recipient's key before the content is visible. This tool shows only the alg and enc of the JWE header and the byte count of each part.