What Is a JWT? JSON Web Token Explained Simply

If you've worked with APIs, you've probably seen a long string made of three chunks separated by dots, something like eyJhbGciOi...eyJzdWIiOi...JRaTc3pY..., in a login response or an Authorization header. That string is very often a JSON Web Token, or JWT. JWTs are commonly used to carry authentication and authorization information, to pass identity claims between systems, and to prove to an API that a request comes from a particular user or service.

One distinction is worth making before anything else: a JWT is a token format, not an authentication system. It defines how to package and sign a set of claims. Deciding who gets a token, how long it lasts, where it's stored, and when it's rejected is up to the application around it. Much of the confusion about JWTs — and most JWT security mistakes — comes from treating the format as if it were the whole system.

What Is a JWT?

JWT stands for JSON Web Token. The specification (RFC 7519) defines it as "a compact, URL-safe means of representing claims to be transferred between two parties." A claim is just a statement about something — for example "this token's subject is user 12345" or "this token expires at a certain time."

A signed JWT is one string made of three sections joined by dots:

xxxxx.yyyyy.zzzzz

which stand for:

Header.Payload.Signature
  • The header describes the token itself, such as which algorithm was used to sign it.
  • The payload holds the claims.
  • The signature lets a receiver detect whether the header or payload was changed.

Both the header and the payload are JSON objects. If you want a refresher on that format, see our guide to JSON and how to format it.

JWT Structure Explained

Header

{
  "alg": "HS256",
  "typ": "JWT"
}
  • alg names the algorithm used to sign the token. HS256 means HMAC with SHA-256 using a shared secret. Other common values include RS256 and ES256, which use a private key to sign and a public key to verify. The actual algorithm depends on how the issuing system is configured — not every JWT uses HS256.
  • typ is the token type. The specification recommends "JWT", though it's optional, and some profiles use other values.

Headers can carry other parameters too, such as kid (a key ID, used to pick the right verification key when a system has several).

Payload

{
  "sub": "12345",
  "name": "John Doe",
  "role": "admin",
  "iat": 1710000000,
  "exp": 1710003600
}

The payload contains the claims. The specification defines a set of registered claims that are commonly used:

  • iss (issuer) — who issued the token
  • sub (subject) — who or what the token is about
  • aud (audience) — who the token is intended for
  • exp (expiration time) — when the token stops being acceptable
  • nbf (not before) — the time before which the token must not be accepted
  • iat (issued at) — when the token was issued
  • jti (JWT ID) — a unique identifier for the token

None of these are mandatory. The specification says explicitly that none of the registered claims are "intended to be mandatory to use or implement in all cases," so a JWT with no exp or no iss is still a valid JWT — whether a particular API accepts it is a separate question. Beyond the registered claims, a payload can hold anything the application needs, like name and role in the example above. Those are application-specific, and their meaning is defined by the system that issues and reads them.

The time claims (exp, iat, nbf) are a count of seconds since 1 January 1970 UTC — not milliseconds, which is what JavaScript's Date.now() returns. Mixing the two up is a classic bug: milliseconds read as seconds give an expiry tens of thousands of years away, while seconds read as milliseconds land in January 1970. In the example, 1710000000 is 9 March 2024, 16:00 UTC.

Signature

Conceptually, the signature is produced like this:

signature = sign( base64url(header) + "." + base64url(payload), key )

The exact operation depends on the algorithm in the header. With HS256, it's an HMAC-SHA-256 computed with a shared secret. With RS256 or ES256, it's a digital signature made with a private key, which anyone with the matching public key can check.

