---
term: 'JSON Web Token (JWT)'
seoTitle: 'What is a JWT? Structure, Claims, Security, Revocation'
headline: 'What is a JSON Web Token (JWT)?'
slug: json-web-token-jwt
category: auth-security
shortDefinition: 'A JSON Web Token is a compact, URL-safe token that carries signed JSON claims, letting servers verify requests without stored sessions.'
relatedTerms:
  - oauth-2-social-login
  - api-key-security
  - identity-access-management-iam
  - access-control-lists-acl
contrastsWith:
  - api-key-security
aboutTerms:
  - 'JWT Claims'
  - 'JWS (Signed JWT)'
  - 'Refresh Tokens'
faq:
  - question: 'What is a JWT in simple terms?'
    answer: 'A signed, Base64-encoded JSON "ID card" a server issues at login. The client presents it on every request, and the server verifies the signature instead of looking up a session — the token itself carries who you are and until when. Officially it rhymes with "jot," per the RFC that defines it.'
  - question: 'What are the three parts of a JWT?'
    answer: 'Header (token type and signing algorithm), payload (the claims — the actual JSON data), and signature, each Base64Url-encoded and joined by dots into header.payload.signature. The signature is computed over the first two parts, so any tampering with them breaks verification.'
  - question: 'Is a JWT encrypted?'
    answer: 'No — encoded, not encrypted. Base64Url is a transport format anyone can reverse; paste any JWT into a decoder and the payload is readable. The signature prevents tampering, not reading. Never put secrets or sensitive personal data in a JWT payload; the encrypted variant (JWE) exists for genuine confidentiality needs.'
  - question: 'What is the difference between a JWT and a session token?'
    answer: 'Where the state lives. A session token is an opaque ID pointing at server-side state — one lookup per request, revocable instantly. A JWT carries the state inside itself — no lookup, verifiable by any service holding the key, but valid until expiry no matter what. Stateless scaling versus instant control.'
  - question: 'Where should I store a JWT in the browser?'
    answer: 'Not in localStorage — any injected script can read it, making XSS a token theft. Current consensus: keep the access token in memory, the refresh token in an HttpOnly, Secure, SameSite cookie, and consider the backend-for-frontend pattern that keeps tokens out of the browser entirely.'
  - question: 'Can a JWT be revoked?'
    answer: 'Not by design — a signed token is valid until its exp claim, which is the price of statelessness. The workarounds all reintroduce state: a denylist keyed on the jti claim, versioned keys, or — the standard answer — very short access-token lifetimes paired with revocable, rotating refresh tokens.'
  - question: 'What are JWT claims?'
    answer: 'The payload''s fields. The RFC registers seven: iss (issuer), sub (subject), aud (audience), exp (expiration), iat (issued at), nbf (not before), and jti (token ID) — plus whatever custom claims you add, like roles. Verifiers must actually validate them; a signature check alone accepts expired tokens meant for other services.'
  - question: 'What is the difference between HS256 and RS256?'
    answer: 'Symmetry. HS256 signs and verifies with one shared secret — fine only when issuer and verifier are the same party, and only with a long random secret. RS256 (and the newer EdDSA) signs with a private key while anyone verifies with the public one — the default whenever more than one service checks tokens.'
  - question: 'Is JWT secure?'
    answer: 'The format is sound; implementations fail. The classic holes: accepting alg none, letting the token''s header choose the algorithm (the RS256-to-HS256 confusion attack), brute-forceable short HMAC secrets, and skipping aud/iss/exp validation. Pin your algorithms, validate every claim, keep lifetimes short.'
  - question: 'What is the difference between JWT and OAuth?'
    answer: 'Different categories: JWT is a token format; OAuth 2.0 is an authorization framework. They compose — OAuth flows commonly issue access tokens that happen to be JWTs, and OpenID Connect ID tokens always are. One says how a token is built; the other says how tokens get handed out.'
