alg: none
A token asking to be trusted with no signature at all
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
Paste a JWT to read its header and payload, with every timestamp turned into a date you can actually read. It runs in your tab, so the token is never sent anywhere, which matters more here than on most pages, because a JWT usually is the credential.
Or drop a file anywhere on this panel. Nothing is uploaded: the analysis runs in this tab.
The answer appears here
Paste on the left and press Decode. Nothing leaves this tab.
Nothing else to flag.
No formatting problems, and nothing the rules object to. Worth remembering what that covers: this reads the file you pasted, not the account or cluster it will be applied to.
No finding matches that filter.
Real input you can load into the tool above. Each one shows a different thing going wrong, because that is what the tool is for.
A token asking to be trusted with no signature at all
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
The exp claim read as a date, which is the check people do by eye and get wrong
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjoxNTE2MjM5MDIyfQ.abc
These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.
Decoding is not verification. Anyone can craft a token with any claims; only checking the signature against the expected key makes them meaningful.
Instead:Verify the signature server-side, and reject alg: none outright.
A JWT is base64, not encrypted. Anything in it is readable by anyone holding the token, including browser extensions and proxy logs.
Instead:Carry an opaque subject id and look the rest up server-side.
A token stays valid until it expires. There is no way to revoke one without server-side state.
Instead:Keep expiry short and pair with a refresh token you can revoke.
Decoding a JWT is twenty lines of code. The useful part is what the claims add up to once they are readable.
exp, nbf and iat are seconds since the epoch, which nobody can read at a glance. Each one is shown as an ISO date and as a plain phrase: expired two hours ago, valid for another six minutes, so the answer you came for is the first thing on the page.
The oldest JWT attack is to take a real token, edit the payload, set the algorithm to none and send it back. Any verifier that trusts the header's own algorithm accepts it. If a token arrives with alg: none, that is the loudest finding here.
A JWT payload is base64, not encryption. Anyone holding the token reads every claim in it, and so does every proxy and access log on the way. Claims named like credentials are called out as critical, because if one is in there it should be treated as leaked.
Decoding proves what a token says, never that it is genuine. Verifying the signature means fetching the issuer's public keys, which is a network request this page does not make, so the report always ends by saying the signature was not verified.
A JSON Web Token is three chunks of base64url joined by dots. It carries a set of claims about who someone is, and a signature that lets a server check the claims have not been altered, without calling back to whoever issued them. That last part is the whole point, and it is also where every JWT design problem comes from.
The header names the signing algorithm. The payload holds the claims. The signature is computed over the first two parts together with a secret or a private key. Only the signature is genuinely opaque: the other two are ordinary JSON that anyone can read, which is what the tool above does.
eyJhbGciOiJIUzI1NiJ9 . eyJzdWIiOiI0MiJ9 . 3rXNPl7ZgqLm...
|__________________| |______________| |____________|
header payload signature
header {"alg":"HS256","typ":"JWT"}
payload {"sub":"42","exp":1893456000} This is the thing to internalise. base64url is not a cipher: it has no key and reversing it is a single function call. Every claim in a token is readable by the browser it was sent to, by every proxy it passes through, and by anything that logged the request. Putting a password, an API key or a national ID number in a payload is publishing it. If a token has ever contained one, treat that value as leaked and rotate it.
A signature answers one question: has this token been altered since it was issued? It says nothing about whether the contents are private, because they are not. Verifying means recomputing the signature with the issuer's key and comparing, which is why verification belongs on your server, and why this page decodes but deliberately does not verify.
signature = HMAC-SHA256(
base64url(header) + "." + base64url(payload),
secret
)
change one character of the payload and the signature no longer matches Seven claim names are registered in RFC 7519 and the rest are yours. Three of them are timestamps, given as NumericDate: seconds since the Unix epoch, as a JSON number. Emitting those in milliseconds is a common bug and pushes the expiry into the year 55000, which reads as 'never expires' to anything checking it.
iss issuer who minted it
sub subject who it is about, usually the user id
aud audience which service is allowed to accept it
exp expires at seconds since 1970, NOT milliseconds
nbf not valid before seconds since 1970
iat issued at seconds since 1970
jti token id unique, for replay detection or a denylist The reason a JWT scales is that the verifier never has to ask anyone whether it is still valid: the signature and the expiry are enough. That is also why logging someone out does not invalidate their token. Once issued, it is accepted until it expires, so the gap between iat and exp is the real blast radius of a leak. Short-lived access tokens with a refresh token behind them is the standard answer; a denylist keyed on jti works but gives back the statelessness you chose a JWT for.
The header names its own algorithm, which means an attacker gets to suggest how their token should be checked. Set alg to none, strip the signature, and a naive verifier that trusts the header will accept an edited payload. A related attack takes an RS256 public key, which is public, and submits a token signed HS256 using that key as the HMAC secret. Both are fixed the same way: decide server-side which algorithm you accept and refuse everything else.
# what a verifier should do
verify(token, key, algorithms=["RS256"]) # pinned
# what it should never do
verify(token, key) # trusts header.alg