What the signature is meant to give a receiver is integrity (the header and payload haven't been altered since signing) and authenticity (the token was produced by whoever holds the signing key) — the latter only to the extent the receiver trusts the verification key it's using. The signature does not hide anything. That's covered below.

A JWT Example, Broken Into Three Parts

Here is a real, working sample token, signed with HS256:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJKb2huIERvZSIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTcxMDAwMDAwMCwiZXhwIjoxNzEwMDAzNjAwfQ.JRaTc3pYd8CGWx7WSe3P5bDFjMW3uDBaaPAyDaVcAcM

Split at the dots:

HEADER:    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
PAYLOAD:   eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJKb2huIERvZSIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTcxMDAwMDAwMCwiZXhwIjoxNzEwMDAzNjAwfQ
SIGNATURE: JRaTc3pYd8CGWx7WSe3P5bDFjMW3uDBaaPAyDaVcAcM

Decoding the first two sections gives back the JSON:

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

{"sub":"12345","name":"John Doe","role":"admin","iat":1710000000,"exp":1710003600}

Those first two sections are encoded with Base64URL — the same idea as Base64, but using - and _ instead of + and /, and with the trailing = padding left off, so the result is safe to put in a URL or HTTP header. If you want the background on how Base64 works, see our Base64 encoding guide. The key point for JWTs: Base64URL is encoding, not encryption. It makes the payload transportable, not secret. Anyone who has the token can decode it.

(This sample was signed with a throwaway demo secret, example-secret-do-not-use-in-production, so you can experiment with it safely. Never use a guessable secret like that for real tokens.)

How Does JWT Authentication Work?

The most common pattern looks like this:

  1. The user sends their credentials (for example, email and password) to a login endpoint.
  2. The server verifies the credentials.
  3. If they're valid, the server creates a JWT containing claims about the user, signs it, and returns it.
  4. The client stores the token.
  5. For each API request, the client sends the token in the Authorization header.
  6. The server verifies the token — checking the signature, the expiration, and any other claims it cares about — and either accepts or rejects the request.

The header looks like this:

Authorization: Bearer <token>

"Bearer" describes how the token works: RFC 6750 defines a bearer token as one where "any party in possession of the token (a 'bearer') can use the token in any way that any other party in possession of it can." In other words, nothing else proves who is sending the token — whoever holds it can use it. That's why bearer tokens must only travel over HTTPS, and why leaked tokens are a real problem.

Where the client stores the token (in memory, in a cookie, in browser storage) is a design decision with trade-offs around XSS and CSRF exposure. There's no single right answer, but the RFC is clear on two points: never send bearer tokens in the clear, and avoid putting them in page URLs, since URLs end up in logs and browser history.

JWT Authentication Example

1. Log in:

POST /login
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "example-password"
}

2. The server responds with a token:

{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600
}

The field names vary between APIs — access_token, token, and jwt are all common — so check the API's documentation.

3. Call a protected endpoint:

GET /api/profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

The server reads the token, verifies its signature with its key, confirms it hasn't expired, and then uses claims like sub to work out whose profile to return. If the token is missing, invalid, or expired, the API typically responds with 401 Unauthorized. A valid token that doesn't grant permission for the requested resource is usually a 403 Forbidden instead.

Notice what the server did not necessarily do: look the user up in a session table. Some systems can accept a request based on the token alone, which is why JWTs are often described as "stateless." But that's a property of a particular design, not of JWTs themselves — many systems also check a database or a revocation list on each request.

What Belongs in the Payload — and What Doesn't

A typical access-token payload might look like this:

{
  "sub": "user_123",
  "role": "user",
  "iss": "example-api",
  "iat": 1710000000,
  "exp": 1710003600
}

It identifies who the token is about (sub), what they're allowed to do (role), who issued it (iss), and the window it's valid for (iat, exp). Keep payloads small — they're sent with every request — and limited to what the receiver actually needs.

Do not put sensitive information in a normal JWT payload: no passwords, credit card numbers, private API keys, or other secrets. Ordinary signed JWTs are readable by anyone who holds them, as the next section explains.

Is a JWT Encrypted?

A normal signed JWT does not hide its contents. Its header and payload are only Base64URL-encoded, so anyone who obtains the token can decode them — the user it was issued to, an attacker who steals it, a developer looking at browser dev tools. The signature stops people from changing the payload without detection; it does not stop them from reading it.

The JWT specification itself warns about this: a JWT "may contain privacy-sensitive information," and when it does, measures must be taken to prevent disclosure. It suggests using an encrypted JWT, sending tokens only over encrypted transport such as TLS, and — the simplest option — leaving privacy-sensitive information out entirely.

Encrypted JWTs do exist (see JWS vs JWE below), but they're a different variant. Don't assume a token you're looking at is encrypted; if you can decode it, it isn't. This is the same lesson as in our Base64 guide: encoding makes data portable, not private.

Encoding vs Signing vs Encryption vs Hashing

