Реалізація JWT аутентифікації для веб-застосунку

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.
Розробка та обслуговування будь-яких видів сайтів:
Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація JWT аутентифікації для веб-застосунку
Середня
~2-3 робочих дні
Часті питання
Наші компетенції:
Етапи розробки
Останні роботи
  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Впровадження JWT аутентифікації для веб-додатків

JWT (JSON Web Token) — компактний токен, що містить підписаний набір claims. На відміну від підходу на основі сесій, сервер не зберігає стан — усе необхідне вбудовано в сам токен. Горизонтальне масштабування без sticky sessions, без звернень до бази на кожному запиті.

Структура JWT

Токен складається з трьох частин, закодованих base64url, розділених крапкою:

header.payload.signature

// Декодований header:
{ "alg": "RS256", "typ": "JWT" }

// Декодований payload:
{
  "sub": "user_42",        // subject — ID користувача
  "iss": "api.example.com", // issuer
  "aud": "app.example.com", // audience
  "iat": 1735600000,        // issued at
  "exp": 1735686400,        // expires at (24 години)
  "jti": "uuid-v4",         // JWT ID для відкликання
  "roles": ["admin"],
  "plan": "pro"
}

Підпис — HMAC-SHA256 (HS256) або RSA/ECDSA (RS256/ES256). RS256 переважний: приватний ключ тільки на сервері видачі, публічний ключ можна розповсюджувати до будь-якого сервера ресурсів для верифікації.

Впровадження (Node.js + jose)

import { SignJWT, jwtVerify, generateKeyPair } from 'jose';

// Генеруємо пару ключів (один раз, зберігаємо у secrets)
const { privateKey, publicKey } = await generateKeyPair('RS256');

// Видаємо токени
async function issueTokens(userId: string, roles: string[]) {
  const now = Math.floor(Date.now() / 1000);

  const accessToken = await new SignJWT({ roles, plan: 'pro' })
    .setProtectedHeader({ alg: 'RS256' })
    .setSubject(userId)
    .setIssuedAt(now)
    .setExpirationTime('15m')  // короткоживущий
    .setJti(crypto.randomUUID())
    .sign(privateKey);

  const refreshToken = await new SignJWT({})
    .setProtectedHeader({ alg: 'RS256' })
    .setSubject(userId)
    .setIssuedAt(now)
    .setExpirationTime('30d')  // довгоживущий
    .setJti(crypto.randomUUID())
    .sign(privateKey);

  return { accessToken, refreshToken };
}

// Верифікація
async function verifyToken(token: string) {
  const { payload } = await jwtVerify(token, publicKey, {
    issuer: 'api.example.com',
    audience: 'app.example.com',
  });
  return payload;
}

Стратегія Access Token + Refresh Token

Access token живе 15 хвилин — мінімальне вікно при компрометації. Refresh token — 30 днів, зберігається безпечно:

// При логіні — встановлюємо refresh token у httpOnly cookie
res.cookie('refresh_token', refreshToken, {
  httpOnly: true,        // недоступний JS
  secure: true,          // тільки HTTPS
  sameSite: 'strict',    // CSRF-захист
  maxAge: 30 * 24 * 3600 * 1000,
  path: '/api/auth/refresh', // cookie надсилається ТІЛЬКИ на /refresh
});

// Access token — у пам'яті (React state або модульна змінна), НІ в localStorage
// localStorage уразливий до XSS

Endpoint оновлення:

app.post('/api/auth/refresh', async (req, res) => {
  const refreshToken = req.cookies.refresh_token;
  if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });

  const payload = await verifyToken(refreshToken);

  // Перевіряємо, чи не відкликаний токен (за jti)
  const isRevoked = await redis.get(`revoked:${payload.jti}`);
  if (isRevoked) return res.status(401).json({ error: 'Token revoked' });

  const user = await db.user.findUnique({ where: { id: payload.sub } });
  const tokens = await issueTokens(user.id, user.roles);

  // Ротуємо refresh token
  await redis.setex(`revoked:${payload.jti}`, 30 * 24 * 3600, '1');
  res.cookie('refresh_token', tokens.refreshToken, { /* ... */ });
  res.json({ access_token: tokens.accessToken });
});

Відкликання через Redis Blocklist

JWT — stateless, неможливо «відкликати» без додаткового сховища. Розв'язання — blocklist з TTL:

// Вихід
app.post('/api/auth/logout', authenticate, async (req, res) => {
  const { jti, exp } = req.jwtPayload;
  const ttl = exp - Math.floor(Date.now() / 1000);

  if (ttl > 0) {
    await redis.setex(`revoked:${jti}`, ttl, '1');
  }

  res.clearCookie('refresh_token', { path: '/api/auth/refresh' });
  res.json({ success: true });
});

// Middleware верифікації з перевіркою blocklist
async function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  const payload = await verifyToken(token);

  const isRevoked = await redis.get(`revoked:${payload.jti}`);
  if (isRevoked) return res.status(401).json({ error: 'Token revoked' });

  req.jwtPayload = payload;
  next();
}

Laravel — впровадження через tymon/jwt-auth

composer require tymon/jwt-auth
php artisan jwt:secret
// config/auth.php
'guards' => [
    'api' => ['driver' => 'jwt', 'provider' => 'users'],
],

// AuthController
public function login(LoginRequest $request)
{
    $credentials = $request->only(['email', 'password']);

    if (!$token = auth('api')->attempt($credentials)) {
        return response()->json(['error' => 'Invalid credentials'], 401);
    }

    return response()->json([
        'access_token' => $token,
        'token_type'   => 'bearer',
        'expires_in'   => auth('api')->factory()->getTTL() * 60,
    ]);
}

public function refresh()
{
    return response()->json([
        'access_token' => auth('api')->refresh(),
    ]);
}

Що НЕ класти у JWT

JWT видимий для будь-кого, хто перехопить токен (перед перевіркою підпису) — підпис гарантує цілісність, не конфіденційність. Не класти у payload:

  • Паролі, секрети
  • Дані про платежі
  • Персональні дані понад необхідне (GDPR — мінімізація даних)

Оптимальний payload: sub (user_id), roles, plan, jti, стандартні claims.

Терміни

JWT auth з RS256, access+refresh токени, httpOnly cookie для refresh, Redis revocation, logout: 3–5 днів. З автооновленням токена у React (silent refresh), підтримкою кількох пристроїв, аудит-логом сесій: 1–2 тижні.