Skip to main content

← Back to blog

What Is a JWT? How JSON Web Tokens Work (and How to Read One)

If you’ve built or debugged a login system, you’ve met the JWT — a compact token that travels between client and server to prove who someone is. Here’s how it works and how to read one safely.

Decode one yourself, no key required. Pasting the standard sample token from the JWT docs into our JWT decoder gives back its two readable halves — header { "alg": "HS256", "typ": "JWT" } and payload { "sub": "1234567890", "name": "John Doe", "iat": 1516239022 } — with no secret supplied. That is the single most important thing to understand about JWTs: the claims travel in the clear. The signature proves the token was issued by someone holding the key and has not been altered since, which is a completely different guarantee from confidentiality. Our decoder runs in your browser, so you can safely inspect a real token without it leaving your machine.

The three parts

A JWT is three Base64URL strings joined by dots: header.payload.signature.

  • Header — metadata, mainly the signing algorithm, e.g. {"alg":"HS256","typ":"JWT"}.
  • Payload — the claims, a JSON object of data about the user and token.
  • Signature — a cryptographic signature over the header and payload, created with a secret (or private key), used to detect tampering.

You can split and read the first two parts with a JWT decoder.

Common claims

The payload holds claims — some standardized:

  • sub — subject (usually the user ID).
  • iss — issuer (who created the token).
  • iat — issued-at time (a Unix timestamp).
  • exp — expiry time (a Unix timestamp). After this, the token should be rejected.
  • aud — intended audience.

Plus any custom claims your app adds (roles, email, etc.).

How JWTs are used for auth

In a typical stateless flow: you log in, the server returns a signed JWT, and your client sends it on every request (usually in an Authorization: Bearer … header). The server verifies the signature and checks exp — if valid, it trusts the claims without a database lookup. That statelessness is the main appeal.

Signed, not (usually) encrypted

Most JWTs are signed (JWS), not encrypted. Signing proves integrity and authenticity, but the payload is only Base64URL-encoded — anyone can read it. So:

  • Never put secrets in the payload (passwords, API keys, private data).
  • Encryption is a separate thing (JWE) and much less common.

This is the point at which most JWT confusion resolves, and it resolves as output. We pasted the token from the JWT specification into our own JWT decoder and supplied no key of any kind. It returned the header

{
"alg": "HS256",
"typ": "JWT"
}

and the payload

{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}

Nothing was decrypted, because nothing was encrypted. The two segments are Base64url text, and the signature protects them from modification, not from reading. Anyone holding the token can read every claim in it, which is why a JWT must never carry anything you would not put in a URL.

Security rules that matter

  • Always verify the signature server-side with your secret/public key. Decoding (like our tool does) is not verification.
  • Check exp. An expired token must be rejected.
  • HS256 vs. RS256: HS256 uses one shared secret; RS256 uses a private key to sign and a public key to verify — better when multiple services need to verify but not issue.
  • Never paste a real secret key into an online tool. Decoding a token to inspect claims is fine; verifying requires the key and belongs in your own code.

Reading a token

To debug an auth issue — is the token expired? does it have the right role? — decode it and inspect the payload. The free JWT decoder shows the header and payload and converts exp to a readable time, all locally in your browser so the token is never uploaded.

That also settles the practical question of what to log. { "sub": "1234567890", "name": "John Doe", "iat": 1516239022 } is safe to print — it is already readable to anyone who has the token. The third segment is not: it is the only part that proves the first two were not edited, and pasting a full token into a bug report hands over a working credential until it expires.

A decoded example

Take this widely used sample token:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Decoding the first two parts gives:

// header
{ "alg": "HS256", "typ": "JWT" }
// payload
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }

The third part is the signature — you can’t “read” it; it only proves the first two weren’t changed.

HS256 vs. RS256 at a glance

HS256RS256
FamilyHMAC-SHA256 (symmetric)RSA-SHA256 (asymmetric)
Keysone shared secretprivate key signs, public key verifies
Who can mint a valid tokenanyone holding the secretonly the private-key holder
Best whenone service issues and verifiesmany services verify, one issues

Working with exp and iat

exp and iat are Unix timestamps — seconds since 1970-01-01 UTC, not milliseconds. The 1516239022 above is 2018-01-18. Convert them with the timestamp converter, or read the Unix timestamp guide if the numbers look unfamiliar.

Two more security gotchas

  • Reject alg: none and always pin the algorithm you expect — otherwise an attacker can strip the signature or downgrade RS256 to HS256.
  • JWTs are hard to revoke before exp. Keep lifetimes short and use refresh tokens for anything sensitive.

Remember: our decoder only decodes — it never verifies. Treat a decoded-but-unverified payload as untrusted until your server checks the signature.