These four terms get mixed up constantly, and JWTs involve several of them:

ConceptPurposeWhere it shows up in a JWT
EncodingRepresent data in another formatHeader and payload are Base64URL-encoded
SigningDetect modification and establish authenticityThe signature section
EncryptionKeep data confidentialOnly in encrypted JWTs (JWE)
HashingProduce a fixed-size, one-way digestUsed inside algorithms like HS256 (SHA-256); passwords are also stored as one-way hashes

JWT vs Session-Based Authentication

FeatureJWTServer session
StateOften used in a stateless API patternSession state is kept on the server
Client sendsThe token itselfA session identifier
Server storageDepends on the architectureUsually session data
RevocationNeeds additional designUsually straightforward — delete the session
Common useAPIs and distributed systemsTraditional web applications

Neither is universally better. Sessions make revocation simple, because the server owns the state. JWTs can let multiple services verify a token without sharing a session store, but they make early revocation harder: once issued, a token stays acceptable until it expires unless you add something to track it, such as short lifetimes, a denylist of token IDs, or rotating refresh tokens — which reintroduces some server-side state. The right choice depends on your architecture, your security requirements, and how much complexity you want to take on.

Access Tokens vs Refresh Tokens

Many systems use two kinds of tokens, following the OAuth 2.0 model:

  • Access token: sent to the API with each request to authorize it. Usually short-lived, so a leaked one is only useful briefly.
  • Refresh token: used to obtain a new access token when the current one expires. Usually longer-lived, and per the OAuth specification it's only ever sent to the authorization server, never to the APIs. Because it's more valuable, it should be handled more carefully.

Implementations differ. OAuth doesn't require access tokens to be JWTs at all — they're often opaque strings the API looks up — though a separate specification (RFC 9068) defines a standard JWT format for those who want one.

What Does JWT Expiration (exp) Mean?

{
  "exp": 1710003600
}

exp is the expiration time claim: a timestamp, in seconds since 1970 UTC, on or after which the token must not be accepted. Here that's 9 March 2024, 17:00 UTC. A server that sees a token whose exp is in the past should reject it, and the client would then need to log in again or use a refresh token. (Many libraries allow a small configurable tolerance for clock differences between servers.)

A common question is whether you could just edit exp to extend a token. You can change the text — but the signature was computed over the original header and payload, so the modified token fails verification and a correctly configured server rejects it. That's the signature doing its job.

How to Decode a JWT

When an API call is failing and you need to see what's inside a token, Kravoy's JWT Decoder reads it for you:

  1. Copy your JWT. Copy only the token itself — if you copied it from an Authorization header, remove the Bearer prefix, or decoding will fail.
  2. Open the JWT Decoder and paste the token.
  3. Check the header — the algorithm and token type.
  4. Check the payload claims. Time claims like exp and iat are shown as readable dates (in your local time zone) alongside the raw numbers.
  5. Check the expiry status at the top — whether the token is valid, expiring soon, or already expired.
  6. Read the security audit notes, which flag things like a missing exp, an unusually long lifetime, no aud or iss claim, or alg: none.

The decoder also has an optional HS256 signature check (paste the signing secret to see whether it matches), a comparison view for diffing the claims of two tokens, and code snippets for using the token in curl, fetch, axios, Python, or Go. It runs in your browser, and the page states that the token isn't saved. Try it with the sample token above: it will report it as expired (the exp is in 2024), and if you enter the demo secret it will confirm the signature matches.

Two limits to know about: signature verification is HS256 only, and the decoder handles standard three-part signed tokens — an encrypted five-part token (JWE) can't be read without decrypting it first. If a token fails to decode, the usual cause is that it was truncated or altered when copied, which often shows up as a decode or invalid-JSON error when the payload is parsed; our guide to common JSON syntax errors explains what those look like.

Is It Safe to Decode a JWT?

Three different actions get lumped together, and they carry different risks:

  • Decoding means Base64URL-decoding the header and payload to read them. It doesn't require the signing secret, because those sections were never encrypted — decoding is the same thing an attacker or a curious developer can do with any token.
  • Verifying means checking the signature with the right key to confirm the token is genuine and unmodified. This is what needs the secret (for HS256) or the public key (for RS256/ES256), and it's what an API must do before trusting a token. A decoder showing you claims has told you nothing about whether the token is authentic.
  • Using means sending the token to an API. A live token is a credential — anyone who has it can act as the user until it expires.

