Skip to main content

Що таке JWT і як він працює?

JWT (JSON Web Token) - це компактний самодостатній токен, який кодує дані про користувача в JSON, підписує їх криптографічно і дозволяє серверу перевіряти особу без запиту до бази даних.

Теорія

TL;DR

  • JWT схожий на запечатаний паспорт: дані видно всім, але будь-яка зміна ламає криптографічний підпис
  • Автентифікація без збереження стану: немає запиту до БД на кожен запит, на відміну від сесій
  • Три частини у Base64Url, з'єднані крапками: header.payload.signature
  • Payload читається будь-ким. JWT підписаний, але не зашифрований
  • JWT - для API і мікросервісів; сесії - коли потрібне миттєве скасування доступу

Швидкий приклад

javascript
const jwt = require('jsonwebtoken'); // Створення токена при логіні const token = jwt.sign( { userId: 123, role: 'admin' }, process.env.JWT_SECRET, { expiresIn: '15m' } ); // eyJhbGciOiJIUzI1NiJ9... // Перевірка на кожному захищеному запиті jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => { if (err) return res.status(401).json({ error: 'Unauthorized' }); // decoded = { userId: 123, role: 'admin', iat: ..., exp: ... } });

sign створює токен. verify перераховує підпис і порівнює байт за байтом із третім сегментом. Зміни хоч щось у payload - підписи не збіжаться, і запит провалиться.

Структура JWT

JWT виглядає так: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEyM30.SflKxwRJSMeKKF2QT4fw

Три частини, дві крапки. Кожна частина кодується у Base64Url - це URL-безпечний варіант Base64 без символів + і =, щоб токен не ламався всередині HTTP-заголовків.

Header вказує алгоритм підпису:

json
{ "alg": "HS256", "typ": "JWT" }

Payload несе claims (твердження про користувача):

json
{ "sub": "123", "role": "admin", "exp": 1716239022, "iat": 1716235422 }

Стандартні claims: sub (ID користувача), iss (видавець), aud (отримувач), exp (час закінчення дії), iat (час створення), nbf (не дійсний до). Поруч можна додавати власні поля.

Signature - рівень безпеки:

HMACSHA256(base64Url(header) + "." + base64Url(payload), secret)

Сервер перераховує цей підпис на кожному запиті. Збіг - токен валідний і незмінений. Розбіжність - хтось підробив дані.

Як працює підпис

Коли ти викликаєш jwt.sign(), Node запускає HMAC-SHA256 над закодованими header і payload, використовуючи твій секретний ключ. Результат і стає третім сегментом рядка токена.

При jwt.verify() те саме обчислення виконується над отриманим header.payload. Бібліотека порівнює результат із третім сегментом через constant-time comparison, що захищає від timing-атак. Якщо exp менший за поточний Unix-час, перевірка провалюється незалежно від підпису.

Саме тому payload будь-якого JWT можна вставити на jwt.io і прочитати всі поля. Підпис не ховає дані. Він лише доводить, що дані не змінювались після видачі токена.

Головна різниця від сесій

З сесіями сервер зберігає стан: ID користувача, ролі та інші дані живуть у пам'яті або базі даних, і кожен запит викликає пошук. З JWT сервер нічого не зберігає. Токен несе всі дані, а перевірка - це чисто криптографічна операція.

Це добре масштабується горизонтально, бо будь-який екземпляр сервера може перевірити будь-який токен зі спільним секретом. Але є компроміс: токен залишається валідним до exp, навіть після логауту. Сесії дозволяють видалити стан негайно. Я бачив, як команди будували чорний список токенів у пам'яті процесу для логауту, а потім втрачали весь список при кожному деплої. Короткий строк дії разом із серверними refresh токенами - єдине рішення, яке тримається в продакшені.

Коли використовувати

  • API-only або мікросервіси: JWT (горизонтальне масштабування)
  • Один сервер зі сховищем сесій: cookies і сесії (простіше скасування)
  • Мобільні додатки або SPA: JWT (зручно додавати до HTTP-заголовків)
  • OAuth2 і OIDC потоки: JWT (стандартні claims, Auth0, Okta, AWS Cognito)
  • Довгі сесії зі скасуванням: короткі access токени плюс серверні refresh токени

Типові помилки

Чутливі дані в payload. Payload - це просто Base64. Будь-хто може вставити токен на jwt.io і прочитати кожне поле. Зберігай тільки opaque ID і ролі. Чутливі дані отримуй із БД після перевірки токена.

Неправильно:

javascript
jwt.sign({ sub: 'user', creditCard: '4111...' }, secret);

Правильно:

javascript
jwt.sign({ sub: 'user123', role: 'admin' }, secret, { expiresIn: '15m' });

Токен без expiration або зі слабким секретом.

javascript
// Неправильно: токен живе вічно, секрет легко зламати jwt.sign(payload, 'secret'); // Правильно: короткий строк дії, 256-бітний випадковий секрет jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '15m' }); // Генерація: crypto.randomBytes(32).toString('hex')

Без явного обмеження алгоритму. Специфікація JWT дозволяє alg: none, що прибирає підпис повністю. Зловмисник змінює payload і виставляє алгоритм none.

