DEV Community

Stack Horizon
Stack Horizon

Posted on

JWT Auth Without the Confusion

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.

Top comments (0)