What is JWT and How to Decode It Safely
JSON Web Tokens show up everywhere in modern web development — authentication headers, OAuth flows, API keys, session management. If you have worked with any API that requires a Bearer token, you have already used a JWT. But most developers treat them as opaque blobs and rarely look inside. Understanding the structure of a JWT, what each part means, and how to decode one without making security mistakes takes about fifteen minutes. This guide covers all of it.
The Anatomy of a JWT
Every JWT is a string of three Base64URL-encoded segments separated by dots:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImlzcyI6Imh0dHBzOi8vYXV0aC5leGFtcGxlLmNvbSIsImF1ZCI6Imh0dHBzOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoxNzQ3MDAwMDAwLCJpYXQiOjE3NDY5OTY0MDAsInNjb3BlIjoicmVhZDp1c2VycyJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c The format is header.payload.signature. Each segment encodes different information.
The Header
The first segment is the header. Decoded, the example above becomes:
{
"alg": "RS256",
"typ": "JWT"
} alg specifies the algorithm used to sign the token — RS256 means RSA with SHA-256. typ identifies the token type. These two fields are the minimum; some tokens also include kid (key ID) to tell the verifier which public key to use when multiple keys are in rotation.
The Payload
The second segment is the payload — the actual data. Decoded:
{
"sub": "user_123",
"iss": "https://auth.example.com",
"aud": "https://api.example.com",
"exp": 1747000000,
"iat": 1746996400,
"scope": "read:users"
} The payload contains claims: statements about the entity the token represents. Some claims are standardized (called registered claims); others are application-specific (called private claims). The payload is Base64URL-encoded, not encrypted. Anyone who gets the token string can read every claim inside it.
The Signature
The third segment is the signature. It is produced by taking the encoded header and payload, concatenating them with a dot, then signing the result with the secret key or private key specified in alg:
signature = sign(
base64url(header) + "." + base64url(payload),
secretOrPrivateKey
) The signature lets any party with the corresponding public key (or secret key, for symmetric algorithms) verify that the token was issued by a trusted source and that neither the header nor the payload was modified after issuance. A single changed character in the payload invalidates the signature entirely.
How to Decode a JWT
Decoding is different from verifying. Decoding simply reads the data inside a token. Verifying confirms the token is authentic and unexpired. You should always verify in production code; decoding alone is useful for debugging.
Step 1: Split the token
Split the JWT string on the . character. You will get three segments. If you get fewer than three, the token is malformed.
Step 2: Decode header and payload
Base64URL decode each of the first two segments. Base64URL is like standard Base64 but uses - instead of + and _ instead of /, with no padding characters. Once decoded, you can parse each segment as JSON.
Step 3: Parse and inspect
Read the claims. Check exp (expiration) against the current Unix timestamp. Look at iss and aud to confirm the token is intended for your application. Do not trust any claim until you have verified the signature.
For quick inspection during development, use the JWT Decoder. It runs entirely in your browser — your token never leaves your device. You can also decode base64 segments manually with the Base64 encoder/decoder if you want to see raw bytes. For verifying signatures in test scripts, the HMAC generator lets you reproduce HS256 signatures without writing code.
Step 4: Verify the signature in code
In production, always verify the signature using a library built for your language. Do not implement signature verification by hand. Popular options include jose or jsonwebtoken for Node.js, PyJWT for Python, golang-jwt/jwt for Go, and nimbus-jose-jwt for Java.
Always pass the algorithm explicitly. Never let the library infer the algorithm from the token header.
Standard Claims (Registered Claims)
RFC 7519 defines a set of registered claim names. These are not required, but when present they must follow the standard semantics.
iss — Issuer
The entity that created and signed the token. Usually a URL identifying the authorization server (for example, https://auth.example.com). Verifiers should check that iss matches an expected value and reject tokens from unknown issuers.
sub — Subject
The principal the token represents — typically a user ID, service account name, or device identifier. The value must be unique within the issuer's context. Your application uses sub to identify which user the request belongs to.
aud — Audience
The intended recipient(s) of the token. If your API's identifier is https://api.example.com, tokens issued for other services should be rejected. Failing to validate aud allows attackers to reuse a token obtained from one service at a different service.
exp — Expiration Time
A Unix timestamp after which the token must not be accepted. Always validate exp. A token without an expiration effectively lives forever — if it is stolen, the attacker has indefinite access.
iat — Issued At
The Unix timestamp when the token was created. Useful for detecting tokens that are technically unexpired but suspiciously old. You can use iat to enforce a maximum token age independent of exp.
nbf — Not Before
The Unix timestamp before which the token must not be accepted. Less common than exp, but useful when issuing tokens in advance — for example, a scheduled task that should not start until a specific time.
jti — JWT ID
A unique identifier for the token. Allows issuers to prevent token replay attacks by storing used jti values and rejecting tokens with duplicate IDs. Necessary when you need one-time-use tokens, such as password reset links.
Security Pitfalls
JWT implementations have a history of subtle vulnerabilities. These are the most important to understand before shipping code that accepts JWTs.
The "alg: none" Attack
Some JWT libraries from the early days of the standard would accept a token that specified "alg": "none" in its header. An attacker could take any valid token, replace the algorithm with none, strip the signature, and the library would accept it as fully valid — no signature required.
The fix: always specify the expected algorithm explicitly when calling your verification function. Treat any token claiming alg: none as invalid. Most modern libraries have addressed this, but it is worth confirming your library defaults before going to production.
Algorithm Confusion (RS256 to HS256 Downgrade)
Some libraries that supported both algorithms would use the alg field in the token header to decide how to verify. An attacker could change the algorithm to HS256, then sign the token using the server's public key as the HMAC secret. The server, seeing HS256, would verify against the public key and accept the forged token.
The fix is the same: lock the algorithm in your verification code. Never let the token header influence which algorithm is used for verification.
Accepting Expired Tokens
Not validating exp is a common oversight — sometimes introduced when developers add a grace period that grows without bound, or when the validation code path is bypassed in a code branch. Treat an expired token the same as a missing token: reject it with a 401 and require the client to re-authenticate or use a refresh token.
Sensitive Data in the Payload
The payload is Base64URL-encoded, not encrypted. Anyone who intercepts the token can decode it. Do not put passwords, credit card numbers, social security numbers, or other sensitive data in the payload. If you need to transmit sensitive claims securely, use a JWE (JSON Web Encryption) token, which encrypts the payload. JWTs signed with JWS (the common case) only guarantee authenticity, not confidentiality.
Missing Audience Validation
Skipping aud validation means a token issued for Service A can be replayed at Service B — as long as both services share the same signing key or trust the same issuer. In multi-service architectures, always validate that the token's audience matches your service's identifier.
Best Practices for 2026
Choose RS256 for Most Applications
RS256 uses a private key to sign and a public key to verify. Only the authorization server holds the private key. Any service can verify tokens using the public key, which can be published openly (often via a JWKS endpoint). If any individual service is compromised, attackers gain the ability to verify tokens — but not to forge new ones.
HS256 uses a single shared secret for both signing and verification. Any service that can verify tokens can also create them. In a microservice setup, sharing the secret across many services increases the blast radius of a breach.
Keep Access Tokens Short-Lived
There is no built-in revocation mechanism for JWTs — once issued, a token is valid until it expires (unless you implement a blocklist, which reintroduces server-side state). Short expiration times (15 minutes to 1 hour) limit the damage window if a token is stolen. Pair access tokens with long-lived refresh tokens stored in secure HttpOnly cookies, not in localStorage.
Use Refresh Token Rotation
When a client uses a refresh token to get a new access token, issue a new refresh token and invalidate the old one. This way, a stolen refresh token is detected the next time the legitimate client tries to use the original: the server sees a reused refresh token and can revoke the entire session. This pattern is described in RFC 6819 and is widely supported by modern authorization servers.
Publish a JWKS Endpoint
A JSON Web Key Set (JWKS) endpoint (/.well-known/jwks.json by convention) publishes your signing public keys in a standard format. Other services can fetch the JWKS and verify tokens without being given keys out-of-band. This also makes key rotation straightforward: add the new key to the JWKS, start signing with it, then remove the old key once all existing tokens have expired.
Rotate Keys Periodically
Even if your private key is never compromised, rotating keys periodically limits the window during which a stolen key could be used to forge tokens. Use kid in your JWT headers to reference the signing key, so verifiers can select the right public key from your JWKS without breaking existing tokens during a rotation.
Validate Every Claim
Check iss, aud, exp, and nbf on every request. Do not skip any of these because they seem redundant in your setup — the conditions that make them seem unnecessary today are exactly the conditions that change during an incident.
JWT vs. Session Cookies
JWTs and server-side sessions solve the same problem — persisting authentication state across stateless HTTP — but with different trade-offs.
JWTs are self-contained. The server does not need to query a database to validate a request. This makes them attractive for distributed systems and APIs consumed by mobile or third-party clients. The downside is that revocation requires a blocklist (which reintroduces server-side state) or tolerating the remaining lifetime of a compromised token.
Session cookies are server-validated. The server stores session state and can revoke access instantly by deleting the session. Cookies with the HttpOnly and Secure flags are protected from JavaScript access and network interception. The downside is that every request requires a database lookup, which can become a bottleneck at scale.
For traditional web applications with server-rendered pages and a single backend, session cookies remain a solid and simpler choice. For APIs consumed by multiple clients, microservice architectures, and mobile apps, JWTs with short expiration and refresh token rotation are the more practical option.
Decode Your Token
The fastest way to inspect a JWT is to paste it into the JWT Decoder. It splits the token, decodes the header and payload, and renders the claims in a readable format — all without sending your token anywhere. If you need to encode or decode raw Base64URL strings manually, the Base64 encoder/decoder handles both standard and URL-safe variants. For verifying HMAC signatures in test scripts, the HMAC generator lets you reproduce a HS256 signature and compare it against what your token contains.
For the full specification, see RFC 7519 (JWT) and the interactive examples at jwt.io.