OAuth 2.0 и JWT авторизация на TypeScript: практическое руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: почему OAuth 2.0 и JWT — стандарт современной авторизации

В современной веб-разработке безопасная аутентификация и авторизация — критически важные компоненты любого приложения. OAuth 2.0 и JWT (JSON Web Tokens) стали де-факто стандартом для построения распределенных систем. OAuth 2.0 предоставляет протокол делегированного доступа, а JWT — компактный и самодостаточный формат токена.

В этой статье мы на практических примерах на TypeScript разберем, как реализовать полный цикл авторизации: от выдачи токенов до их валидации и обновления. Вы узнаете, как защитить API-эндпоинты, реализовать refresh-токены и обработать типовые ошибки.

1. Базовая настройка проекта и установка зависимостей

Для начала создадим проект на Node.js с TypeScript. Убедитесь, что у вас установлены Node.js (версия 18+) и npm.

mkdir oauth-jwt-typescript
cd oauth-jwt-typescript
npm init -y
npm install express cors dotenv jsonwebtoken bcrypt
npm install -D typescript @types/express @types/cors @types/jsonwebtoken @types/bcrypt ts-node nodemon
npx tsc --init

Настроим tsconfig.json для работы с ES-модулями и строгими типами. В файл .env добавим секретные ключи:

ACCESS_TOKEN_SECRET=your-super-secret-access-key
REFRESH_TOKEN_SECRET=your-super-secret-refresh-key
ACCESS_TOKEN_EXPIRES_IN=15m
REFRESH_TOKEN_EXPIRES_IN=7d

2. Реализация генерации и верификации JWT токенов

Создадим утилиту src/utils/jwt.ts для работы с токенами. Мы будем использовать два типа токенов: Access Token (короткоживущий) и Refresh Token (долгоживущий).

import jwt from 'jsonwebtoken';
import dotenv from 'dotenv';

dotenv.config();

interface TokenPayload {
  userId: string;
  email: string;
  role: 'user' | 'admin';
}

export const generateAccessToken = (payload: TokenPayload): string => {
  return jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET!, {
    expiresIn: process.env.ACCESS_TOKEN_EXPIRES_IN || '15m',
  });
};

export const generateRefreshToken = (payload: TokenPayload): string => {
  return jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET!, {
    expiresIn: process.env.REFRESH_TOKEN_EXPIRES_IN || '7d',
  });
};

export const verifyAccessToken = (token: string): TokenPayload => {
  return jwt.verify(token, process.env.ACCESS_TOKEN_SECRET!) as TokenPayload;
};

export const verifyRefreshToken = (token: string): TokenPayload => {
  return jwt.verify(token, process.env.REFRESH_TOKEN_SECRET!) as TokenPayload;
};

Обратите внимание: мы используем строгую типизацию для payload. Это гарантирует, что в токене всегда будут необходимые поля.

3. Создание middleware для защиты маршрутов

Теперь реализуем middleware, который будет проверять наличие и валидность Access Token в заголовке Authorization. Создадим src/middleware/auth.ts:

import { Request, Response, NextFunction } from 'express';
import { verifyAccessToken } from '../utils/jwt';

export interface AuthRequest extends Request {
  user?: {
    userId: string;
    email: string;
    role: 'user' | 'admin';
  };
}

export const authenticate = (req: AuthRequest, res: Response, next: NextFunction): void => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN

  if (!token) {
    res.status(401).json({ error: 'Access token is required' });
    return;
  }

  try {
    const decoded = verifyAccessToken(token);
    req.user = decoded;
    next();
  } catch (error) {
    res.status(403).json({ error: 'Invalid or expired access token' });
  }
};

export const authorizeRole = (...roles: string[]) => {
  return (req: AuthRequest, res: Response, next: NextFunction): void => {
    if (!req.user || !roles.includes(req.user.role)) {
      res.status(403).json({ error: 'Insufficient permissions' });
      return;
    }
    next();
  };
};

Мы добавили два middleware: authenticate для проверки токена и authorizeRole для разграничения доступа по ролям. Это позволяет гибко настраивать права.

4. Реализация эндпоинтов логина и обновления токенов

Создадим основной роутер src/routes/auth.ts. Здесь мы реализуем вход пользователя (выдачу пары токенов) и обновление Access Token через Refresh Token.

import { Router, Request, Response } from 'express';
import bcrypt from 'bcrypt';
import { generateAccessToken, generateRefreshToken, verifyRefreshToken } from '../utils/jwt';
import { authenticate, AuthRequest } from '../middleware/auth';

const router = Router();

// Хранилище refresh-токенов (в реальном проекте — база данных)
const refreshTokens: string[] = [];

// POST /api/auth/login
router.post('/login', async (req: Request, res: Response) => {
  const { email, password } = req.body;

  // Имитация поиска пользователя в БД
  const user = { userId: '123', email, role: 'user' as const, passwordHash: '$2b$10$...' };

  // Проверка пароля
  const isValidPassword = await bcrypt.compare(password, user.passwordHash);
  if (!isValidPassword) {
    res.status(401).json({ error: 'Invalid credentials' });
    return;
  }

  const payload = { userId: user.userId, email: user.email, role: user.role };
  const accessToken = generateAccessToken(payload);
  const refreshToken = generateRefreshToken(payload);

  refreshTokens.push(refreshToken); // Сохраняем refresh токен

  res.json({
    accessToken,
    refreshToken,
    tokenType: 'Bearer',
    expiresIn: 900, // 15 минут в секундах
  });
});

// POST /api/auth/refresh
router.post('/refresh', (req: Request, res: Response) => {
  const { refreshToken } = req.body;

  if (!refreshToken) {
    res.status(401).json({ error: 'Refresh token is required' });
    return;
  }

  if (!refreshTokens.includes(refreshToken)) {
    res.status(403).json({ error: 'Invalid refresh token' });
    return;
  }

  try {
    const decoded = verifyRefreshToken(refreshToken);
    const payload = { userId: decoded.userId, email: decoded.email, role: decoded.role };
    const newAccessToken = generateAccessToken(payload);

    res.json({
      accessToken: newAccessToken,
      tokenType: 'Bearer',
      expiresIn: 900,
    });
  } catch (error) {
    res.status(403).json({ error: 'Expired or invalid refresh token' });                

Блоги

Рекомендаций по книгам