Express.js: middleware для верификации токенов

Middleware в Express.js для проверки токенов

В HTTP-приложениях на базе Express.js проверка доступа обычно реализуется через промежуточные функции, которые перехватывают запрос до выполнения бизнес-логики маршрута. В контексте токенов основная задача такого слоя сводится к нескольким операциям:

  • извлечение токена из заголовка запроса
  • проверка его подлинности и целостности
  • декодирование полезной нагрузки
  • передача информации о пользователе дальше по цепочке обработки запроса
  • блокировка доступа при невалидных данных

Middleware становится точкой централизованного контроля безопасности, исключая дублирование логики в каждом роуте.


Заголовок Authorization и схема Bearer

Стандартным способом передачи токена в HTTP является заголовок:

Authorization: Bearer <token>

В middleware первым шагом выполняется разбор строки:

  • проверка наличия заголовка
  • проверка схемы Bearer
  • выделение самого токена

Отсутствие заголовка или некорректный формат рассматриваются как ошибка авторизации без перехода к криптографической проверке.


JWT как основа токенов доступа

JSON Web Token представляет собой компактный формат передачи данных между сторонами. Структура состоит из трёх частей:

  • header — алгоритм подписи и метаданные
  • payload — полезная нагрузка (id пользователя, роли, сроки действия)
  • signature — криптографическая подпись

Основная цель middleware — проверить signature и валидность payload, включая срок действия (exp).


Библиотека jose и работа с JWT

В современных Node.js приложениях предпочтение часто отдаётся библиотеке jose, которая реализует стандарты JOSE (JWS, JWE, JWT) без устаревших зависимостей.

Ключевые возможности:

  • проверка JWT (JWS)
  • поддержка HMAC и RSA/ECDSA алгоритмов
  • работа с JWKS (JSON Web Key Set)
  • строгая реализация стандартов RFC

Базовый метод проверки токена:

import { jwtVerify } from 'jose';

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

Типовая структура middleware в Express.js:

import { jwtVerify } from 'jose';

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

export async function authMiddleware(req, res, next) {
    const header = req.headers.authorization;

    if (!header) {
        return res.status(401).json({ message: 'Отсутствует Authorization заголовок' });
    }

    const [type, token] = header.split(' ');

    if (type !== 'Bearer' || !token) {
        return res.status(401).json({ message: 'Некорректный формат токена' });
    }

    try {
        const { payload } = await jwtVerify(token, secret);

        req.user = payload;

        next();
    } catch (err) {
        return res.status(401).json({ message: 'Недействительный токен' });
    }
}

В результате успешной проверки объект payload становится доступным через req.user, что позволяет использовать данные пользователя в последующих middleware и контроллерах.


Алгоритм HS256 и симметричная проверка

При использовании HMAC (HS256) один и тот же секрет применяется для подписи и проверки токена.

const secret = new TextEncoder().encode('super-secret-key');

const { payload } = await jwtVerify(token, secret, {
    algorithms: ['HS256']
});

Особенности:

  • простота реализации
  • высокая скорость
  • необходимость защищённого хранения секрета
  • невозможность масштабирования между независимыми сервисами без общего секрета

Алгоритм RS256 и асимметричная криптография

При использовании RSA модель меняется: приватный ключ подписывает токен, публичный — проверяет.

import { jwtVerify, importSPKI } from 'jose';

const publicKey = await importSPKI(process.env.PUBLIC_KEY, 'RS256');

const { payload } = await jwtVerify(token, publicKey, {
    algorithms: ['RS256']
});

Преимущества:

  • разделение ролей (auth server и resource server)
  • безопасное распространение публичного ключа
  • возможность масштабирования архитектуры

Проверка токенов через JWKS

В распределённых системах ключи часто ротируются, поэтому используется JWKS endpoint.

import { createRemoteJWKSet, jwtVerify } from 'jose';

const JWKS = createRemoteJWKSet(
    new URL('https://auth.example.com/.well-known/jwks.json')
);

export async function authMiddleware(req, res, next) {
    const header = req.headers.authorization;

    if (!header) {
        return res.status(401).end();
    }

    const token = header.split(' ')[1];

    try {
        const { payload } = await jwtVerify(token, JWKS);
        req.user = payload;
        next();
    } catch {
        return res.status(401).end();
    }
}

JWKS обеспечивает:

  • автоматическое обновление ключей
  • поддержку ротации без перезапуска сервиса
  • централизованное управление подписью токенов

Обогащение запроса пользовательскими данными

После успешной верификации middleware обычно добавляет объект пользователя:

req.user = {
    id: payload.sub,
    email: payload.email,
    roles: payload.roles
};

Это позволяет downstream-слою не выполнять повторную проверку токена.


Контроль доступа на основе ролей

Middleware часто расширяется логикой авторизации:

export function requireRole(role) {
    return (req, res, next) => {
        if (!req.user?.roles?.includes(role)) {
            return res.status(403).json({ message: 'Доступ запрещён' });
        }
        next();
    };
}

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

app.get('/admin', authMiddleware, requireRole('admin'), handler);

Обработка ошибок и истечение срока действия

JWT содержит поле exp, которое автоматически проверяется библиотекой jose. При истечении срока действия выбрасывается ошибка, которая должна приводить к ответу:

  • 401 Unauthorized — токен недействителен или истёк
  • 403 Forbidden — недостаточно прав при валидном токене

Типизация ошибок позволяет отделять просроченные токены от поддельных или повреждённых.


Безопасность и практические ограничения

При проектировании middleware необходимо учитывать ряд факторов:

  • токен не должен храниться в localStorage в браузере при высоких требованиях к безопасности
  • секреты и приватные ключи не должны попадать в кодовую базу
  • обязательно ограничение алгоритмов через параметр algorithms
  • проверка iss (issuer) и aud (audience) снижает риск подделки токенов
  • короткое время жизни access token уменьшает последствия компрометации

Проверка issuer и audience

await jwtVerify(token, JWKS, {
    issuer: 'https://auth.example.com',
    audience: 'api.service.local'
});

Эти параметры добавляют дополнительный уровень контроля над тем, кем и для кого был выпущен токен.


Интеграция middleware в архитектуру Express.js

В реальных приложениях middleware обычно выносится в отдельный слой и подключается глобально или выборочно:

app.use('/api', authMiddleware);

или точечно:

app.get('/profile', authMiddleware, getProfile);

Такая организация позволяет разделять публичные и защищённые маршруты без дублирования логики проверки.


Производительность и кэширование ключей

При использовании JWKS важно учитывать:

  • кэширование публичных ключей снижает нагрузку на сеть
  • повторное использование JWKSet внутри процесса ускоряет верификацию
  • ограничение частоты запросов к auth-серверу предотвращает деградацию производительности

Библиотека jose автоматически включает механизмы кэширования при использовании createRemoteJWKSet, но в крупных системах часто добавляется дополнительный слой кэша на уровне инфраструктуры.