Перейти к содержимому

JWT / Session / OAuth 2.0 / OIDC

Выбор механизма аутентификации — архитектурное решение с trade-offs между stateless (JWT) и stateful (session) подходами, делегированным доступом (OAuth 2.0) и федеративной идентификацией (OIDC); уязвимости в реализации JWT — постоянная тема security-интервью.

// ATTACK: JWT без корректной верификации — алгоритм confusion + "none" атака
// Структура JWT: base64url(header).base64url(payload).signature
// Декодирование без проверки подписи:
const [h64, p64] = token.split('.');
const header = JSON.parse(atob(h64.replace(/-/g,'+').replace(/_/g,'/')));
const payload = JSON.parse(atob(p64.replace(/-/g,'+').replace(/_/g,'/')));
// ❌ Уязвимо: доверяем payload без проверки подписи
if (payload.role === 'admin') grantAccess();
// ❌ Алгоритм "none" (CVE-2015-9235):
// Атакующий меняет header: { "alg": "none" } и убирает подпись
// Некоторые библиотеки принимают такой токен как валидный!
// ❌ Algorithm confusion (RS256 → HS256):
// RS256: подпись RSA private key, верификация public key
// Атака: поменять alg на HS256, подписать публичным ключом как HMAC-secret
// Сервер верифицирует HS256 с "публичным ключом" → принимает подделанный токен!
jwt.verify(token, publicKey); // ❌ не передаём { algorithms }
// MITIGATION: безопасная верификация JWT
import jwt from 'jsonwebtoken';
// ✅ Всегда явно указывать allowed algorithms
function verifyAccessToken(token) {
return jwt.verify(token, process.env.JWT_PUBLIC_KEY, {
algorithms: ['RS256'], // запрещаем "none" и HS256
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
});
}
// ✅ Хранение токенов
// Access token (TTL 15 мин): httpOnly cookie или in-memory (React state/Ref)
// НЕ в localStorage — XSS-скрипт прочитает его
// res.cookie('access_token', token, {
// httpOnly: true, secure: true, sameSite: 'strict', maxAge: 900_000
// });
// ✅ Refresh Token Rotation
// Refresh token (TTL 7-30 дней): httpOnly cookie, one-time use
// При использовании refresh token → выдать новый access + новый refresh
// Если refresh token использован повторно → Reuse Detection → инвалидировать всю семью
// JWT vs Session:
// JWT: stateless, горизонтальное масштабирование, но нельзя инвалидировать до exp
// Session (Redis): stateful, мгновенный logout/ban, но нужен shared store
// OAuth 2.0 PKCE flow (Authorization Code + Proof Key for Code Exchange)
// Для SPA и мобильных: нет client_secret (нельзя хранить безопасно)
// 1. Генерируем code_verifier (случайная строка) и code_challenge
const verifier = crypto.getRandomValues(new Uint8Array(32));
const verifierB64 = btoa(String.fromCharCode(...verifier))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
const hashBuf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifierB64));
const challenge = btoa(String.fromCharCode(...new Uint8Array(hashBuf)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
sessionStorage.setItem('pkce_verifier', verifierB64);
// 2. Редирект на Authorization Server
const url = new URL('https://auth.example.com/authorize');
url.searchParams.set('response_type', 'code');
url.searchParams.set('client_id', CLIENT_ID);
url.searchParams.set('redirect_uri', REDIRECT_URI);
url.searchParams.set('code_challenge', challenge);
url.searchParams.set('code_challenge_method', 'S256');
url.searchParams.set('scope', 'openid profile email'); // OIDC scopes
url.searchParams.set('state', randomState); // CSRF-защита
location.href = url.toString();
// 3. Callback: обмен code → tokens (verifier доказывает, что мы инициатор)
const tokens = await fetch('https://auth.example.com/token', {
method: 'POST',
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authCode,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
code_verifier: sessionStorage.getItem('pkce_verifier'),
}),
}).then(r => r.json());
// tokens.id_token → OIDC: JWT с sub, email, name — идентификация пользователя
// tokens.access_token → OAuth2: для API-запросов (Bearer token)

Итог: JWT stateless, не инвалидируется до exp — используйте короткий TTL + refresh rotation; OAuth 2.0 PKCE для делегированного доступа; OIDC id_token для идентификации.