A JWT is three Base64-URL-encoded chunks separated by dots:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMiLCJuYW1lIjoiQWxpY2UifQ.K-EzFhRtkPm...
From left to right:
{"alg":"HS256","typ":"JWT"}.Each chunk is Base64-URL-encoded, so you can copy a JWT into a chat or URL without worrying about special characters.
Anyone with the token can read the payload. Try it: paste a JWT into any JWT decoder and you will see the JSON. This is by design - the payload is not secret, it is just signed.
The header and payload are not encrypted. Never put a password, an API key, or anything sensitive in a JWT payload. Put a user ID and a session reference if you need to.
The signature is what makes the token trustworthy. Without the server's key, an attacker cannot generate a token with a different payload that still passes verification.
| Claim | Meaning |
|---|---|
sub | Subject - usually the user ID. |
iss | Issuer - which server issued the token. |
aud | Audience - which service the token is intended for. |
exp | Expiry - Unix timestamp after which the token is invalid. |
iat | Issued-at timestamp. |
nbf | Not-before - earliest time the token is valid. |
jti | A unique ID for this token, useful for revocation. |
Beyond these standard claims, applications add their own (roles, scopes, tenant ID, etc.).
The argument:
In practice most production systems use a mix: short-lived JWTs (15-60 min) for fast API auth, plus a longer-lived refresh token stored as a session-like record server-side so you can revoke it.
Old JWT libraries had a famous bug: if you set alg: "none" in the header, some implementations would accept the token without verifying the signature. This let attackers forge any token they wanted.
Modern libraries refuse alg: none by default, and you should reject it explicitly when you accept JWTs. Always verify with the exact algorithm you expect.
Decode JWTs, Base64, URLs, and more on iPhone, iPad, and Mac. Plus JSON pretty-printing and regex testing. · iPhone, iPad & Mac