
JWT Authentication Explained: Tokens, Claims & Security
JWT authentication is the mechanism most modern web and mobile applications use to verify identity across stateless API requests. Instead of storing session data on the server, the server issues a signed JSON Web Token (JWT) after login. The client sends this token with each subsequent request, and the server verifies the signature to confirm the user's identity. This approach scales horizontally without shared session state and works across domains, microservices, and mobile platforms. This guide covers how JWT authentication works, claim types, token expiration and refresh, and the security practices that separate a correct implementation from a vulnerable one.
How JWT Authentication Works
The JWT authentication flow has four phases: login, token issuance, token storage, and token verification.
<svg viewBox="0 0 800 360" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:800px;margin:24px auto;display:block">
<rect x="0" y="0" width="800" height="360" fill="#f8fafc" rx="12"/>
<text x="400" y="35" fill="#1e293b" font-size="17" font-family="sans-serif" text-anchor="middle" font-weight="bold">JWT Authentication Flow</text>
<rect x="40" y="60" width="140" height="50" fill="#3b82f6" rx="8"/>
<text x="110" y="90" fill="white" font-size="12" font-family="sans-serif" text-anchor="middle" font-weight="bold">1. Login</text>
<rect x="40" y="120" width="140" height="30" fill="#dbeafe" rx="4"/>
<text x="110" y="140" fill="#1e40af" font-size="10" font-family="sans-serif" text-anchor="middle">POST /login</text>
<line x1="190" y1="85" x2="230" y2="85" stroke="#94a3b8" stroke-width="2" marker-end="url(#arr5)"/>
<rect x="240" y="60" width="140" height="50" fill="#8b5cf6" rx="8"/>
<text x="310" y="90" fill="white" font-size="12" font-family="sans-serif" text-anchor="middle" font-weight="bold">2. Issue Token</text>
<rect x="240" y="120" width="140" height="30" fill="#ede9fe" rx="4"/>
<text x="310" y="140" fill="#5b21b6" font-size="10" font-family="sans-serif" text-anchor="middle">Sign & return JWT</text>
<line x1="390" y1="85" x2="430" y2="85" stroke="#94a3b8" stroke-width="2" marker-end="url(#arr5)"/>
<rect x="440" y="60" width="140" height="50" fill="#10b981" rx="8"/>
<text x="510" y="90" fill="white" font-size="12" font-family="sans-serif" text-anchor="middle" font-weight="bold">3. Store Token</text>
<rect x="440" y="120" width="140" height="30" fill="#d1fae5" rx="4"/>
<text x="510" y="140" fill="#065f46" font-size="10" font-family="sans-serif" text-anchor="middle">Cookie / localStorage</text>
<line x1="590" y1="85" x2="630" y2="85" stroke="#94a3b8" stroke-width="2" marker-end="url(#arr5)"/>
<rect x="640" y="60" width="120" height="50" fill="#f59e0b" rx="8"/>
<text x="700" y="90" fill="white" font-size="12" font-family="sans-serif" text-anchor="middle" font-weight="bold">4. Send Token</text>
<rect x="640" y="120" width="120" height="30" fill="#fef3c7" rx="4"/>
<text x="700" y="140" fill="#92400e" font-size="10" font-family="sans-serif" text-anchor="middle">Authorization: Bearer</text>
<rect x="200" y="180" width="400" height="60" fill="#1e293b" rx="8"/>
<text x="400" y="205" fill="white" font-size="13" font-family="sans-serif" text-anchor="middle" font-weight="bold">Server: Verify Signature</text>
<text x="400" y="225" fill="#94a3b8" font-size="11" font-family="sans-serif" text-anchor="middle">Check exp, iss, aud → Allow or Reject</text>
<line x1="700" y1="155" x2="700" y2="175" stroke="#94a3b8" stroke-width="2"/>
<line x1="700" y1="175" x2="400" y2="175" stroke="#94a3b8" stroke-width="2"/>
<line x1="400" y1="175" x2="400" y2="180" stroke="#94a3b8" stroke-width="2" marker-end="url(#arr5)"/>
<rect x="200" y="270" width="180" height="50" fill="#10b981" rx="8"/>
<text x="290" y="300" fill="white" font-size="13" font-family="sans-serif" text-anchor="middle" font-weight="bold">Valid → 200 OK</text>
<rect x="420" y="270" width="180" height="50" fill="#ef4444" rx="8"/>
<text x="510" y="300" fill="white" font-size="13" font-family="sans-serif" text-anchor="middle" font-weight="bold">Invalid → 401</text>
<line x1="350" y1="240" x2="290" y2="270" stroke="#94a3b8" stroke-width="2" marker-end="url(#arr5)"/>
<line x1="450" y1="240" x2="510" y2="270" stroke="#94a3b8" stroke-width="2" marker-end="url(#arr5)"/>
<defs>
<marker id="arr5" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#94a3b8"/>
</marker>
</defs>
</svg>
1. Login
The client sends credentials (username/password) to a login endpoint:
POST /api/login
Content-Type: application/json
{
"username": "alice",
"password": "secret123"
}
2. Token Issuance
The server verifies the credentials, creates a JWT containing the user's identity and claims, signs it with a secret or private key, and returns it:
HTTP/1.1 200 OK
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwibmFtZSI6IkFsaWNlIiwiZXhwIjoxNzMyNzU4NDAwLCJpYXQiOjE3MzI3NTQ4MDB9.sN3aZqKJ8wQyVbR5xT9mF2pL6hY0jN4kD7sW1cE3bA"
}
You can inspect this token with the JWT decoder to see the header, payload, and claims.
3. Token Storage
The client stores the token. The storage location matters for security (covered below). Common options:
- HttpOnly cookies — not accessible via JavaScript, protected from XSS
- localStorage — accessible via JavaScript, vulnerable to XSS
- Memory (in-memory variable) — cleared on page reload, most secure but least convenient
4. Token Verification
On each subsequent request, the client sends the token in the Authorization header:
GET /api/profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
The server verifies the signature, checks the exp claim, and processes the request. No database lookup needed — the token itself carries the identity.
JWT Claim Types
Claims are statements about the token and the user. They fall into three categories:
Registered Claims
Standard claims defined in RFC 7519:
| Claim | Purpose |
|---|---|
iss |
Issuer — who created the token |
sub |
Subject — who the token is about (user ID) |
aud |
Audience — intended recipient |
exp |
Expiration — when the token expires |
nbf |
Not Before — when the token becomes valid |
iat |
Issued At — when the token was created |
jti |
JWT ID — unique token identifier |
Private Claims
Custom claims defined by your application — role, permissions, email, department. These are agreed upon between the issuer and consumer:
{
"sub": "1234",
"role": "admin",
"permissions": ["read", "write", "delete"],
"department": "engineering"
}
Public Claims
Claims registered in the IANA JSON Web Token Claims Registry to avoid collisions between applications. Use these when your tokens are consumed by external parties.
Token Expiration and Refresh Tokens
Access Tokens
Access tokens are short-lived JWTs used to authenticate API requests. They should expire quickly — 5 to 15 minutes is common. Short expiration limits the damage if a token is stolen: the attacker has a narrow window before it becomes invalid.
The exp claim sets the expiration as a Unix timestamp:
{
"iat": 1732754800,
"exp": 1732755700
}
This token is valid for 900 seconds (15 minutes). You can decode and check exp with the JWT decoder.
Refresh Tokens
Since access tokens expire quickly, users would need to log in constantly without refresh tokens. A refresh token is a longer-lived credential (days or weeks) stored securely. When the access token expires, the client sends the refresh token to get a new access token without requiring the user to re-enter credentials:
POST /api/refresh
Authorization: Bearer <refresh_token>
→ Response: { "token": "<new_access_token>" }
Refresh tokens are typically stored in HttpOnly cookies and revoked server-side when the user logs out. Unlike access tokens, refresh tokens require server-side state because they can be revoked.
Security Best Practices
Use HTTPS Always
JWTs sent over HTTP can be intercepted. Always use HTTPS in production. The token in the Authorization header is bearer — anyone who captures it can use it until it expires. Learn how to verify your site's TLS setup with our SSL checker guide.
Keep Access Tokens Short-Lived
Set exp to 5-15 minutes. The shorter the token, the less damage a stolen token can do. Use refresh tokens for session continuity.
Store Tokens Securely
| Storage | XSS Risk | CSRF Risk | Recommendation |
|---|---|---|---|
| HttpOnly cookie | Low | Medium | Best for web apps |
| localStorage | High | Low | Acceptable for SPAs with CSP |
| Memory | None | None | Most secure, lost on reload |
Don't Put Sensitive Data in the Payload
The JWT payload is Base64-encoded, not encrypted. Anyone with the token can read it. Never include passwords, API keys, or personal data in claims. Use the Base64 converter to see how easily payload data is readable.
Verify the Algorithm
Ensure your verification library enforces the expected algorithm. Some older libraries accepted alg: none in the header, allowing attackers to forge tokens. Modern libraries reject this, but always verify explicitly:
// Node.js example with jsonwebtoken
jwt.verify(token, secret, { algorithms: ['HS256'] }, (err, decoded) => {
if (err) {
// Invalid token
return;
}
// Token is valid — use decoded claims
});
Use Strong Signing Keys
For HMAC (HS256), use a secret of at least 256 bits (32 bytes). For RSA (RS256), use at least 2048-bit keys. Weak keys can be brute-forced. Generate a strong secret using a hash generator or a cryptographic random generator.
JWT Authentication in Code
Here's a minimal Node.js implementation:
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET;
// Issue a token on login
function login(user) {
const token = jwt.sign(
{ sub: user.id, role: user.role },
SECRET,
{ expiresIn: '15m', issuer: 'myapp' }
);
return token;
}
// Verify token on each request
function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, SECRET, { algorithms: ['HS256'] });
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
Related Tools
- JWT Decoder — Decode and inspect JWT tokens
- Base64 Converter — Decode JWT parts manually
- Hash Generator — Generate strong signing secrets
Published: August 20, 2026
Category: Data Tools
Reading Time: 8 minutes



