Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке JWT і як він працює?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**JWT (JSON Web Token)** - це компактний підписаний токен, який несе дані про користувача в JSON і дозволяє серверу перевіряти особу без запиту до бази даних. ```javascript const token = jwt.sign({ userId: 1 }, secret, { expiresIn: '15m' }); jwt.verify(token, secret); // { userId: 1, iat: ..., exp: ... } ``` **Головне:** JWT підписаний, але не зашифрований. Payload може прочитати будь-хто, але валідний підпис може створити тільки власник секрету.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.