A JWT looks secret. It is not. The header and the payload are only Base64url text. Anyone who has the token can read the claims. The signature is the part that proves the token was issued by someone who holds the key, and that check does not happen by decoding it.
This is the distinction I keep having to make while debugging login. A readable exp or sub is useful. It is not proof the token is valid.
What the three parts are
A JWT is three pieces separated by dots:
header.payload.signature
The header names the algorithm, usually HS256 or RS256. The payload holds the claims: sub, name, role, iat, exp, and whatever else the issuer put there. The signature is the third part. A decoder shows it. It does not check it.
Base64url is not encryption. Changing - and _ back to + and /, padding with =, and running a Base64 decode gives you JSON. No secret is required for that step.
A token you can read by eye
The sample on the KeptTools JWT decoder is shaped like a real token, with a placeholder in the signature slot so you can see the three parts:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IktlcHRUb29scyBVc2VyIiwiaWF0IjoxNzA5ODAwMDAwLCJleHAiOjE3Mzk4MDAwMDAsInJvbGUiOiJkZXZlbG9wZXIifQ.signature-placeholder
The header decodes to:
{
"alg": "HS256",
"typ": "JWT"
}
The payload decodes to:
{
"sub": "1234567890",
"name": "KeptTools User",
"iat": 1709800000,
"exp": 1739800000,
"role": "developer"
}
iat and exp are Unix timestamps in seconds. 1709800000 is the issued-at time. 1739800000 is the expiry. Compare exp with the current time before you treat the session as active. An expired token still decodes.
What decoding does not tell you
A decoded payload does not mean:
- the signature matches the header and payload
- the token came from your server
- the token has not been edited
- the session is still valid
Signature checks belong on the server that holds the HMAC secret or the public key. For HS256, that secret must never be in frontend code. For RS256, the browser can hold a public key, but verification is still a separate step from reading the JSON.
If you only need to see why a request is failing, decoding is enough. If you are deciding whether to trust the token, it is not.
Do it without uploading the token
Paste the token into a page that decodes it locally, or do the same two Base64url decodes in your own console. Either way, a production access token or a refresh token should not be sent to a server you do not control.
I use the decoder at https://kepttools.com/jwt-decoder for this. The token stays in the tab. The page shows the header, the payload, and the signature string. It does not verify the signature, and it does not load analytics on that page.
Paste a token, or load the sample. Read alg in the header and exp in the payload. Then verify the signature in the app that issued the token.
Top comments (0)