What is a JSON Web Token?
A JSON Web Token (JWT) is a compact, URL-safe way to represent claims as a single string. The most common use is authentication: a server issues a JWT after the user logs in, and the client sends it back on every subsequent request as proof of identity. JWTs are popular because they're self-contained — the server can verify and read a token without consulting a database, which scales well for stateless APIs.
The format is defined in RFC 7519. A JWT is three base64url-encoded segments separated by dots: header.payload.signature. The header says how the signature was generated, the payload contains the actual claims, and the signature lets a server verify the token wasn't tampered with.
The three parts
Take this token (the one our sample button generates):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyLTEy...
The first segment eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 is the header. Decoded from base64url, that's the JSON {"alg":"HS256","typ":"JWT"} — meaning the token was signed with HMAC-SHA256 and is of type JWT.
The second segment is the payload — the actual claims about the user, expiration, scope, and so on. Like the header, it's just base64url-encoded JSON.
The third segment is the signature, computed over the header and payload using the secret (HS256) or private key (RS256, ES256). It exists so a server receiving the token can prove it was issued by something with access to the signing key, and that nobody has modified it in transit. Decoding a JWT does not require any key — only verifying it does.
Standard payload claims
The JWT spec defines seven "registered" claims that have a standard meaning. Most JWTs use a subset of these plus their own custom claims.
| Claim | Name | Meaning |
|---|---|---|
| iss | Issuer | Who issued the token (often a URL like https://auth.example.com) |
| sub | Subject | Who the token is about — usually a user ID |
| aud | Audience | Who the token is for — usually an API identifier |
| exp | Expiration | Unix timestamp after which the token is no longer valid |
| nbf | Not Before | Unix timestamp before which the token is not yet valid |
| iat | Issued At | Unix timestamp when the token was created |
| jti | JWT ID | Unique identifier for the token — useful for revocation lists |
The decoder above shows iat, exp, nbf, and auth_time as both raw integers and human-readable dates with relative time, because these are the claims you most often need to inspect when debugging.
Algorithm types
The alg field in the header tells the verifier which algorithm was used to produce the signature. The most common are:
| Algorithm | Type | Key model |
|---|---|---|
| HS256 | HMAC SHA-256 | Symmetric — same secret signs and verifies |
| HS384, HS512 | HMAC SHA-384/512 | Symmetric, larger output |
| RS256 | RSA SHA-256 | Asymmetric — private signs, public verifies |
| RS384, RS512 | RSA SHA-384/512 | Asymmetric, larger output |
| ES256 | ECDSA P-256 SHA-256 | Asymmetric, smaller signatures than RSA |
| ES384, ES512 | ECDSA P-384/521 | Asymmetric, varying curves |
| PS256 | RSA-PSS SHA-256 | Asymmetric, modern padding |
| EdDSA | Ed25519 | Asymmetric, fast, recommended for new systems |
| none | (no signature) | Insecure — see warning above |
Asymmetric algorithms (RS, ES, PS, EdDSA) are preferred for systems where the signer and verifier are different parties — the verifier only needs the public key, which can be safely distributed. Symmetric algorithms (HS) are faster but require both parties to share the same secret.
Decoding is not verifying
The single most important security distinction with JWTs: decoding the contents of a token does not prove anything about its authenticity. Anyone can write any claims they want into a JWT-shaped string and ship it to your server. A server that trusts a JWT based only on its decoded contents — without verifying the signature — has effectively no authentication.
This decoder, like every browser-based JWT decoder, exists for inspection and debugging only. It tells you what's in a token, not whether the token is genuine. For the genuine-or-not question, your server needs to verify the signature using the secret or public key associated with the issuer.
Common bug: a developer copies a token from a real production session into this decoder, sees a valid-looking payload with "sub": "admin", and assumes their auth code is working. But if the code accepting that token doesn't verify the signature, an attacker could send a different token with a forged "sub": "admin" claim and get the same access. The presence of valid-looking claims is necessary but not sufficient.
Common bugs this tool helps catch
- Wrong clock — your server thinks tokens are valid for an hour, but the displayed expiration is suspiciously far in the future. Often a bug where milliseconds are passed where seconds are expected (a 1000-second exp becomes 1,000,000 ms = ~16 minutes off).
- Time zone confusion — claims look right in your local zone but a downstream service is comparing against UTC. The relative-time hints above make this immediately visible.
- Missing exp — token has no expiration claim, meaning it's valid forever (until manually revoked). Almost always unintentional.
- alg mismatch — issuer and verifier disagree on the algorithm. Often surfaces as "valid signature but verification fails" in production logs.
- iat in the future — clock skew between the issuing service and the verifier, or a misconfigured time source. Tokens become temporarily unusable.
- Long-lived token — a token with exp 6+ months in the future, intended as a temporary workaround, that nobody ever revoked. Security review should flag these.
Programming language reference
Quick reference for decoding a JWT in 8 common languages, without using a library (the decoding itself is just base64url + JSON):
| Language | Decode header (or payload) of token jwt |
|---|---|
| Python 3.11+ | import base64, json; json.loads(base64.urlsafe_b64decode(jwt.split('.')[0] + '=' * (-len(jwt.split('.')[0]) % 4))) |
| Node.js 16+ | JSON.parse(Buffer.from(jwt.split('.')[0], 'base64url').toString()) |
| Browser JS | JSON.parse(atob(jwt.split('.')[0].replace(/-/g,'+').replace(/_/g,'/'))) |
| Java | new String(Base64.getUrlDecoder().decode(jwt.split("\\.")[0]), StandardCharsets.UTF_8) |
| Go | data, _ := base64.RawURLEncoding.DecodeString(strings.Split(jwt, ".")[0]) |
| Ruby | require 'base64'; require 'json'; JSON.parse(Base64.urlsafe_decode64(jwt.split('.')[0])) |
| PHP | json_decode(base64_decode(strtr(explode('.', $jwt)[0], '-_', '+/') . str_repeat('=', 3 - (3 + strlen(explode('.', $jwt)[0])) % 4))) |
| Rust | use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; URL_SAFE_NO_PAD.decode(jwt.split('.').next().unwrap()) |
For verification, always use a maintained library — never roll your own signature check. Common libraries: PyJWT (Python), jsonwebtoken (Node.js), jose4j or nimbus-jose-jwt (Java), github.com/golang-jwt/jwt (Go), ruby-jwt (Ruby), firebase/php-jwt (PHP), jsonwebtoken (Rust).
Related tools
If you're working with JWT timestamps, you might also need: