Understanding JWT Authentication

JWTs are everywhere in modern web auth. Here's how they work and how to use them properly.

What is a JWT?

JSON Web Token. A signed string that contains claims (data) you can trust.

Structure: `header.payload.signature`

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VySWQiOiIxMjMiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.
signature_here

How It Works

1. User logs in with credentials 2. Server verifies credentials 3. Server creates JWT with user data 4. Server sends JWT to client 5. Client stores JWT 6. Client sends JWT with each request 7. Server verifies signature, trusts the data

Creating JWTs

const jwt = require('jsonwebtoken');

const SECRET = process.env.JWT_SECRET; // Keep this secret!

// Create token
const token = jwt.sign(
  { userId: user.id, email: user.email },
  SECRET,
  { expiresIn: '24h' }
);

// Verify token
try {
  const decoded = jwt.verify(token, SECRET);
  console.log(decoded.userId); // "123"
} catch (err) {
  console.log('Invalid token');
}

Express Middleware

function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // "Bearer TOKEN"
  
  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }
  
  jwt.verify(token, SECRET, (err, decoded) => {
    if (err) {
      return res.status(403).json({ error: 'Invalid token' });
    }
    req.user = decoded;
    next();
  });
}

// Usage
app.get('/api/profile', authenticateToken, (req, res) => {
  // req.user contains the decoded token data
  res.json({ userId: req.user.userId });
});

Storage: Where to Put the Token

Local Storage

localStorage.setItem('token', token);

Pros: Simple Cons: Vulnerable to XSS

HTTP-Only Cookie

res.cookie('token', token, {
  httpOnly: true,
  secure: true, // HTTPS only
  sameSite: 'strict'
});

Pros: Not accessible via JavaScript Cons: Requires CSRF protection

I prefer HTTP-only cookies for sensitive apps.

Common Mistakes

Storing Sensitive Data in Payload

JWTs are encoded, not encrypted. Anyone can decode the payload:

// Don't do this
{ password: "secret123", ssn: "123-45-6789" }

// Do this
{ userId: "123", role: "user" }

Not Setting Expiration

// Always set expiration
jwt.sign(payload, secret, { expiresIn: '24h' });

Using Weak Secrets

// ❌ Bad
const SECRET = "secret";

// ✅ Good
const SECRET = crypto.randomBytes(64).toString('hex');

Not Handling Expiration

// Check if token is expired
jwt.verify(token, SECRET, (err, decoded) => {
  if (err?.name === 'TokenExpiredError') {
    // Token expired, refresh or re-authenticate
  }
});

Refresh Tokens

For better security, use short-lived access tokens with refresh tokens:

1. Access token: 15 minutes 2. Refresh token: 7 days, stored securely 3. When access token expires, use refresh token to get new one

More complex but more secure.

Back to Journal