Skip to content

JWTs, explained

JWT stands for JSON Web Token. You'll usually see one as a long string with three dot-separated parts, passed around after login or attached to an API request. The format is common because it is compact, text-friendly, and easy for different systems to share.

That convenience leads people into a bad habit: they treat JWTs as mysterious or automatically trustworthy because the string looks technical. It is not mysterious. A JWT is just data plus a signature, packaged in a standard way. If you know what each part does, most of the confusion drops away fast.

This guide walks through the structure, shows what base64url is doing inside the token, explains why decoding is not the same thing as verification, and calls out the mistakes that show up over and over in production systems.

When you'd actually use a JWT

JWTs show up whenever one system needs to hand another system a compact set of claims about a user, a session, or a request.

After you sign in to a web app, the server might issue a JWT that says who you are, when the token expires, and which audience it was meant for. An API gateway might check that token before forwarding the request to an internal service. A single sign-on flow might pass a token between an identity provider and an application that trusts it. Mobile apps use them. Browser apps use them. Machine-to-machine APIs use them too.

The attraction is simple: a JWT can carry enough context to avoid a database lookup on every request, and because the payload is signed, the receiving system can detect tampering as long as it has the right key and checks the signature correctly.

Still, "signed" does not mean "always safe." A JWT is only as trustworthy as the code that verifies it, the key management behind it, and the rules around claims like expiration and audience. That's why understanding the format matters.

How a JWT is structured

A standard JWT has three parts:

3. signature

They are joined with dots:

xxxxx.yyyyy.zzzzz

The header and payload are JSON objects. Each one is encoded with base64url, which is a URL-safe variant of Base64. The signature is calculated from the encoded header, a dot, and the encoded payload.

Header

The header says what kind of token this is and which signing algorithm is in play. A small example:

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

alg matters because it tells the verifier how to validate the signature. typ is often JWT, though many systems do not rely on it for security decisions.

Payload

The payload contains claims. Some are registered, some are private to the app. A small example:

{
  "sub": "1234567890",
  "name": "Ada Lovelace",
  "role": "admin",
  "iat": 1717000000,
  "exp": 1717003600,
  "aud": "api.example.com"
}

Common registered claims include:

  • sub: subject, usually the user or principal ID
  • iat: issued at time
  • exp: expiration time
  • nbf: not before time
  • iss: issuer
  • aud: intended audience

The payload is readable by anyone who has the token. That point is worth slowing down for: a signed JWT is not encrypted. If you put a password, API secret, or private personal data in the payload, anyone who can copy the token can decode and read it.

Signature

The signature is what lets a verifier detect that the header or payload changed after the token was issued.

For an HMAC-signed token, the server computes a signature from:

base64url(header) + "." + base64url(payload)

Then it signs that string with a shared secret. If an attacker edits "role":"user" to "role":"admin" but does not have the secret, the signature will no longer match.

For asymmetric algorithms such as RS256 or ES256, the issuer signs with a private key and the receiver verifies with the matching public key.

base64url inside a JWT

JWTs do not use standard Base64. They use base64url.

The difference is small but important:

  • + becomes -
  • / becomes _
  • padding with = is often omitted

That change keeps the encoded parts safe inside URLs, cookies, and HTTP headers without extra escaping. If you paste a JWT into a normal Base64 decoder and it complains, missing padding or URL-safe characters are usually the reason.

If this part feels familiar, it should. JWTs borrow the same binary-to-text idea covered in Base64 encoding, explained. The goal is not secrecy. The goal is to package structured data in a form that text-oriented systems can move around safely.

A worked token example

Here is a tiny example header:

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

And a tiny example payload:

{
  "sub": "42",
  "name": "Sam",
  "admin": false,
  "exp": 1735689600
}

First, each JSON object is UTF-8 text. Then each one is base64url-encoded. You might end up with something shaped like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiI0MiIsIm5hbWUiOiJTYW0iLCJhZG1pbiI6ZmFsc2UsImV4cCI6MTczNTY4OTYwMH0

Join those with a dot, sign the result, and append the signature:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiJTYW0iLCJhZG1pbiI6ZmFsc2UsImV4cCI6MTczNTY4OTYwMH0.signature_here

If you decode only the first two sections, you learn what the token claims. You do not learn whether the claims are trustworthy. A forged token can decode perfectly. Decoding tells you the contents. Verification tells you whether the issuer really signed those contents and whether the claims still pass your policy checks.

That is the line many bugs cross.

Decoding versus verification

Decoding is mechanical. You split on dots, base64url-decode the first two parts, and parse the JSON.

Verification is a security check. It asks harder questions:

  • Does the signature match?
  • Is the algorithm one you expect?
  • Is the token expired?
  • Is the issuer one you trust?
  • Is the audience meant for this app?
  • Is the token not-before time in the past?

You need both pieces for different reasons. Decoding is useful for debugging, inspection, education, and understanding what a token is trying to say. Verification is what decides whether your app should accept it.

If a library or homemade helper gives you decoded claims and the code starts using them before verification happens, that is a real security bug, not a style issue.

Try it in your browser

Our JWT Decoder runs locally in your browser. When you paste in a token, the decoded header and payload stay on your device and are not uploaded to a server.

That makes it useful for inspecting development tokens, checking claim names, confirming timestamps, or seeing whether a token uses base64url padding the way you expect. It also keeps the mental model clean: the tool helps you decode what is there, but it does not turn an untrusted token into a trusted one.

Common mistakes

Treating decoding as proof. This is the classic mistake. The token opens fine in a decoder, the payload looks plausible, and someone assumes it must be valid. It might be fake, expired, signed with the wrong key, or meant for another audience.

Putting secrets in the payload. Anyone who has the token can read the payload. Never store passwords, API secrets, session secrets, or sensitive personal data there unless the token is separately encrypted and your design truly calls for it.

Ignoring claim checks. Signature verification by itself is not enough. If you never check exp, aud, or iss, you can still accept a token you should reject.

Accepting whatever algorithm the token asks for. The verifier should enforce the algorithm it expects. Do not let the token pick a weak or unexpected algorithm and quietly proceed.

Rolling your own JWT handling. Writing a parser is easy. Writing a secure verifier is not. Mature libraries exist for a reason.

Forgetting clocks drift. Expiration and not-before checks depend on time. In distributed systems, a few seconds of clock skew can matter, so many libraries allow a small configured leeway.

FAQ

Yes. You can usually decode the header and payload without any secret at all. The secret or public key is needed for verification, not for basic inspection.

Usually, no. A standard signed JWT is readable after decoding. If you need confidentiality, you are looking for encryption, not just signing.

Because JWTs use base64url, not standard Base64, and they often omit = padding. A decoder that expects the standard alphabet may need the string normalized first.

HS256 uses one shared secret for signing and verification. RS256 uses a private key to sign and a public key to verify. The right choice depends on who issues tokens and who needs to validate them.

That is an application security decision, not a JWT-format rule. The storage choice affects exposure to XSS, session handling, and logout behavior. The token format does not solve those risks for you.

Related guides