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-риски: безопасность, отказоустойчивость, логирование и тесты.