Skip to content
SupraIQ

Developer

What Is a JWT and How Do You Read One?

Learn how a JSON Web Token is built, how to decode a sample by hand, what the claims mean, and what a server must verify before trusting one.

By Vigneshwaran M · 2026-10-03 · 6 min read

Note: This article was written with AI assistance and may contain inaccuracies or outdated information. Please check important details against the official sources listed below.

A JSON Web Token (JWT) is a small, self-contained string that carries a set of claims about something, usually a user or a client application. The format is defined in RFC 7519. You will most often meet one as a login token: after you sign in, a server hands your app a JWT, and the app sends it back with later requests to say who you are. Because the token carries its own data, the receiving server can read it without looking anything up in a session database.

This article is educational. It explains how the format works and is not a security audit of your system. To follow along with a real token of your own, paste it into the JWT decoder.

The three parts of a token

A JWT is three Base64URL strings joined by dots:

header.payload.signature
  • Header: a JSON object that describes the token, mainly the signing algorithm (alg) and the token type (typ).
  • Payload: a JSON object holding the claims, which are the actual statements the token makes.
  • Signature: a value computed over the first two parts, so that tampering can be detected.

Base64URL is a variant of Base64 that swaps + and / for - and _ and usually drops the = padding, so the result is safe inside URLs and headers. If Base64 itself is new to you, the Base64 encoder and decoder lets you experiment.

A worked example you can check

Here is a made-up token. Every value is fabricated for this article, including the throwaway key used to sign it. The header is:

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

The payload is:

{"iss":"https://auth.example.test","sub":"user-1042","aud":"demo-api","iat":1767225600,"nbf":1767225600,"exp":1767229200,"jti":"a1b2c3"}

Encoding each one as Base64URL gives these two strings, which I generated and double-checked with a short Node.js script:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS50ZXN0Iiwic3ViIjoidXNlci0xMDQyIiwiYXVkIjoiZGVtby1hcGkiLCJpYXQiOjE3NjcyMjU2MDAsIm5iZiI6MTc2NzIyNTYwMCwiZXhwIjoxNzY3MjI5MjAwLCJqdGkiOiJhMWIyYzMifQ

The signature was produced by running HMAC with SHA-256 over the text first-string.second-string, using the placeholder key demo-secret-not-real, then encoding the result in Base64URL. The complete token is therefore:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS50ZXN0Iiwic3ViIjoidXNlci0xMDQyIiwiYXVkIjoiZGVtby1hcGkiLCJpYXQiOjE3NjcyMjU2MDAsIm5iZiI6MTc2NzIyNTYwMCwiZXhwIjoxNzY3MjI5MjAwLCJqdGkiOiJhMWIyYzMifQ.A7kfTNAk1eNEkZ2VtMFXJ-NNnQ_MqVPq1t48_3BGe24

Notice that eyJ opens both of the first two parts. That is simply what the Base64URL of an opening brace and quote looks like, which is why almost every JWT you see starts that way. The two time values differ by 3600, so this token is valid for exactly one hour, from 2026-01-01 00:00 UTC to 01:00 UTC.

What the registered claims mean

RFC 7519 reserves a handful of short claim names. All of them are optional in the format itself, though your application will normally require some.

| Claim | Meaning | | --- | --- | | iss | The issuer: who created and signed the token | | sub | The subject: who or what the token is about | | aud | The audience: which service the token is meant for | | exp | Expiry: the moment after which the token must be refused | | nbf | Not before: the moment before which the token must be refused | | iat | Issued at: when the token was created | | jti | A unique identifier for this one token |

Times are whole seconds since 1970-01-01 UTC, not milliseconds, which is a frequent source of off-by-1000 bugs. The jti value is useful if you want to track or revoke individual tokens, since it gives each one a name. You can add your own custom claims too, such as a role, but remember that anything you add is visible to whoever holds the token.

How signing works, conceptually

The signature lets a receiver detect any change to the header or payload. There are two broad families.

Shared-secret signing (HMAC). Algorithms such as HS256 use one secret known to both the issuer and the verifier. Whoever can verify a token can also create one, so this suits cases where a single service issues and checks its own tokens.

Key-pair signing (RSA or ECDSA). Algorithms such as RS256 and ES256 use a private key to sign and a matching public key to verify. The public key can be shared widely, so many services can check tokens without being able to forge them. This is the usual choice when one identity provider serves many separate applications.

In both cases the signature covers the encoded header and payload exactly as they appear. Change a single character and verification fails. The signing formats come from RFC 7515, and the algorithm names are listed in RFC 7518.

Decoding is not verifying

Anyone holding a token can decode the header and payload, because Base64URL is an encoding and not encryption. Decoding only tells you what the token says about itself. It does not prove that anyone trustworthy wrote it.

Think of it as reading the text on an envelope versus checking the seal. A forged token can claim any sub or role it likes, and it will decode perfectly. Only a signature check, using the correct key, shows that the token came from the expected issuer and was not altered. A decoder tool is for inspecting and debugging. Trust decisions belong on your server, using a well-maintained library rather than hand-written code. For the underlying idea, see Base64 is not encryption.

What a server should check

When a request arrives with a token, a sound verifier does all of the following, in the library of your choice:

  1. Verify the signature with the right key.
  2. Compare the alg against an allow-list that you set, instead of trusting whatever the token header says.
  3. Reject the token if exp has passed or nbf has not yet arrived, allowing only a small clock-skew margin.
  4. Confirm that aud names your service.
  5. Confirm that iss is an issuer you trust.

The first two matter together. The header is untrusted input, so the verifier decides which algorithms are acceptable and which key applies, never the token. The recommendations in RFC 8725 cover these points in more depth.

Common mistakes

  • Secrets in the payload. Signed is not secret. Passwords, keys and sensitive personal data do not belong there.
  • Accepting the none algorithm. This value means an unsigned token. Unless you deliberately want that, your allow-list should exclude it.
  • Skipping exp. A token that never expires stays useful to anyone who obtains it, indefinitely.
  • Very long lifetimes. Short-lived access tokens limit the damage from a leak. Use a separate, carefully handled refresh mechanism if sessions must last.
  • Careless storage. Treat tokens like passwords: keep them out of logs, URLs and shared screenshots, and think carefully about where your client stores them.
  • Ignoring audience and issuer. A valid token meant for another service should not open yours.
  • Pasting live tokens into websites. Even a tool that decodes locally is a habit worth avoiding with production tokens.

A short checklist

  • Payload holds no secrets.
  • Signature verified server-side with a maintained library.
  • Algorithms restricted to an explicit allow-list.
  • exp, nbf, aud and iss all checked.
  • Access tokens kept short-lived.
  • Tokens kept out of logs and URLs.

Try it yourself

Paste the sample token above into the JWT decoder to see the header and payload laid out, then confirm the Base64URL pieces with the Base64 encoder and decoder. For a deeper look at why readable does not mean protected, read Base64 is not encryption.

Sources