So decoding is low-risk, but pasting a live production token somewhere is not. Never share active production access tokens publicly — in screenshots, chat messages, support tickets, GitHub issues, or logs. Even with a decoder that runs locally, prefer expired or test tokens where you can. If a live token has been exposed, treat it as compromised: revoke it or let it expire, and rotate the signing key if the exposure suggests it might have leaked.

Common JWT Mistakes

  1. Treating Base64URL encoding as encryption. The payload is readable by anyone holding the token.
  2. Putting sensitive information in the payload. Passwords, card numbers, and secrets don't belong there.
  3. Using unnecessarily long token lifetimes. The longer a token lives, the more damage a leaked one can do.
  4. Not validating expiration. An expired token that still works defeats the point of having exp.
  5. Not verifying the signature. Decoding a token and trusting its claims is not the same as verifying it. The specification is explicit that a JWT's contents "cannot be relied upon in a trust decision unless its contents have been cryptographically secured and bound to the context necessary for the trust decision."
  6. Trusting claims without proper validation. Check that the issuer and audience are what you expect, not just that the signature is valid.
  7. Logging live access tokens. Logs are widely readable and long-lived.
  8. Assuming every JWT uses the same signing algorithm. Read the algorithm from your own configuration, not blindly from the token — a token whose header says alg: none claims to have no signature at all, and a verifier must never accept that unless it has explicitly chosen to.

JWT Security Checklist

  • Verify the signature on every token.
  • Validate the expected algorithm and configuration — don't let the token decide.
  • Check expiration (exp), and nbf if used.
  • Validate the issuer (iss) where applicable.
  • Validate the audience (aud) where applicable.
  • Use HTTPS everywhere tokens travel.
  • Avoid sensitive data in ordinary JWT payloads.
  • Keep token lifetimes as short as your use case allows.
  • Use a strong, randomly generated secret for HS256 — the JWT best-practices RFC says human-memorizable passwords must not be used directly as the key.
  • Protect refresh tokens carefully.
  • Don't expose production tokens in logs, screenshots, URLs, or public repositories.

JWT vs JWS vs JWE

These three acronyms are related but not interchangeable:

  • JWT (RFC 7519) is the format for a set of claims.
  • JWS (JSON Web Signature) is a structure for signed content. A signed JWT is a JWT carried inside a JWS — this is the three-part token that's by far the most common, and the kind this article has focused on.
  • JWE (JSON Web Encryption) is a structure for encrypted content. A JWT carried inside a JWE has five dot-separated parts instead of three, and its contents can't be read without the decryption key.

If you see three parts, it's a signed token whose payload is readable. If you see five, it's encrypted.

Frequently Asked Questions

What does JWT stand for?

JSON Web Token. The specification suggests pronouncing it like the English word "jot," though many people just say the letters.

Is JWT encrypted?

Not by default. A standard signed JWT is encoded and signed, not encrypted. Encrypted JWTs (JWE) exist, but they're a separate variant with five parts instead of three.

Can anyone decode a JWT?

Anyone who has the token can decode its header and payload, because they're only Base64URL-encoded. They can't forge a valid signature without the signing key.

Is JWT safe?

A JWT is only as safe as the system around it. The format supports strong signatures, but security depends on verifying signatures, checking expiration and claims, protecting tokens in transit and storage, and keeping sensitive data out of the payload.

What is the difference between JWT and a session?

A session keeps state on the server and gives the client an identifier to look it up. A JWT packs claims into a signed token the client sends with each request. Sessions make revocation simpler; JWTs can let multiple services verify a request without a shared session store. Neither is universally better.

What are the JWT header, payload, and signature?

The header describes the token and its signing algorithm, the payload contains the claims, and the signature lets a receiver detect any change to the first two sections.

What is an expired JWT?

A token whose exp claim is in the past. A server should reject it, and the client needs a new token — by logging in again or using a refresh token.

Can I decode a JWT without the secret key?

Yes. Decoding only reverses the Base64URL encoding and doesn't need a key. What you can't do without the key is verify that the signature is genuine.

Recent Articles