The mental model that fixes everything
JWT is just a token format. It is not authentication, not a session, and not a database. Once you separate those ideas, most of the pain disappears.
A JWT is a JSON object that is signed. That’s it. The payload holds claims like sub (subject) and exp (expiration). The signature proves the token wasn’t tampered with.
What JWT is not
- Not a session store: You can’t revoke a JWT before it expires. If you need revocation, you need a blocklist or short expiry.
- Not a database: Don’t stuff heavy data in the payload. It gets sent on every request.
- Not a magic bullet: It’s a way to pass claims between parties without a shared server-side state.
The three flows that matter
1. Access token only
Simplest flow: login returns a JWT, client sends it in the Authorization header, server verifies it on every request.
// server middleware (Express example)
const jwt = require('jsonwebtoken');
function auth(req, res, next) {
const header = req.headers.authorization;
if (!header) return res.status(401).json({ error: 'No token' });
const token = header.split(' ')[1]; // Bearer <token>
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch (err) {
res.status(401).json({ error: 'Invalid token' });
}
}
Enter fullscreen mode Exit fullscreen mode
Works fine for small apps, but every request hits your auth logic and the token can’t be invalidated early.
2. Access + refresh token
Common pattern for SPAs. Access token lives 15 minutes, refresh token lives 7 days. The refresh token is stored securely (httpOnly cookie) and used only to get a new access token.
// issue tokens on login
const accessToken = jwt.sign({ userId }, process.env.JWT_SECRET, { expiresIn: '15m' });
const refreshToken = jwt.sign({ userId }, process.env.REFRESH_SECRET, { expiresIn: '7d' });
res.json({ accessToken });
res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: true, sameSite: 'strict' });
Enter fullscreen mode Exit fullscreen mode
Refresh endpoint:
app.post('/refresh', (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) return res.sendStatus(401);
try {
const payload = jwt.verify(refreshToken, process.env.REFRESH_SECRET);
const newAccessToken = jwt.sign({ userId: payload.userId }, process.env.JWT_SECRET, { expiresIn: '15m' });
res.json({ accessToken: newAccessToken });
} catch {
res.clearCookie('refreshToken');
res.sendStatus(403);
}
});
Enter fullscreen mode Exit fullscreen mode
This gives you short-lived access tokens (less risk if leaked) and long-lived sessions without storing server-side state.
3. Stateless vs stateful
If you need to revoke tokens immediately (like on password change), you have two options:
- Keep a token version in your user table. Include
versionin the JWT payload. Bump the version to invalidate all old tokens. - Use a blocklist (Redis or DB) for revoked tokens. Check the blocklist before verifying.
Both add state. If you don’t need revocation, stay stateless.
Common mistakes I see
- Storing JWT in localStorage: XSS can steal it. Use httpOnly cookies for refresh tokens, and keep access tokens in memory if possible.
- Putting sensitive data in payload: It’s base64 encoded, not encrypted. Anyone can read it.
- Using the same secret for access and refresh: Use separate secrets. If one leaks, the other is still safe.
-
Not checking
exp: Most libraries do it automatically, but if you hand-roll, don’t forget.
The decision checklist
Ask these before adding JWT:
- Do you need to revoke tokens? If yes, plan for a blocklist or versioning.
- Are you building an API for multiple clients? JWT works well.
- Is your server the only consumer? A simple session cookie might be easier.
Final thought
JWT is a tool, not a religion. Use it when it fits: stateless APIs, microservices, or cross-domain auth. For classic server-rendered apps, traditional sessions are often simpler. The confusion comes from mixing the token format with the auth strategy. Keep them separate and you’ll be fine.
답글 남기기
댓글을 달기 위해서는 로그인해야합니다.