javascript
// Неправильно: приймає будь-який алгоритм, включаючи "none" jwt.verify(token, secret); // Правильно: явний whitelist jwt.verify(token, secret, { algorithms: ['HS256'] });

JWT в localStorage. Будь-який XSS-скрипт може прочитати localStorage і вкрасти токен. Використовуй httpOnly cookie або тримай токени короткоживучими з refresh-флоу.

JWT без реальної стратегії відкликання. Токен звільненого співробітника залишається валідним до exp. Рішення: access токени на 15 хвилин плюс refresh токени в Redis, що видаляються при логауті або блокуванні користувача.

Де зустрічається в реальних проектах

  • Express + passport-jwt: витягує і перевіряє Authorization: Bearer <token> у middleware
  • React/Next.js: fetch або axios додає токен до заголовка кожного API-запиту
  • Auth0, Okta, AWS Cognito: видають JWT після логіну, верифікація через JWKS endpoint
  • GraphQL + Apollo: функція context витягує і декодує JWT для resolver'ів
  • HS256 vs RS256: HS256 - симетричний (один секрет для підпису і перевірки, швидко); RS256 - асиметричний (приватний ключ підписує, публічний перевіряє, краще для мультисервісних архітектур)

Питання на співбесіді

Q: Опиши шлях JWT від логіну до API-виклику.
A: Користувач відправляє дані входу, сервер підписує payload і повертає токен. Клієнт додає його як Authorization: Bearer <token> на кожному наступному запиті. Middleware перевіряє підпис і передає claims в обробник для авторизаційної логіки.

Q: Як відкликати JWT?
A: Короткий строк дії access токена (15 хвилин) плюс серверні refresh токени в Redis. При логауті видали refresh токен. Для додаткового захисту: claim jti і чорний список для критичних операцій.

Q: Різниця між HS256 і RS256?
A: HS256 - симетричний: один і той самий секрет для підпису і перевірки. RS256 - асиметричний: приватний ключ підписує, будь-який сервіс із публічним ключем перевіряє. RS256 краще підходить коли кілька сервісів перевіряють токени і ділитись секретом між ними небезпечно.

Q: Навіщо Base64Url замість звичайного Base64?
A: Звичайний Base64 використовує +, / і =, які ламають HTTP-заголовки і URL. Base64Url замінює їх на безпечні аналоги, тому токен передається без спотворень.

Q (Senior): Спроектуй систему JWT-авторизації без downtime для мільйона користувачів.
A: Короткі access токени (5 хвилин) плюс 24-годинні refresh токени в Redis-кластері з TTL рівним часу дії refresh токена. Ротація при кожному використанні: старий видаляється, нова пара видається. Для ротації ключів - JWKS endpoint із коротким TTL кешу на сервісах-клієнтах. Моніторинг latency верифікації підписів у міру зростання Redis.

Приклади

Логін і захищений роут

javascript
const express = require('express'); const jwt = require('jsonwebtoken'); const app = express(); app.post('/login', express.json(), (req, res) => { // Замінити на реальну перевірку з БД if (req.body.email !== 'user@example.com' || req.body.password !== 'pass') { return res.status(401).json({ error: 'Invalid credentials' }); } const token = jwt.sign( { id: 1, email: req.body.email }, process.env.JWT_SECRET, { expiresIn: '15m' } ); res.json({ token }); }); app.get('/profile', (req, res) => { const token = req.headers.authorization?.split(' ')[1]; // Bearer <token> try { const decoded = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] }); res.json({ user: decoded }); } catch (err) { res.status(401).json({ error: 'Unauthorized' }); } });

/login повертає { token: "eyJ..." }. Клієнт зберігає токен і відправляє Authorization: Bearer eyJ... на кожному запиті. /profile перевіряє підпис і повертає декодований об'єкт користувача. Явна опція algorithms - це захист від атаки через alg: none.

Ротація refresh токенів (refresh token rotation)

Коли access токен закінчується, клієнту потрібен спосіб отримати новий без повторного логіну. Ротація refresh токенів вирішує це і одночасно обмежує шкоду від крадіжки токена.

javascript
const refreshTokens = new Map(); // В продакшені - Redis app.post('/refresh', express.json(), (req, res) => { const { refreshToken } = req.body; if (!refreshTokens.has(refreshToken)) { return res.status(403).json({ error: 'Invalid refresh token' }); } try { const decoded = jwt.verify(refreshToken, process.env.REFRESH_SECRET); // Ротація: старий токен видалено, видаємо нову пару refreshTokens.delete(refreshToken); const newAccess = jwt.sign( { id: decoded.id }, process.env.JWT_SECRET, { expiresIn: '15m' } ); const newRefresh = jwt.sign( { id: decoded.id }, process.env.REFRESH_SECRET, { expiresIn: '7d' } ); refreshTokens.set(newRefresh, true); res.json({ accessToken: newAccess, refreshToken: newRefresh }); } catch { res.status(403).json({ error: 'Refresh failed' }); } });

Якщо зловмисник вкраде refresh токен, він зможе використати його один раз. Але при наступній легітимній ротації вкрадений токен видаляється зі сховища і провалюється при наступній спробі. Цей патерн описаний у рекомендаціях Auth0 і Okta.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?