codeLanguages: [javascript, dart, swift, kotlin]
externalAuthorities:
  - name: 'RFC 7519 — JSON Web Token (JWT)'
    url: 'https://datatracker.ietf.org/doc/html/rfc7519'
  - name: 'RFC 8725 — JWT Best Current Practices'
    url: 'https://datatracker.ietf.org/doc/html/rfc8725'
  - name: 'OWASP JSON Web Token Cheat Sheet'
    url: 'https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_Cheat_Sheet.html'
  - name: 'RFC 7515 — JSON Web Signature (JWS)'
    url: 'https://datatracker.ietf.org/doc/html/rfc7515'
  - name: 'JSON Web Token — Wikipedia'
    url: 'https://en.wikipedia.org/wiki/JSON_Web_Token'
cta:
  title: 'Auth that already made these choices'
  text: 'Back4app handles sessions with revocable tokens, verifies identity-provider JWTs server-side for social login, and enforces per-user permissions on every request — token hygiene as platform behavior.'
  linkText: 'Start building free'
  linkUrl: 'https://www.back4app.com/signup'
author: 'Back4app Engineering'
publishedDate: '2026-07-24'
translationKey: json-web-token-jwt
---

**A JSON Web Token is a compact, URL-safe token that carries signed JSON claims, letting servers verify requests without stored sessions.** That is [RFC 7519's](https://datatracker.ietf.org/doc/html/rfc7519) own framing — "a compact, URL-safe means of representing claims to be transferred between two parties" — and the two ideas in it carry everything that follows: the token *contains* its facts, and a signature makes those facts *checkable* by anyone holding the right key, with no database in the loop.

## Key takeaways

| Question | Answer |
| --- | --- |
| The shape | `header.payload.signature` — three Base64Url parts joined by dots |
| The trick | Anyone can *decode* it; only key-holders can *forge* it |
| Not encryption | The payload is readable — signed ≠ secret (that's JWE's job) |
| The trade | Stateless verification ↔ no built-in revocation until `exp` |
| The discipline | Pin algorithms · validate `iss`/`aud`/`exp` · short lifetimes + refresh rotation |

## A JWT, decoded

```text
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJzdWIiOiJ1c3ItOGZrMiIsInJvbGUi… . dBjftJeZ4CVP…

header     { "alg": "HS256", "typ": "JWT" }
payload    { "sub": "usr-8fk2",  "role": "editor",
             "iss": "https://api.example.com",  "aud": "example-web",
             "iat": 1767024900,  "exp": 1767025800 }        ← 15-minute life
signature  HMACSHA256( base64url(header) + "." + base64url(payload), secret )

Anyone can DECODE the first two parts — Base64Url is packaging, not encryption.
Only key-holders can FORGE the third — which is the entire trick.
```

The contrast worth seeing in code — the *stateful* alternative every JWT decision is measured against:

**JavaScript:**

```javascript
// JavaScript / Node.js — Back4app JS SDK
// The stateful contrast: Back4app issues revocable session tokens
const user = await Parse.User.logIn('ada', 'correct-horse-battery');
const token = user.getSessionToken(); // r:abc123… — opaque, server-side state
// Every request presents it; the server can revoke it instantly:
await Parse.User.logOut(); // token invalid NOW — no waiting for an exp claim
```

**Flutter:**

```dart
// Flutter / Dart — Back4app Flutter SDK
// The stateful contrast: Back4app issues revocable session tokens
final user = ParseUser('ada', 'correct-horse-battery', null);
await user.login();
final token = user.sessionToken; // r:abc123… — opaque, server-side state
// Every request presents it; the server can revoke it instantly:
await user.logout(); // token invalid NOW — no waiting for an exp claim
```

**Swift:**

```swift
// iOS / Swift — Back4app Swift SDK
// The stateful contrast: Back4app issues revocable session tokens
let user = try await User.login(username: "ada", password: "correct-horse-battery")
let token = user.sessionToken // r:abc123… — opaque, server-side state
// Every request presents it; the server can revoke it instantly:
try await User.logout() // token invalid NOW — no waiting for an exp claim
```

**Kotlin:**

```kotlin
// Android / Kotlin — Back4app Android SDK
// The stateful contrast: Back4app issues revocable session tokens
val user = ParseUser.logIn("ada", "correct-horse-battery")
val token = user.sessionToken // r:abc123… — opaque, server-side state
// Every request presents it; the server can revoke it instantly:
ParseUser.logOut() // token invalid NOW — no waiting for an exp claim
```

## How verification works

```mermaid
flowchart LR
  accTitle: JWT issue and verify flow
  accDescr: At login the server signs a token containing claims and returns it to the client. The client stores it and sends it as a bearer token on each request. The server verifies the signature with its key and validates the claims, accepting or rejecting without any session lookup.
  L["Login<br/>(credentials verified once)"] --> S["Server signs JWT<br/>claims + key"]
  S --> C["Client holds token"]
  C -->|"Authorization: Bearer eyJ…"| V["Any server with the key:<br/>verify signature · validate claims"]
  V -->|"valid"| OK["Request proceeds<br/>no session lookup"]
  V -->|"tampered / expired / wrong aud"| NO["401"]
```

Precision the explainers skip: "JWT" names the claims format; what everyone actually passes around is a **JWS** ([RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515)) — the *signed* serialization — while **JWE** is the encrypted sibling for payloads that must stay unreadable. And verification is two jobs, not one: check the signature, then **validate the claims** — `exp` and `nbf` against the clock, `iss` against your allowlist of issuers, `aud` against *this service's* identifier. A perfectly signed token that's expired, from the wrong issuer, or minted for a different audience is a perfectly signed attack.

## Claims: the payload's vocabulary

| Claim | Name | Verifier's job |
| --- | --- | --- |
| `iss` | Issuer | Is this an issuer I trust? |
| `sub` | Subject | Who is this about — the stable user ID |
| `aud` | Audience | Was this minted for *me*? Reject others' tokens |
| `exp` | Expiration | Reject after this Unix timestamp |
| `iat` / `nbf` | Issued at / not before | Sanity-check the validity window |
| `jti` | Token ID | Unique handle — the hook a denylist needs |
| *custom* | Roles, tenant, plan… | App semantics — keep minimal, never secret |

Keep payloads lean twice over: tokens ride every request as headers (cookies cap near 4 KB, and each claim is repeated bandwidth), and everything in them is legible to whoever holds the token.

## JWT vs. session tokens

| | JWT (stateless) | Session token (stateful) |
| --- | --- | --- |
| The token is | The state itself, signed | An opaque pointer to server state |
| Per-request cost | Signature check, no lookup | One session-store lookup |
| Revocation | **None until `exp`** — by design | Instant — delete the session |
| Cross-service auth | Any service with the key verifies | Services must share the session store |
| Logout means | Client discards; token stays valid | Token actually dies |
| Best home | APIs, microservices, third-party verification | Single-backend apps, sensitive sessions |

The unfashionable truth several best-practice guides now lead with: for a server-rendered app with one backend, framework sessions are simpler *and* more controllable — JWTs earn their keep when tokens must be verified across services or by parties who shouldn't phone home per request.

## The revocation problem, honestly

Statelessness *is* the inability to revoke — the same property, described twice. A signed token is valid until `exp` no matter what happened since: logout, password change, account ban. Every fix reintroduces state, so choose which flavor: a **denylist** keyed on `jti` + `iss` (the [OWASP-recommended](https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_Cheat_Sheet.html) construction) checked per request — small state, but state; **key versioning**, which revokes *everyone* at once; or the standard architecture — **short-lived access tokens (5–15 minutes) plus revocable refresh tokens** with rotation: each refresh issues a new refresh token and retires the old, so a stolen one dies on first replay, and reuse of a retired token flags theft and kills the whole token family. Revocation latency then equals the access token's lifetime — which is the real reason those lifetimes are short.

## Choosing an algorithm — and the attacks on the choice

| | HS256 (HMAC) | RS256 (RSA) | EdDSA / ES256 |
| --- | --- | --- | --- |
| Keys | One shared secret | Private signs, public verifies | Private signs, public verifies |
| Verifiers need | The *secret* (can also forge!) | Public key only | Public key only |
| Fits | Issuer = verifier, single party | Multi-service, third-party | Same, smaller/faster signatures |
| Sharp edge | Short secrets brute-force offline from one captured token — use ≥256 random bits | Larger tokens, slower | Newer library support |

The attack section [RFC 8725](https://datatracker.ietf.org/doc/html/rfc8725) exists for, in three sentences. `alg: "none"` is a legal header value — libraries that honor it accept unsigned tokens. The confusion attack: a verifier that lets the *token's header* pick the algorithm can be handed an "HS256" token signed with the server's *public* RSA key as the HMAC secret — a public value used as a private one. Both die the same way: **the verifier pins its accepted algorithms and keys in configuration and never trusts the header to choose.**

## Where to store JWTs in a browser

| Location | XSS steals it? | CSRF sends it? | Survives refresh? | Verdict |
| --- | --- | --- | --- | --- |
| localStorage | **Yes** | No | Yes | Avoid — script-readable |
| Plain cookie | Yes (script-readable) | **Yes** | Yes | Worst of both |
| HttpOnly + Secure + SameSite cookie | No | Mitigated by SameSite | Yes | Good — for the refresh token |
| In memory | Only while injected | No | No | Good — for the access token |
| Backend-for-frontend holds tokens | Browser never has them | Cookie rules apply | Yes | Strongest for SPAs |

The threat model, not the folklore: localStorage trades CSRF-immunity for XSS-theft, cookies trade the reverse — which is why the consensus splits the pair: access token in memory, refresh token in a hardened cookie.

## Common use cases

- **API authentication** — the bearer token behind `Authorization` headers on [REST](/glossary/rest-api/) and GraphQL APIs.
- **Microservice identity** — one gateway-issued token verified independently by every service, no shared session store.
- **OpenID Connect ID tokens** — the signed identity assertion in every [social login](/glossary/oauth-2-social-login/) — always a JWT.
- **Cross-system handoffs** — signed links, webhook payloads, download grants: claims verified by a party that can't call you back.
- **Stateless authorization hints** — roles and tenant IDs carried in claims, with the authoritative check still server-side.

## Should you use JWTs or sessions? A decision matrix

| Your situation | Reach for |
| --- | --- |
| Single backend, server-rendered app | Sessions — simpler, instantly revocable |
| Public API consumed by many services | JWTs, asymmetric keys |
| Microservices behind a gateway | JWTs — verify locally, no shared store |
| Instant lockout is a hard requirement | Sessions, or JWTs + denylist and short `exp` |
| Third parties must verify your assertions | JWTs — that's the format's home game |
| Mobile app against a BaaS | The platform's session mechanism — it chose already |

## Limitations and trade-offs

- **Irrevocability is structural.** Every mitigation — denylists, short lifetimes, rotation — is a partial return of the state you removed; price that in before choosing stateless.
- **Claims go stale.** Roles snapshot at issuance; a demoted admin stays an admin until expiry. Short lifetimes bound the staleness window.
- **The payload is public.** Treat it as readable by the user and any token thief — identifiers yes, secrets and PII no.
- **Library defaults have burned people.** Algorithm pinning, claim validation, and `typ` checking are your configuration responsibilities, not the library's guarantees.
- **Size compounds.** Every claim rides every request; generous payloads tax mobile bandwidth and can overflow cookie limits.

## Tokens on Back4app

Back4app is an open-source Backend-as-a-Service (BaaS) platform that combines a managed database, auto-generated REST and GraphQL APIs, authentication, file storage, and Cloud Code serverless functions. The platform's own choice illustrates the decision matrix: client sessions use **revocable, server-side session tokens** — the code tabs above — so logout, password changes, and dashboard-side session termination take effect immediately, with no expiry window to wait out. JWTs appear where they belong: identity providers' OIDC ID tokens are verified server-side by auth adapters during [social login](/glossary/oauth-2-social-login/), and Cloud Code functions can mint or verify JWTs for third-party handoffs using standard libraries — with the signing secret in server-side configuration, never shipped to clients. Stateful where control matters, stateless at the edges where verification must travel: the architecture this article argues for, preassembled.
