正在加载,请稍候…

Node.js Authentication: JWT vs Sessions — Complete Implementation Guide

Implement secure authentication in Node.js — stateless JWT with refresh tokens, stateful sessions with Redis, refresh token rotation, PKCE flow, and security best practices.

JWT vs Sessions: When to Use What

Neither is universally better — the right choice depends on your architecture.

JWT Sessions
Storage Client (localStorage/cookie) Server (Redis/DB)
Scalability Horizontal (stateless) Needs shared store
Revocation Hard (short TTL + blacklist) Easy (delete from store)
Payload Carries data Just an ID
Mobile-friendly Yes Requires cookies

Secure JWT Implementation

// auth/jwt.js
import jwt from 'jsonwebtoken';
import crypto from 'crypto';

const ACCESS_TOKEN_TTL = '15m';
const REFRESH_TOKEN_TTL = '7d';

export function generateTokenPair(userId, role) {
  const accessToken = jwt.sign(
    { sub: userId, role, type: 'access' },
    process.env.JWT_ACCESS_SECRET,
    { expiresIn: ACCESS_TOKEN_TTL, algorithm: 'HS256' }
  );
  
  const refreshToken = jwt.sign(
    { sub: userId, jti: crypto.randomUUID(), type: 'refresh' },
    process.env.JWT_REFRESH_SECRET,
    { expiresIn: REFRESH_TOKEN_TTL, algorithm: 'HS256' }
  );
  
  return { accessToken, refreshToken };
}

export function verifyAccessToken(token) {
  return jwt.verify(token, process.env.JWT_ACCESS_SECRET);
}

export function verifyRefreshToken(token) {
  return jwt.verify(token, process.env.JWT_REFRESH_SECRET);
}

Refresh Token Rotation

// routes/auth.js
import { redis } from '../lib/redis.js';

app.post('/auth/refresh', async (req, res) => {
  const { refreshToken } = req.cookies;
  if (!refreshToken) {
    return res.status(401).json({ error: 'No refresh token' });
  }
  
  try {
    const payload = verifyRefreshToken(refreshToken);
    const { sub: userId, jti } = payload;
    
    // Check if token was revoked (used or blacklisted)
    const isRevoked = await redis.get(`revoked:${jti}`);
    if (isRevoked) {
      // Possible token theft — invalidate ALL refresh tokens for user
      await redis.del(`user:${userId}:refresh_tokens`);
      return res.status(401).json({ error: 'Token reuse detected' });
    }
    
    // Revoke the used refresh token
    await redis.set(
      `revoked:${jti}`,
      '1',
      'EX',
      7 * 24 * 60 * 60 // 7 days
    );
    
    // Issue new token pair
    const { accessToken, refreshToken: newRefreshToken } = generateTokenPair(userId);
    
    res.cookie('refreshToken', newRefreshToken, {
      httpOnly: true,
      secure: true,
      sameSite: 'strict',
      maxAge: 7 * 24 * 60 * 60 * 1000,
    });
    
    return res.json({ accessToken });
  } catch (err) {
    return res.status(401).json({ error: 'Invalid refresh token' });
  }
});

Auth Middleware

export function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing token' });
  }
  
  const token = authHeader.slice(7);
  try {
    const payload = verifyAccessToken(token);
    req.user = { id: payload.sub, role: payload.role };
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
    }
    return res.status(401).json({ error: 'Invalid token' });
  }
}

// Role-based access
export function requireRole(...roles) {
  return (req, res, next) => {
    if (!roles.includes(req.user?.role)) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    next();
  };
}

Session-Based Auth with Redis

import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';

const redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect();

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  name: 'sid',
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 24 * 60 * 60 * 1000,
  },
}));

// Login
app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await verifyCredentials(email, password);
  
  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  // Regenerate session to prevent session fixation
  req.session.regenerate((err) => {
    if (err) return next(err);
    req.session.userId = user.id;
    req.session.role = user.role;
    res.json({ message: 'Logged in' });
  });
});

// Logout
app.post('/logout', (req, res) => {
  req.session.destroy((err) => {
    res.clearCookie('sid');
    res.json({ message: 'Logged out' });
  });
});

Password Hashing with Argon2

import argon2 from 'argon2';

// Hash password on register
export async function hashPassword(password) {
  return argon2.hash(password, {
    type: argon2.argon2id,
    memoryCost: 65536, // 64 MB
    timeCost: 3,
    parallelism: 4,
  });
}

// Verify on login
export async function verifyPassword(hash, password) {
  return argon2.verify(hash, password);
}

Security Checklist

  • Use httpOnly + secure + sameSite cookies for refresh tokens
  • Rotate refresh tokens on every use
  • Detect token reuse (token theft)
  • Use Argon2id for password hashing
  • Rate limit auth endpoints (5 attempts / 15 min)
  • Log all auth events for audit
  • Store minimal data in JWT payload
  • Use short-lived access tokens (15min)
  • Implement logout (token blacklist or session delete)