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/scope —
jwtVerifyлишь валидирует подпись и срок; авторизацию по роли пишите отдельно. - Время жизни 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-риски: безопасность, отказоустойчивость, логирование и тесты.