Express.jsMiddleCoding

Как реализовать JWT-аутентификацию в Express-приложении?

JWT-аутентификация в Express: генерируем токен при логине через jsonwebtoken.sign(), отправляем клиенту, в middleware проверяем подпись через jwt.verify(). Access-токен живёт 15 мин, refresh-токен — в httpOnly cookie.

Архитектура JWT-аутентификации

JWT (JSON Web Token) — самодостаточный токен, содержащий payload и подпись. Сервер не хранит сессии: при каждом запросе он только проверяет подпись токена. Стандартный подход: короткоживущий access token (15 мин) в заголовке Authorization + долгоживущий refresh token (7 дней) в httpOnly cookie.

Установка

npm install jsonwebtoken bcrypt express-async-errors

Генерация токенов при логине

// auth.router.js
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const router = express.Router();

const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET;   // минимум 32 байта
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;

function generateTokens(userId, roles) {
  const accessToken = jwt.sign(
    { sub: userId, roles },
    ACCESS_SECRET,
    { expiresIn: '15m', algorithm: 'HS256' }
  );
  const refreshToken = jwt.sign(
    { sub: userId },
    REFRESH_SECRET,
    { expiresIn: '7d', algorithm: 'HS256' }
  );
  return { accessToken, refreshToken };
}

router.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await db.findUserByEmail(email);

  if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  const { accessToken, refreshToken } = generateTokens(user.id, user.roles);

  // Refresh token только в httpOnly cookie
  res.cookie('refreshToken', refreshToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000, // 7 дней в мс
    path: '/auth/refresh',             // доступен только на endpoint обновления
  });

  res.json({ accessToken, expiresIn: 900 });
});

Middleware для проверки токена

// middleware/authenticate.js
const jwt = require('jsonwebtoken');

function authenticate(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 payload = jwt.verify(token, process.env.JWT_ACCESS_SECRET, {
      algorithms: ['HS256'],  // явно указываем алгоритм!
    });
    req.user = { id: payload.sub, roles: payload.roles };
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' });
    }
    return res.status(401).json({ error: 'Invalid token' });
  }
}

function requireRole(role) {
  return (req, res, next) => {
    if (!req.user?.roles?.includes(role)) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    next();
  };
}

module.exports = { authenticate, requireRole };

Обновление access token

router.post('/refresh', (req, res) => {
  const token = req.cookies.refreshToken;
  if (!token) return res.status(401).json({ error: 'No refresh token' });

  try {
    const payload = jwt.verify(token, process.env.JWT_REFRESH_SECRET, {
      algorithms: ['HS256'],
    });
    // Опционально: проверяем токен в allowlist в Redis
    const { accessToken } = generateTokens(payload.sub, payload.roles);
    res.json({ accessToken, expiresIn: 900 });
  } catch {
    res.clearCookie('refreshToken', { path: '/auth/refresh' });
    res.status(401).json({ error: 'Invalid refresh token' });
  }
});

router.post('/logout', (req, res) => {
  res.clearCookie('refreshToken', { path: '/auth/refresh' });
  // Если нужен blacklist: добавляем jti access-токена в Redis до истечения
  res.status(204).end();
});

Использование middleware в маршрутах

const { authenticate, requireRole } = require('./middleware/authenticate');

app.get('/api/profile', authenticate, (req, res) => {
  res.json({ userId: req.user.id });
});

app.delete('/api/users/:id', authenticate, requireRole('admin'), async (req, res) => {
  await db.deleteUser(req.params.id);
  res.status(204).end();
});

Подводные камни

  • Алгоритм none — без явного указания algorithms: ['HS256'] библиотека может принять токен с алгоритмом none и нулевой подписью.
  • Хранение access token в localStorage — доступен через XSS; лучше держать в памяти JS (переменная) и обновлять через refresh.
  • Слабый секрет — короткие секреты ломаются брутфорсом офлайн, если утёк токен. Используйте минимум 256-битные ключи из crypto.randomBytes(32).toString('hex').
  • Нет инвалидации токенов — JWT stateless, поэтому смена пароля или выход не отзывают уже выданные токены. Нужен Redis-blacklist по jti (JWT ID).
  • Чувствительные данные в payload — payload не зашифрован, только подписан; не кладите пароли, email, PII в claims.
  • refresh token в localStorage — при XSS злоумышленник получает долгоживущий токен; только httpOnly cookie.
  • Отсутствие ротации refresh token — каждый запрос на /refresh должен выдавать новый refresh token и инвалидировать старый (refresh token rotation).

Common mistakes

  • Дает общий ответ про Node.js и не называет конкретные API Express.js.
  • Не объясняет, где в lifecycle находится JWT-аутентификация.
  • Не разделяет validation, authorization, business logic и persistence.
  • Игнорирует ошибки, лимиты входных данных, observability и тестирование.

What the interviewer is testing

  • Может объяснить JWT-аутентификация на примере кода.
  • Называет ключевые API: Authorization: Bearer, jwt.verify().
  • Использует точные API Express.js, а не вымышленные hooks/decorators/methods.
  • Видит production-риски: безопасность, отказоустойчивость, логирование и тесты.

Sources

Related topics