Day 1 of 5
⏱ ~60 minutes
Auth and Security for Web Apps — Day 1

JWT Authentication: Stateless, Secure, and Scalable

JSON Web Tokens are the standard for API authentication in 2026. Day 1 covers how JWTs work, how to implement them correctly, and the common mistakes that lead to security vulnerabilities.

How JWT Authentication Works

A JWT is a signed token containing claims (user ID, email, roles). The server creates it on login and the client sends it with every request. The server verifies the signature — no database lookup required.

JWT Structure
# A JWT looks like:
# eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsImV4cCI6MTcwMH0.abc123

# Three parts, base64-encoded and separated by dots:
# HEADER.PAYLOAD.SIGNATURE

# Header: {"alg": "HS256", "typ": "JWT"}
# Payload: {"userId": 1, "exp": 1700000000}
# Signature: HMAC-SHA256(header + "." + payload, SECRET_KEY)
Terminal
npm install jsonwebtoken bcrypt express
auth.js — Login and JWT Issuance
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

const JWT_SECRET = process.env.JWT_SECRET; // Long random string
const JWT_EXPIRY = '15m'; // Short-lived access tokens

// Login endpoint
app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  
  // Look up user
  const user = await User.findByEmail(email);
  if (!user) return res.status(401).json({ error: 'Invalid credentials' });
  
  // Verify password (bcrypt compares hash)
  const valid = await bcrypt.compare(password, user.passwordHash);
  if (!valid) return res.status(401).json({ error: 'Invalid credentials' });
  
  // Issue JWT
  const token = jwt.sign(
    { userId: user.id, email: user.email, role: user.role },
    JWT_SECRET,
    { expiresIn: JWT_EXPIRY }
  );
  
  res.json({ token });
});

// Authentication middleware
function requireAuth(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }
  
  const token = authHeader.slice(7);
  
  try {
    const decoded = jwt.verify(token, JWT_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' });
    }
    return res.status(401).json({ error: 'Invalid token' });
  }
}

// Protected route
app.get('/profile', requireAuth, (req, res) => {
  res.json({ user: req.user });
});
⚠️
Critical security rules for JWTs:
• Never store JWTs in localStorage — use httpOnly cookies for web apps
• Use short expiry times (15 min) with refresh tokens for long sessions
• The JWT_SECRET must be a long random string (32+ chars), never hardcoded
• Never put sensitive data (passwords, SSN) in the payload — it is base64, not encrypted
Day 1 Exercise
Build a JWT Auth Flow
  1. Create a simple Express app with the login endpoint and requireAuth middleware.
  2. Test the login endpoint — verify you get a JWT back.
  3. Decode the JWT at jwt.io and read the payload.
  4. Call a protected endpoint with and without the token.
  5. Test with an expired token (set expiry to 1s in testing).

Day 1 Summary

  • JWTs contain signed claims — the server verifies the signature without a database lookup.
  • Use short expiry times (15 min) and refresh tokens for long sessions.
  • Never store JWTs in localStorage for web apps — httpOnly cookies are more secure.
  • The signature makes JWTs tamper-evident, not encrypted — do not put secrets in the payload.
Finished this lesson?