Node.js 50 🟢 Stateless Authentication with JSON Web Tokens (JWT)
A traditional session lives on the server. The user logs in, the server creates the session record, and the session ID goes into the cookie. The next request carries the cookie, and the server looks up the session in the store. The lookup is the state, and the state is the scaling problem: the session store must be shared across the instances, and the every request requires the store round trip. The JSON Web Token takes a different approach. The server signs the token and gives it to the client. The client sends the token with the every request, and the server verifies the signature. The verification is the stateless, and the state lives in the token itself. The JWT is the self-contained credential, and the stateless authentication is the scaling answer.
The JWT has the three parts separated by the dots: the header, the payload, and the signature. The header declares the algorithm and the type. The payload carries the claims — the sub, the iat, the exp, and the custom. The signature is the cryptographic proof that the header and the payload were not tampered with. The three parts are the base64url-encoded, and the token is the string like the xxxxx.yyyyy.zzzzz. The server signs with the private key, and the client cannot forge the token without the key.
The statelessness is the benefit and the risk. The benefit is the scaling: the any instance can verify the token without the shared store. The risk is the revocation: the token is valid until it expires, and the server cannot revoke it before. The short-lived access token and the long-lived refresh token are the compromise. The access token is the 15 minutes, and the refresh token is the 7 days. The access token expiry limits the damage, and the refresh token is the revocable.
This chapter covers three areas. First, why the JWT exists — the session-scaling problem and the stateless verification. Second, the token anatomy and the signing — the header, the payload, the signature, the algorithms, and the jsonwebtoken library. Third, the authentication flow — the login, the access token, the refresh token, the middleware, and the security practices. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the JWT flow.
Key point: A JSON Web Token is the self-contained credential with the three parts: the header, the payload, and the signature. The server signs the token with the private key, and the client sends it with the every request. The server verifies the signature without the shared store, and the stateless authentication scales. The short-lived access token and the long-lived refresh token balance the security and the usability. The jsonwebtoken library is the Node.js implementation.
Why the JWT exists
The session-scaling problem. The session store is the shared state. The load balancer distributes the requests across the instances, and the every instance must read the same session store. The Redis session store solves the problem, but it adds the infrastructure and the round trip. The JWT removes the store: the token is the self-contained, and the any instance verifies it with the public key. The horizontal scaling is the simpler, and the stateless is the point.
The cross-service problem. The microservices must share the identity. The API gateway authenticates the user, and the downstream services need the identity. The JWT is the token that the gateway passes, and the services verify it. The shared session store across the services is the complex, and the JWT is the portable.
The stateless-verification problem. The signature is the proof. The server does not need the store, the lookup, or the database query. The jwt.verify(token, secret) is the cryptographic check, and the payload is the trusted after the check. The verification is the CPU-bound, and it is the microseconds.
The tampering problem. The payload is the base64-encoded, not the encrypted. The anyone can decode the payload and read the claims. The signature prevents the tampering, not the reading. The sensitive data must not go into the JWT payload, and the HTTPS is the requirement.
The revocation problem. The JWT cannot be revoked before the expiry. The logout does not invalidate the token on the server. The short expiry limits the window, and the refresh token provides the revocation point. The refresh token is the stored server-side, and the logout deletes it. The access token is the short, and the refresh token is the revocable.
The trade-off. The JWT trades the revocation for the statelessness. The session store allows the immediate revocation, and the JWT does not. The short access token and the refresh token are the compromise, and the trade-off is between the security and the scaling.
a. The token anatomy and the signing
The JWT has the three parts.
header.payload.signature
The header. The header declares the algorithm and the type.
{
"alg": "HS256",
"typ": "JWT"
}
The alg is the algorithm, and the typ is the type. The header is the base64url-encoded.
The payload. The payload carries the claims.
{
"sub": "1234567890",
"name": "Alice",
"iat": 1516239022,
"exp": 1516242622
}
The sub is the subject, the iat is the issued at, and the exp is the expiry. The custom claims — the role, the email — are the additions.
The signature. The signature is the HMAC or the RSA over the header and the payload.
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
The signature proves the header and the payload were not tampered.
The jsonwebtoken library. The jsonwebtoken is the standard Node.js library.
import jwt from 'jsonwebtoken';
const token = jwt.sign(
{ sub: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '15m', issuer: 'myapp' }
);
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
issuer: 'myapp',
});
The sign creates the token, and the verify validates the signature and the claims. The expiresIn sets the exp, and the issuer sets the iss.
The algorithms. The HS256 is the symmetric (the same secret for the sign and the verify), and the RS256 is the asymmetric (the private key signs, the public key verifies).
// The HS256
const token = jwt.sign(payload, secret, { algorithm: 'HS256' });
// The RS256
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
The RS256 is the better for the multi-service because the public key can be shared, and the private key stays on the auth service.
The base64url. The JWT uses the base64url encoding, and the jsonwebtoken handles it.
const parts = token.split('.');
const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString());
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
The payload is the readable without the verification, and the signature is the check.
b. The authentication flow
The login. The login verifies the credentials, creates the access token and the refresh token, and stores the refresh token.
async function login(email, password) {
const user = await db.query('SELECT * FROM users WHERE email = $1', [email]);
if (!user) throw new Error('Invalid credentials');
const valid = await bcrypt.compare(password, user.password_hash);
if (!valid) throw new Error('Invalid credentials');
const accessToken = jwt.sign(
{ sub: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ sub: user.id, type: 'refresh' },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);
await redis.setex(`refresh:${user.id}:${refreshToken}`, 604800, 'valid');
return { accessToken, refreshToken };
}
The access token is the 15 minutes, and the refresh token is the 7 days. The refresh token is stored in the Redis for the revocation.
The auth middleware. The middleware extracts the token, verifies it, and attaches the user to the request.
function authenticate(req, res, next) {
const header = req.headers.authorization;
if (!header || !header.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
const token = header.slice(7);
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = { id: decoded.sub, role: decoded.role };
next();
} catch (e) {
if (e.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(401).json({ error: 'Invalid token' });
}
}
The Bearer prefix is the convention, and the verify throws the TokenExpiredError or the JsonWebTokenError.
The refresh. The refresh endpoint verifies the refresh token, checks the Redis, and issues the new access token.
async function refresh(refreshToken) {
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
const stored = await redis.get(`refresh:${decoded.sub}:${refreshToken}`);
if (!stored) throw new Error('Refresh token revoked');
const accessToken = jwt.sign(
{ sub: decoded.sub, role: decoded.role },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
return { accessToken };
}
The Redis check is the revocation point, and the new access token is the 15 minutes.
The logout. The logout deletes the refresh token from the Redis.
async function logout(refreshToken) {
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
await redis.del(`refresh:${decoded.sub}:${refreshToken}`);
}
The access token remains valid until the expiry, and the refresh token is the revoked.
The role check. The middleware can check the role.
function requireRole(role) {
return (req, res, next) => {
if (req.user.role !== role) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
app.get('/admin', authenticate, requireRole('admin'), handler);
The requireRole is the middleware factory, and the authenticate runs first.
c. The security practices
The secret. The JWT_SECRET must be the strong and the random. The openssl rand -base64 64 generates the 64-byte secret, and the .env stores it.
openssl rand -base64 64
The secret must not be the short, the guessable, or the committed.
The expiry. The access token should be the 15 minutes, and the refresh token should be the 7 days. The long-lived access token is the security risk.
{ expiresIn: '15m' }
The exp claim is the standard, and the verify checks it.
The HTTPS. The token is the bearer credential, and the HTTPS prevents the interception. The HTTP is the insecure, and the token must not travel over the HTTP.
The storage. The token should not be in the localStorage if the XSS is the concern, because the JavaScript can read it. The httpOnly cookie is the alternative, and the CSRF protection is the requirement.
res.cookie('token', accessToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 15 * 60 * 1000,
});
The httpOnly prevents the JavaScript access, the secure requires the HTTPS, and the sameSite prevents the CSRF.
The algorithm. The alg: 'none' attack must be prevented. The verify must specify the allowed algorithms.
jwt.verify(token, secret, { algorithms: ['HS256'] });
The algorithms option prevents the none and the algorithm confusion attacks.
The payload. The sensitive data must not go into the payload, because the base64 is the readable.
// ❌ The sensitive
jwt.sign({ sub: user.id, password: user.password }, secret);
// ✅ The safe
jwt.sign({ sub: user.id, role: user.role }, secret);
The payload is the readable, and the secrets must not be there.
The refresh rotation. The refresh token should be the rotated on the each use.
async function refresh(refreshToken) {
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
const stored = await redis.get(`refresh:${decoded.sub}:${refreshToken}`);
if (!stored) throw new Error('Refresh token revoked');
await redis.del(`refresh:${decoded.sub}:${refreshToken}`);
const newRefreshToken = jwt.sign(
{ sub: decoded.sub, type: 'refresh' },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);
await redis.setex(`refresh:${decoded.sub}:${newRefreshToken}`, 604800, 'valid');
const accessToken = jwt.sign(
{ sub: decoded.sub, role: decoded.role },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
return { accessToken, refreshToken: newRefreshToken };
}
The rotation invalidates the old refresh token, and the stolen token is the useless after the legitimate refresh.
The blacklist. The short-lived token and the blacklist are the alternative.
async function logout(accessToken) {
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET, {
ignoreExpiration: true,
});
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
await redis.setex(`blacklist:${accessToken}`, ttl, '1');
}
The blacklist stores the revoked token until the expiry, and the middleware checks it.
Complete Example Session
// ============================================
// PART 1: THE TOKEN SIGNING
// ============================================
import jwt from 'jsonwebtoken';
const token = jwt.sign(
{ sub: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '15m', issuer: 'myapp' }
);
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
issuer: 'myapp',
});
// ============================================
// PART 2: THE LOGIN
// ============================================
async function login(email, password) {
const user = await db.query('SELECT * FROM users WHERE email = $1', [email]);
if (!user) throw new Error('Invalid credentials');
const valid = await bcrypt.compare(password, user.password_hash);
if (!valid) throw new Error('Invalid credentials');
const accessToken = jwt.sign(
{ sub: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ sub: user.id, type: 'refresh' },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);
await redis.setex(`refresh:${user.id}:${refreshToken}`, 604800, 'valid');
return { accessToken, refreshToken };
}
// ============================================
// PART 3: THE MIDDLEWARE
// ============================================
function authenticate(req, res, next) {
const header = req.headers.authorization;
if (!header || !header.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
const token = header.slice(7);
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = { id: decoded.sub, role: decoded.role };
next();
} catch (e) {
if (e.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(401).json({ error: 'Invalid token' });
}
}
// ============================================
// PART 4: THE REFRESH
// ============================================
async function refresh(refreshToken) {
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
const stored = await redis.get(`refresh:${decoded.sub}:${refreshToken}`);
if (!stored) throw new Error('Refresh token revoked');
const accessToken = jwt.sign(
{ sub: decoded.sub, role: decoded.role },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
return { accessToken };
}
// ============================================
// PART 5: THE LOGOUT
// ============================================
async function logout(refreshToken) {
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
await redis.del(`refresh:${decoded.sub}:${refreshToken}`);
}
// ============================================
// PART 6: THE ROLE CHECK
// ============================================
function requireRole(role) {
return (req, res, next) => {
if (req.user.role !== role) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
app.get('/admin', authenticate, requireRole('admin'), handler);
// ============================================
// PART 7: THE HTTPONLY COOKIE
// ============================================
res.cookie('token', accessToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 15 * 60 * 1000,
});
// ============================================
// PART 8: THE ALGORITHM RESTRICTION
// ============================================
jwt.verify(token, secret, { algorithms: ['HS256'] });
// ============================================
// PART 9: THE REFRESH ROTATION
// ============================================
async function refreshWithRotation(refreshToken) {
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
const stored = await redis.get(`refresh:${decoded.sub}:${refreshToken}`);
if (!stored) throw new Error('Refresh token revoked');
await redis.del(`refresh:${decoded.sub}:${refreshToken}`);
const newRefreshToken = jwt.sign(
{ sub: decoded.sub, type: 'refresh' },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);
await redis.setex(`refresh:${decoded.sub}:${newRefreshToken}`, 604800, 'valid');
const accessToken = jwt.sign(
{ sub: decoded.sub, role: decoded.role },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
return { accessToken, refreshToken: newRefreshToken };
}
// ============================================
// PART 10: THE COMPLETE APPLICATION
// ============================================
import express from 'express';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import Redis from 'ioredis';
const app = express();
app.use(express.json());
const redis = new Redis(process.env.REDIS_URL);
app.post('/login', async (req, res) => {
try {
const { accessToken, refreshToken } = await login(req.body.email, req.body.password);
res.cookie('access_token', accessToken, {
httpOnly: true, secure: true, sameSite: 'strict', maxAge: 15 * 60 * 1000,
});
res.cookie('refresh_token', refreshToken, {
httpOnly: true, secure: true, sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000,
path: '/auth/refresh',
});
res.json({ success: true });
} catch (e) {
res.status(401).json({ error: e.message });
}
});
app.post('/auth/refresh', async (req, res) => {
try {
const { accessToken, refreshToken } = await refreshWithRotation(req.cookies.refresh_token);
res.cookie('access_token', accessToken, {
httpOnly: true, secure: true, sameSite: 'strict', maxAge: 15 * 60 * 1000,
});
res.cookie('refresh_token', refreshToken, {
httpOnly: true, secure: true, sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000,
path: '/auth/refresh',
});
res.json({ success: true });
} catch (e) {
res.status(401).json({ error: e.message });
}
});
app.post('/logout', authenticate, async (req, res) => {
await logout(req.cookies.refresh_token);
res.clearCookie('access_token');
res.clearCookie('refresh_token', { path: '/auth/refresh' });
res.json({ success: true });
});
app.get('/profile', authenticate, (req, res) => {
res.json({ userId: req.user.id, role: req.user.role });
});
app.listen(3000);
The ten parts show the token signing, the login, the middleware, the refresh, the logout, the role check, the httpOnly cookie, the algorithm restriction, the refresh rotation, and the complete application.
Quick Reference
The Token Parts
| Part | The content |
|---|---|
| Header | The algorithm and the type |
| Payload | The claims (the sub, the iat, the exp) |
| Signature | The HMAC or the RSA |
The Standard Claims
| Claim | The meaning |
|---|---|
sub | The subject (the user id) |
iat | The issued at |
exp | The expiry |
iss | The issuer |
aud | The audience |
nbf | The not before |
jti | The JWT id |
The Algorithms
| Algorithm | The type |
|---|---|
HS256 | The HMAC with the SHA-256 |
RS256 | The RSA with the SHA-256 |
ES256 | The ECDSA with the SHA-256 |
The jsonwebtoken Methods
| Method | The purpose |
|---|---|
jwt.sign(payload, secret, options) | The create |
jwt.verify(token, secret, options) | The validate |
jwt.decode(token) | The decode without the verify |
The Errors
| Error | The cause |
|---|---|
TokenExpiredError | The exp passed |
JsonWebTokenError | The invalid signature |
NotBeforeError | The nbf not reached |
The Security
| The practice | The purpose |
|---|---|
| The strong secret | The prevent the forgery |
| The short expiry | The limit the damage |
| The HTTPS | The prevent the interception |
The httpOnly cookie | The prevent the XSS |
The sameSite | The prevent the CSRF |
The algorithms option | The prevent the alg none |
| The refresh rotation | The detect the stolen token |
Best Practices
✅ Do This:
// Use the strong secret
const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '15m' }); // ✅
// Use the short access token
{ expiresIn: '15m' } // ✅
// Use the refresh token for the revocation
await redis.setex(`refresh:${userId}:${token}`, 604800, 'valid'); // ✅
// Use the httpOnly cookie
res.cookie('token', token, { httpOnly: true, secure: true, sameSite: 'strict' }); // ✅
// Use the algorithms option
jwt.verify(token, secret, { algorithms: ['HS256'] }); // ✅
// Use the rotation
await redis.del(`refresh:${userId}:${oldToken}`); // ✅
// Use the HTTPS
// The token must not travel over the HTTP // ✅
❌ Don’t Do This:
// Don't put the sensitive data in the payload
jwt.sign({ password: user.password }, secret); // the readable // ❌
// Don't use the long-lived access token
{ expiresIn: '30d' } // the no revocation // ❌
// Don't forget the algorithms option
jwt.verify(token, secret); // the alg none attack // ❌
// Don't store the token in the localStorage
localStorage.setItem('token', token); // the XSS // ❌
// Don't use the weak secret
jwt.sign(payload, 'secret'); // the guessable // ❌
// Don't skip the expiry check
jwt.verify(token, secret, { ignoreExpiration: true }); // the danger // ❌
// Don't forget the refresh revocation
// The logout must delete the refresh token // ❌
Common Pitfalls
| The pitfall | The why | The fix |
|---|---|---|
| The token never expires | The no expiresIn | Add the expiry |
| The alg none attack | The no algorithms | Specify the algorithms |
| The XSS | The localStorage | Use the httpOnly cookie |
| The CSRF | The sameSite | Set the sameSite |
| The token revoked | The no revocation | Use the refresh token |
| The payload readable | The base64 | Do not put the secrets |
| The secret leaked | The committed | Use the environment |
Real-World Examples
1. The Sign
jwt.sign({ sub: user.id }, secret, { expiresIn: '15m' });
2. The Verify
jwt.verify(token, secret, { algorithms: ['HS256'] });
3. The Middleware
const token = req.headers.authorization?.slice(7);
const decoded = jwt.verify(token, secret);
req.user = { id: decoded.sub };
4. The Refresh
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
const accessToken = jwt.sign({ sub: decoded.sub }, secret, { expiresIn: '15m' });
5. The Logout
await redis.del(`refresh:${userId}:${token}`);
6. The Role
if (req.user.role !== 'admin') return res.status(403);
7. The Cookie
res.cookie('token', token, { httpOnly: true, secure: true, sameSite: 'strict' });
8. The Rotation
await redis.del(`refresh:${userId}:${oldToken}`);
9. The Blacklist
await redis.setex(`blacklist:${token}`, ttl, '1');
10. The Complete
const accessToken = jwt.sign({ sub: user.id }, secret, { expiresIn: '15m' });
Visual
The JWT Structure
┌──────────────────────────────────────────────────────────────┐
│ THE JWT STRUCTURE │
│ │
│ header.payload.signature │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The header: │ │
│ │ {"alg":"HS256","typ":"JWT"} │ │
│ │ → the base64url: eyJhbGciOiJIUzI1NiIs... │ │
│ │ │ │
│ │ The payload: │ │
│ │ {"sub":"1","role":"admin","exp":1516242622} │ │
│ │ → the base64url: eyJzdWIiOiIxIiwicm9... │ │
│ │ │ │
│ │ The signature: │ │
│ │ the HMAC over the header and the payload │ │
│ │ → the base64url: SflKxwRJSMeKKF2QT4f... │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The three parts join with the dots. │
│ │
└──────────────────────────────────────────────────────────────┘
The Authentication Flow
┌──────────────────────────────────────────────────────────────┐
│ THE AUTHENTICATION FLOW │
│ │
│ The login: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ POST /login { email, password } │ │
│ │ → the verify the credentials │ │
│ │ → the sign the access + the refresh │ │
│ │ → the store the refresh in the Redis │ │
│ │ → the set the httpOnly cookie │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The request: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ GET /profile │ │
│ │ Cookie: the access_token │ │
│ │ → the verify the signature │ │
│ │ → the attach the user │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The refresh: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ POST /auth/refresh │ │
│ │ → the verify the refresh token │ │
│ │ → the check the Redis │ │
│ │ → the rotate the refresh │ │
│ │ → the new access token │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The Access vs the Refresh Token
┌──────────────────────────────────────────────────────────────┐
│ THE ACCESS vs THE REFRESH TOKEN │
│ │
│ THE ACCESS TOKEN: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The lifetime: the 15 minutes │ │
│ │ The storage: the httpOnly cookie │ │
│ │ The use: the every request │ │
│ │ The revocation: the blacklist (the optional) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ THE REFRESH TOKEN: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The lifetime: the 7 days │ │
│ │ The storage: the httpOnly cookie (the /auth/refresh)│ │
│ │ The use: the only the refresh endpoint │ │
│ │ The revocation: the Redis delete │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The access token is the short and the stateless. │
│ The refresh token is the long and the revocable. │
│ │
└──────────────────────────────────────────────────────────────┘
The Refresh Rotation
┌──────────────────────────────────────────────────────────────┐
│ THE REFRESH ROTATION │
│ │
│ The use 1: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The refresh token A → the verify → the OK │ │
│ │ → the delete A from the Redis │ │
│ │ → the create B → the store B │ │
│ │ → the return B │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The attacker with the stolen A: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The refresh token A → the verify → the OK │ │
│ │ → the Redis check → the NOT FOUND │ │
│ │ → the 401 the error │ │
│ │ → the theft is the detected │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The rotation detects the stolen token. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| JWT | The JSON Web Token |
| The parts | The header, the payload, the signature |
| The claims | The sub, the iat, the exp, the iss |
| The sign | jwt.sign(payload, secret, options) |
| The verify | jwt.verify(token, secret, options) |
| The access token | The 15 minutes |
| The refresh token | The 7 days |
| The algorithm | The HS256 or the RS256 |
| The revocation | The Redis delete |
| The rotation | The new refresh on the each use |
| The cookie | The httpOnly, the secure, the sameSite |
Key takeaways:
- A JSON Web Token is the self-contained credential with the three parts: the header, the payload, and the signature. The server signs the token with the private key, and the client sends it with the every request. The server verifies the signature without the shared store, and the stateless authentication scales.
- The payload is the base64-encoded, not the encrypted. The anyone can decode the payload and read the claims. The signature prevents the tampering, not the reading. The sensitive data must not go into the payload.
- The access token is the short-lived, and the refresh token is the long-lived. The access token is the 15 minutes, and the refresh token is the 7 days. The access token expiry limits the damage, and the refresh token is the revocable.
- The refresh token is the revocation point. The Redis stores the refresh token, and the logout deletes it. The access token cannot be revoked before the expiry, but the refresh token can.
- The rotation detects the stolen refresh token. The new refresh token is issued on the each use, and the old is deleted. The attacker with the stolen token fails the Redis check.
- The
httpOnlycookie prevents the XSS. The JavaScript cannot read thehttpOnlycookie, and the token is the safer. Thesecurerequires the HTTPS, and thesameSiteprevents the CSRF. - The
algorithmsoption prevents the alg none attack. Thejwt.verify(token, secret, { algorithms: ['HS256'] })ensures the only the HMAC is the accepted. - The result is the stateless, the scalable, and the secure authentication. The JWT is the portable, and the refresh token is the revocable. The result is the modern authentication, which is the foundation of the API security.
Remember: A JWT is the self-contained credential with the three parts. The server signs, and the client sends. The stateless verification scales. The access token is the short, and the refresh token is the long and the revocable. The rotation detects the theft. The result is the stateless authentication, which is the foundation of the modern API.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!