FastifyMiddleCoding

Как реализовать аутентификацию в Fastify с помощью fastify-jwt или @fastify/jwt?

@fastify/jwt регистрируется с секретом, добавляет методы app.jwt.sign() и request.jwtVerify(). Защищённые роуты вызывают request.jwtVerify() в preHandler-хуке; публичные роуты пропускают хук.

Установка

npm install @fastify/jwt

Регистрация плагина

import Fastify from 'fastify';
import jwt from '@fastify/jwt';

const app = Fastify({ logger: true });

await app.register(jwt, {
  secret: process.env.JWT_SECRET, // минимум 32 символа
  sign: {
    expiresIn: '15m', // access-token живёт 15 минут
  },
  // Для RS256 передайте { private: '...', public: '...' }
});

Декоратор authenticate

Создайте переиспользуемый декоратор, который будет применяться как preHandler на защищённых роутах:

app.decorate('authenticate', async (request, reply) => {
  try {
    await request.jwtVerify();
  } catch (err) {
    reply.code(401).send({ error: 'Unauthorized', message: err.message });
  }
});

Роуты: логин и защищённый эндпоинт

// POST /auth/login — выдача токена
app.post('/auth/login', async (request, reply) => {
  const { email, password } = request.body;

  // Проверка пользователя из БД (упрощённо)
  const user = await findUserByEmail(email);
  if (!user || !(await verifyPassword(password, user.passwordHash))) {
    return reply.code(401).send({ error: 'Invalid credentials' });
  }

  const token = app.jwt.sign(
    { sub: user.id, email: user.email, role: user.role },
    { expiresIn: '15m' }
  );

  return { accessToken: token };
});

// GET /profile — только для аутентифицированных
app.get(
  '/profile',
  { preHandler: [app.authenticate] },
  async (request) => {
    // request.user содержит декодированный payload
    const { sub, email, role } = request.user;
    return { userId: sub, email, role };
  }
);

Refresh-токены

Access-токен намеренно короткоживущий. Refresh-токен выдаётся с большим TTL и хранится в httpOnly-куке:

app.post('/auth/login', async (request, reply) => {
  const user = await authenticateUser(request.body);

  const accessToken = app.jwt.sign(
    { sub: user.id, role: user.role },
    { expiresIn: '15m' }
  );

  const refreshToken = app.jwt.sign(
    { sub: user.id, type: 'refresh' },
    { expiresIn: '7d' }
  );

  reply
    .setCookie('refreshToken', refreshToken, {
      httpOnly: true,
      secure: true,
      sameSite: 'strict',
      path: '/auth/refresh',
      maxAge: 7 * 24 * 3600,
    })
    .send({ accessToken });
});

app.post('/auth/refresh', async (request, reply) => {
  const token = request.cookies?.refreshToken;
  if (!token) return reply.code(401).send({ error: 'No refresh token' });

  try {
    const payload = app.jwt.verify(token);
    if (payload.type !== 'refresh') throw new Error('Wrong token type');

    const newAccess = app.jwt.sign(
      { sub: payload.sub },
      { expiresIn: '15m' }
    );
    return { accessToken: newAccess };
  } catch {
    return reply.code(401).send({ error: 'Invalid refresh token' });
  }
});

TypeScript: расширение типа request.user

declare module '@fastify/jwt' {
  interface FastifyJWT {
    payload: { sub: string; email: string; role: string };
    user: { sub: string; email: string; role: string };
  }
}

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

  • Слабый секрет — строки вроде "secret" или "password" брутфорсятся мгновенно. Используйте минимум 256-битный случайный ключ (openssl rand -hex 32).
  • HS256 vs RS256 — HS256 требует хранить один секрет на всех сервисах. Если токены проверяют несколько сервисов, используйте RS256: публичный ключ можно раздать свободно.
  • Отсутствие инвалидации токенов — JWT stateless; отозвать конкретный токен без redis-списка отозванных невозможно. Планируйте механизм revoke заранее.
  • Хранение в localStorage — уязвимо к XSS. Access-токен лучше хранить в памяти JS, refresh-токен — в httpOnly-куке.
  • request.jwtVerify() проверяет только Bearer-заголовок по умолчанию — если токен приходит в куке, передайте { onlyCookie: true } или настройте extractToken.
  • Отсутствие проверки role/scopejwtVerify лишь валидирует подпись и срок; авторизацию по роли пишите отдельно.
  • Время жизни access-токена слишком велико — токен на 24 часа сводит на нет смысл JWT при компрометации. Держите access TTL ≤15–30 минут.
  • Отладка без clock skew — если сервер и клиент имеют рассинхронизированные часы, токен может считаться истёкшим. Используйте опцию clockTolerance (например, 30 секунд).

Common mistakes

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

What the interviewer is testing

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

Sources

Related topics