How JWT Authentication Works
JSON Web Tokens explained: header, payload, signature, claims, signing algorithms, access and refresh tokens, validation rules, browser storage tradeoffs, and the security mistakes that break auth systems.
TL;DR - Key Points
What Is a JWT?
A JSON Web Token is a compact, URL-safe token format used to represent claims between parties. In authentication systems, a server or identity provider issues a JWT after login. The client then sends that token with API requests, and the API verifies it before trusting the request.
A typical JWT looks like three long text segments separated by dots: header.payload.signature. The header says what kind of token it is and which algorithm was used. The payload contains claims such as user ID, issuer, audience, expiry, roles, or scopes. The signature lets the server detect whether the header or payload was changed.
The most important security fact is simple: signed JWTs are not secret. The header and payload are encoded, not encrypted. If a token contains an email, role, tenant ID, or internal identifier, anyone holding the token can decode and read it. Never put passwords, API keys, private notes, or sensitive personal data in a normal signed JWT.
JWTs are useful because APIs can verify tokens without calling a session database on every request. That statelessness is powerful, but it creates tradeoffs around revocation, logout, key rotation, and stale permissions.
The Three Parts of a JWT
| Part | Example | Purpose |
|---|---|---|
| Header | { "alg": "RS256", "typ": "JWT" } | Describes token type and signing algorithm. |
| Payload | { "sub": "user_123", "exp": 1767225600 } | Contains claims about the subject, issuer, audience, expiry, roles, or scopes. |
| Signature | Sign(base64url(header) + '.' + base64url(payload)) | Lets the server detect tampering when it verifies the token. |
Base64URL decoding a JWT is safe for inspection, but it does not prove the token is real. Trust begins only after signature and claim validation.
Common JWT Claims
| Claim | Name | Meaning | Example |
|---|---|---|---|
| iss | Issuer | Who issued the token | https://auth.example.com |
| sub | Subject | Who the token is about | user_123 |
| aud | Audience | Who should accept the token | api://billing-service |
| exp | Expiration Time | Token must not be accepted after this Unix timestamp | 1767225600 |
| nbf | Not Before | Token must not be accepted before this time | 1767222000 |
| iat | Issued At | When the token was issued | 1767220200 |
| jti | JWT ID | Unique token identifier, useful for replay detection or revocation lists | 01HZY... |
The time claims use Unix timestamps in seconds. Do not compare exp to JavaScript Date.now() directly unless you divide Date.now() by 1000.
JWT Authentication Flow
| Step | Client | Server / API |
|---|---|---|
| 1. Login | Sends username/password or OAuth code | Validates credentials with identity provider or user database |
| 2. Issue tokens | Receives access token and maybe refresh token | Signs JWT with configured algorithm, expiry, issuer, audience, and subject |
| 3. API request | Sends Authorization: Bearer <access_token> | Extracts token before route handler |
| 4. Verify token | No action | Checks signature, exp, nbf, iss, aud, algorithm, and required claims |
| 5. Authorize | Receives result or 403 | Uses scopes/roles/permissions to decide whether action is allowed |
| 6. Refresh | Uses refresh token when access token expires | Validates refresh token, rotates it, and issues new access token |
Signing Algorithms
| Algorithm | Type | Key Model | Best Use | Risk |
|---|---|---|---|---|
| HS256 | Symmetric HMAC | Same secret signs and verifies | Simple first-party systems where every verifier can safely know the secret | Secret leakage lets attackers mint tokens. |
| RS256 | Asymmetric RSA | Private key signs, public key verifies | Distributed systems, OAuth/OIDC, APIs verifying tokens from an auth server | Key rotation and JWKS caching must be handled carefully. |
| ES256 | Asymmetric ECDSA | Private key signs, public key verifies | Modern compact signatures when library support is mature | Implementation quality and operational familiarity matter. |
| none | No signature | No cryptographic verification | Almost never for authentication | Must not be accepted for auth tokens unless explicitly designed for a safe non-auth context. |
A verifier should know which algorithm it expects before reading the token. Do not let an attacker choose the verification rules through the token header.
Access Tokens vs Refresh Tokens
| Factor | Access Token | Refresh Token |
|---|---|---|
| Purpose | Authorize API requests | Get new access tokens |
| Lifetime | Short, often minutes | Longer, often days or weeks |
| Sent to APIs | Yes, on most authenticated requests | No, only to token refresh endpoint |
| Storage priority | Reduce theft and exposure | Protect heavily; rotate and revoke on reuse |
| Revocation | Harder if stateless and short-lived | Usually server-tracked for rotation and logout |
| If stolen | Attacker can call APIs until expiry | Attacker may mint new access tokens unless reuse detection stops it |
Server-Side JWT Validation Checklist
| Check | Why It Matters | If It Fails |
|---|---|---|
| Parse format | A normal signed JWT has exactly three dot-separated segments. | Reject malformed tokens before deeper processing. |
| Allowlist algorithm | Do not trust alg blindly from the token header. | Reject unexpected algorithms and never auto-switch key types. |
| Verify signature | Proves header and payload were not changed after signing. | Reject on any verification error. |
| Validate exp and nbf | Prevents expired or not-yet-valid tokens from being accepted. | Reject or allow only small clock skew. |
| Validate iss | Confirms the trusted issuer created the token. | Reject tokens from unknown issuers. |
| Validate aud | Prevents a token meant for one API being replayed at another. | Reject audience mismatch. |
| Validate scopes/roles | Authentication is not the same as authorization. | Return 403 when identity is valid but permission is missing. |
| Check revocation when needed | Logout, compromise, or refresh-token reuse may require server-side state. | Reject revoked jti/session identifiers. |
Browser Storage Options
| Storage | Strengths | Weaknesses |
|---|---|---|
| HttpOnly Secure cookie | Not readable by JavaScript; works well with browser apps | Needs CSRF protections and careful SameSite/domain settings |
| Memory only | Token disappears on refresh/close; less persistent theft risk | Requires refresh strategy and can complicate UX |
| localStorage | Simple and persistent | Readable by JavaScript, so XSS can steal tokens |
| sessionStorage | Clears when tab/session ends | Still readable by JavaScript and vulnerable to XSS |
| Native mobile secure storage | Can use OS-backed secure storage | Still needs device compromise and refresh-token rotation considerations |
Storage is a threat-model decision. Whatever you choose, XSS prevention, HTTPS, cookie flags, CSRF strategy, and short-lived access tokens matter.
Common JWT Vulnerabilities
| Issue | Problem | Fix |
|---|---|---|
| Decoding without verifying | The app trusts payload fields after Base64URL decoding only. | Always verify the signature and registered claims server-side. |
| Algorithm confusion | The verifier accepts unexpected alg values or mixes symmetric/asymmetric keys incorrectly. | Hard-code an algorithm allowlist and use libraries safely. |
| Long-lived access tokens | A stolen token remains useful for too long. | Use short access-token lifetimes and refresh-token rotation. |
| Secrets in payload | JWT payload is readable by anyone who has the token. | Store only non-sensitive claims or use encrypted JWE where appropriate. |
| Missing audience checks | A token issued for one service is accepted by another. | Validate aud for every API. |
| No revocation strategy | Logout or compromise cannot invalidate already-issued stateless tokens. | Use short expiry, jti/session records, refresh-token rotation, or introspection where needed. |
| Storing tokens in localStorage with XSS | Malicious JavaScript can read and exfiltrate tokens. | Prevent XSS, consider HttpOnly cookies or memory storage, and use CSP. |
JWT Debugging Quick Reference
| Symptom | Likely Cause | What to Check |
|---|---|---|
| User is logged out immediately | exp is in the past or clock skew is wrong | Decode exp, compare to current Unix seconds, verify server clock. |
| API returns 401 | Signature invalid, token expired, wrong issuer, wrong audience | Check verification logs, iss, aud, kid, alg, and expiry. |
| API returns 403 | Token is valid but missing permission | Inspect scopes, roles, tenant ID, and route authorization policy. |
| Works locally, fails in production | Different issuer, audience, public key, cookie domain, or HTTPS setting | Compare environment config and JWKS URL. |
| Refresh works once then fails | Refresh-token rotation or reuse detection issue | Confirm client stores the newest refresh token after every refresh. |
| Token accepted by wrong service | Audience validation missing | Add strict aud validation per API. |
Frequently Asked Questions
Is a JWT encrypted?
Usually no. The common JWT used in web authentication is a signed JWS. The header and payload are Base64URL-encoded, which means they are easy to decode. Signing protects integrity, not confidentiality. Use JWE or avoid putting sensitive data in the token if confidentiality is required.
Is decoding a JWT enough to trust it?
No. Decoding only reads the JSON. Anyone can create a fake header and payload. A backend must verify the signature and validate claims like exp, iss, aud, and nbf before trusting the token.
Where should JWTs be stored in a browser?
There is no one perfect answer. HttpOnly Secure cookies reduce JavaScript token theft but require CSRF defenses. Memory storage reduces persistence but complicates refresh. localStorage is simple but vulnerable to XSS token theft. Choose based on your threat model.
How long should an access token live?
Many systems use short lifetimes, often minutes, because access tokens are bearer credentials. The exact value depends on risk, UX, and whether refresh-token rotation and revocation are implemented.
What is the difference between authentication and authorization?
Authentication proves who the caller is. Authorization decides what that caller may do. A valid JWT can authenticate a user, but the API still needs to check scopes, roles, tenant access, and object-level permissions.
Can JWTs be revoked?
Pure stateless JWT access tokens are difficult to revoke before expiry. Common patterns include short access-token lifetimes, refresh-token rotation, server-side session IDs, jti deny-lists, key rotation for emergency invalidation, or token introspection.
What is JWKS?
JWKS stands for JSON Web Key Set. It is a published set of public keys that APIs can use to verify tokens signed by an authorization server. The token header often includes kid, a key ID that helps the verifier pick the right public key.
Should JWT contain roles?
It can, but be careful. Roles and scopes inside JWTs can become stale until the token expires. For high-risk permissions, check server-side authorization state or keep access tokens short.
Related Concepts
Related Tools
JWT Decoder
Decode JWT headers and payloads locally and inspect standard claims.
JWT Token Validator
Validate JWT structure and debug token problems during development.
JWT Encoder Signer
Create and sign test JWTs for local development workflows.
Timestamp to Date Converter
Convert exp, iat, and nbf Unix timestamps into readable dates.