What Is a JWT? How to Decode a JSON Web Token (and What Never to Put in One)
Anyone can decode a JWT, because the header and payload are just base64url-encoded JSON. What matters is what you put inside it, and whether your server verifies the signature and claims before trusting it.
The short answer
A JWT (JSON Web Token) is a compact, URL-safe string that carries a set of claims as JSON, defined in RFC 7519 (the spec suggests pronouncing it "jot"). The common, signed form has three parts joined by dots: header.payload.signature. To decode one, split it on the dots and base64url-decode the first two parts — each turns into plain JSON. You don't need a key for that, because base64url is an encoding, not encryption. Decoding tells you what a token says; only verifying the signature with the right key tells you whether to believe it.
Anatomy of a real token
Here is a genuine HS256 token, signed with a random 256-bit secret we generated for this article and haven't published. Line breaks after the dots are for display only:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ1c2VyXzg0MTMiLCJuYW1lIjoiSmFuZSBEb2UiLCJpYXQiOjE3ODk1NDkyMDAsImV4cCI6MTc4OTU1MDEwMH0.
jTzXUkptGVt4F1Bwp8MNRZgkI3EAnwwYbXtPoT_RqdABase64url-decoding the first two segments gives:
// Header
{
"alg": "HS256",
"typ": "JWT"
}
// Payload
{
"sub": "user_8413",
"name": "Jane Doe",
"iat": 1789549200,
"exp": 1789550100
}The header says how the token was signed (alg) and what it is (typ). The payload holds the claims: sub, iat and exp are registered claims from RFC 7519, while name is a custom one. The signature is an HMAC-SHA256 over the first two segments exactly as they appear in the token:
signature = base64url(
HMAC-SHA256(secret, base64url(header) + "." + base64url(payload))
)Change one character of the payload and the signature no longer matches — but only someone holding the secret can check that.
Base64url is encoding, not encryption
Base64url, as defined for JWTs in RFC 7515, is ordinary Base64 using the URL-safe alphabet — - and _ instead of + and / — with the trailing = padding dropped. It is fully reversible by anyone. It's also why most tokens start with eyJ: that is how {" followed by a letter comes out in Base64. So everything in the payload is readable by whoever holds the token — the user, browser extensions, proxies, and any log file it lands in. RFC 7519's privacy section says it plainly: omitting privacy-sensitive information from a JWT is the simplest way to minimise privacy issues.
Can you decode a JWT without the secret key?
Yes — decoding needs nothing but the token. What you can't do without the key is verify it. With HS256, the same shared secret signs and verifies. With RS256 or ES256, the issuer signs with a private key and anyone with the matching public key can verify. RFC 7519 is explicit that a token's contents can't be relied on for a trust decision unless they're cryptographically secured, so a decoder is a debugging aid, not an authentication check. Anyone can build a token claiming "admin": true; the signature check on your server is what rejects it.
Paste a token into CheckSEO's JWT Decoder to see its header and payload, with exp, iat and nbf turned into readable UTC dates. Decoding runs entirely in your browser, so the token isn't uploaded. It doesn't verify signatures.
Decode a JWT in your browserReading the registered claims
RFC 7519's registered claims are all optional. These six matter most:
- iss (issuer) — who created the token.
- sub (subject) — who the token is about, unique within that issuer.
- aud (audience) — who the token is for. If it's present and the recipient isn't listed, the token must be rejected.
- exp (expiration time) — on or after this moment, the token must not be accepted.
- nbf (not before) — before this moment, the token must not be accepted.
- iat (issued at) — when the token was created, useful for working out its age.
The three time claims use a NumericDate: the number of seconds since 1970-01-01T00:00:00Z UTC, ignoring leap seconds. Seconds, not milliseconds — JavaScript dates count milliseconds, so multiply by 1,000 before converting:
node -e 'console.log(new Date(1789550100 * 1000).toISOString())'
# 2026-09-16T09:15:00.000ZSo the example token was issued at 09:00 UTC on 16 September 2026 and expired 15 minutes later. RFC 7519 lets verifiers allow a small leeway for clock skew, "usually no more than a few minutes".
How to decode a JWT on the command line
Node's Buffer understands base64url directly, so this prints the header and payload of whatever is in $TOKEN:
node -e '
const [h, p] = process.argv[1].split(".");
for (const s of [h, p])
console.log(JSON.parse(Buffer.from(s, "base64url")));
' "$TOKEN"With jq (we tested 1.7.1), swap the URL-safe characters back first, because its @base64d filter expects the standard alphabet:
printf '%s' "$TOKEN" | jq -R 'split(".")[0:2]
| map(gsub("-"; "+") | gsub("_"; "/") | @base64d | fromjson)'Piping a segment straight into base64 -d is unreliable because the padding is gone: on macOS it silently dropped the last two characters of our example payload, with no error. For a one-off check, CheckSEO's Base64 decoder accepts either alphabet without padding. Tokens typed into commands end up in shell history, so use test or expired ones.
JWS vs JWE
A JWT is carried as either a JWS (JSON Web Signature, RFC 7515) or a JWE (JSON Web Encryption, RFC 7516). A JWS is signed: three segments, readable payload — what almost everyone means by "JWT". A JWE is encrypted and has five segments: protected header, encrypted key, initialization vector, ciphertext and authentication tag. If claims genuinely need to be confidential, use a JWE — or better, keep them out of the token.
What never to put in a JWT, and 6 other mistakes
Secrets or personal data in the payload
Passwords, API keys, card numbers, or personal data you wouldn't print in a log. Anyone holding the token can decode it. Put an opaque identifier in sub and look the rest up on the server.
Accepting alg: none
An unsecured JWT uses "alg": "none" and an empty signature. Some libraries once treated these as validly signed, letting anyone forge tokens. RFC 8725 (JWT Best Current Practices) says libraries shouldn't accept none unless explicitly asked; the OWASP JWT Cheat Sheet says to make sure your parser rejects it.
Algorithm confusion (RS256 key used as an HS256 secret)
If a server expects RS256 but lets the token's own alg header pick the algorithm, an attacker can sign an HS256 token using the server's public key as the HMAC secret — and a vulnerable library verifies it with that same public key. Hard-code the algorithms you accept. RFC 8725 requires each key to be used with exactly one algorithm.
Weak HMAC secrets
An HS256 token signed with a human-memorable password can be brute-forced offline by anyone who has one token. RFC 8725 says such passwords must not be used directly as the key. OWASP recommends a secret from a cryptographically secure generator, at least as long as the hash output — 256 bits for HS256.
Not validating exp, aud and iss
A valid signature only proves who signed the token. Also reject it if it's expired or not yet valid, if iss isn't the issuer you trust, or if aud doesn't name your service. RFC 8725 requires audience checks when one issuer serves several applications, so a token meant for one API can't be replayed against another.
Long-lived tokens with no way to revoke them
A JWT stays valid until exp, even after logout or a password change. OWASP notes that revoking stateless tokens needs a deny list — typically keyed on jti and iss — which means the server is keeping state again. Short-lived access tokens limit the damage from a stolen one.
Storing tokens where XSS can read them
OWASP's HTML5 Security Cheat Sheet advises against keeping session identifiers in localStorage, which any script on the page can read, and notes that an HttpOnly cookie mitigates that. Cookies have their own catch: they're sent automatically, so you need CSRF protection, and OWASP treats SameSite as defence in depth rather than a full fix. Neither option makes XSS harmless, since injected script can still send requests as the user.
So token storage is also an XSS question: preventing injected script — including with a Content-Security-Policy, covered in our security headers checklist — matters as much as where the token lives.
Decode your token now
CheckSEO's JWT Decoder decodes the header and payload locally in your browser — the token is never uploaded. It strips a pasted Bearer prefix, shows iss, sub, aud and the exp/iat/nbf times as UTC dates with relative times, and flags expired or not-yet-valid tokens. It warns about alg: none, a missing exp and millisecond timestamps, and recognises five-segment JWEs. It doesn't verify signatures: that needs the issuer's key, and belongs on your server.
Frequently asked questions
Can I decode a JWT without the secret key?
Yes. The header and payload of a signed JWT are base64url-encoded JSON, not encrypted, so anyone can decode them without a key. The secret or public key is only needed to verify the signature, which is what proves the token is genuine and hasn't been altered.
Is it safe to decode a JWT online?
Only with a decoder that runs in your browser and doesn't upload the token, as CheckSEO's does. A live token is still a credential that anyone holding it can use until it expires, so prefer test or expired tokens, and never paste a signing secret into a web page.
How do I check if a JWT is valid?
Verify it on your server with a maintained JWT library. Check the signature using the algorithm and key you expect, then confirm exp and nbf against the current time, and that iss and aud match your issuer and service. Decoding a token on its own never proves it is valid.
Can a JWT never expire?
Yes. The exp claim is optional in RFC 7519, so a token without it has no built-in expiry and stays usable until the signing key changes or the server tracks revoked tokens. Issue short-lived tokens with an exp claim, and configure your verifier to reject tokens that lack one.
What is the difference between a JWT and OAuth?
OAuth 2.0 is an authorization framework that defines how apps obtain access tokens; a JWT is a token format. OAuth doesn't require JWTs, since access tokens can be opaque strings, but many providers issue JWT access tokens, and OpenID Connect ID tokens are always JWTs.
Check your own site with the JWT Decoder.
Open JWT DecoderMore from the blog
What Are UTM Parameters? A Practical Guide With Naming Conventions
What are UTM parameters? The five tags, what GA4 does with each, a naming convention that stops split reports, and the internal-link mistake that breaks attribution.
Regex Cheat Sheet: Every Pattern You Actually Use, With Live Examples
Regex cheat sheet you can scan: anchors, classes, quantifiers, groups, lookarounds and flags — plus the 8 patterns people actually paste, ready to test.