What JWT Actually Is
JWT (JSON Web Token) is a compact, URL-safe way to transmit claims between two parties. It’s not a magic security solution. It’s a token format: three base64url-encoded segments separated by dots (header.payload.signature).
header: {"alg":"HS256","typ":"JWT"}
payload: {"sub":"user123","iat":1700000000}
signature: HMACSHA256(base64url(header)+"."+base64url(payload), secret)
Enter fullscreen mode Exit fullscreen mode
The Common Mistake: Storing JWT in localStorage
Don’t do it. localStorage is accessible via JavaScript (XSS). If an attacker injects a script, they steal your tokens. Use httpOnly cookies instead. The cookie is sent automatically with requests and is not readable by JS.
// Bad: vulnerable to XSS
localStorage.setItem('token', jwt);
// Better: httpOnly cookie (set by server)
Set-Cookie: token=<jwt>; HttpOnly; Secure; SameSite=Strict
Enter fullscreen mode Exit fullscreen mode
How to Implement JWT Auth (The Practical Way)
1. Login endpoint issues a JWT in a cookie
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = authenticate(username, password);
if (!user) return res.status(401).send('Invalid credentials');
const token = jwt.sign(
{ sub: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
res.cookie('token', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 15 * 60 * 1000
});
res.json({ message: 'Logged in' });
});
Enter fullscreen mode Exit fullscreen mode
2. Middleware verifies the token on each request
function authMiddleware(req, res, next) {
const token = req.cookies.token;
if (!token) return res.status(401).send('No token');
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
return res.status(403).send('Invalid token');
}
}
app.get('/protected', authMiddleware, (req, res) => {
res.json({ user: req.user });
});
Enter fullscreen mode Exit fullscreen mode
3. Refresh tokens for longer sessions
Access tokens are short-lived (15 min). Use a refresh token (long-lived, stored in a separate httpOnly cookie) to get new access tokens without asking for credentials again.
app.post('/refresh', (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) return res.status(401).send('No refresh token');
try {
const decoded = jwt.verify(refreshToken, process.env.REFRESH_SECRET);
const newAccessToken = jwt.sign(
{ sub: decoded.sub },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
res.cookie('token', newAccessToken, { httpOnly: true, secure: true, sameSite: 'strict' });
res.json({ message: 'Token refreshed' });
} catch (err) {
return res.status(403).send('Invalid refresh token');
}
});
Enter fullscreen mode Exit fullscreen mode
What JWT Does NOT Solve
- CSRF: If you use cookies, you need CSRF protection. Use SameSite=Strict or CSRF tokens.
- Token revocation: JWT is stateless. You can’t revoke a single token unless you maintain a blacklist (which defeats statelessness). For logout, clear the cookie on the server side or use short expiry.
- Payload tampering: The signature ensures integrity. Never trust the payload without verifying the signature.
Common Pitfalls
- Storing secrets in code: Use environment variables.
-
Using weak secrets: Use a long, random string (e.g.,
openssl rand -hex 64). - Not validating algorithms: If your server accepts ‘none’ algorithm, attackers can forge tokens. Always specify allowed algorithms.
// Explicitly set algorithms
jwt.verify(token, secret, { algorithms: ['HS256'] });
Enter fullscreen mode Exit fullscreen mode
Summary
JWT is a tool, not a silver bullet. Use httpOnly cookies, short-lived tokens, refresh tokens, and proper CSRF protection. Understand the tradeoffs: statelessness vs. revocation. Keep it simple.
답글 남기기