Введение в OAuth 2.0 и JWT на TypeScript
В современной веб-разработке безопасная авторизация пользователей — это основа любого приложения. Два самых популярных стандарта — OAuth 2.0 и JWT (JSON Web Token) — позволяют реализовать надежную систему аутентификации и разграничения доступа. В этой статье мы разберем, как объединить эти технологии на TypeScript, чтобы создать безопасный API с нуля.
Эта статья предназначена для начинающих разработчиков, которые уже знакомы с основами TypeScript и хотят понять, как работает авторизация на практике. Мы не будем углубляться в сложные теоретические выкладки, а сосредоточимся на конкретных примерах кода.
Что такое OAuth 2.0?
OAuth 2.0 — это протокол авторизации, который позволяет сторонним приложениям получать ограниченный доступ к ресурсам пользователя без передачи его пароля. Представьте, что вы хотите войти на сайт через Google: вы не даете сайту свой пароль от Google, а просто разрешаете ему получить ваш email и имя. Это и есть OAuth 2.0.
Что такое JWT?
JWT (JSON Web Token) — это компактный и самодостаточный формат токена, который содержит всю необходимую информацию о пользователе в зашифрованном виде. Токен состоит из трех частей: заголовок, полезная нагрузка (payload) и подпись. Благодаря подписи сервер может быть уверен, что токен не был подделан.
1. Настройка проекта и установка зависимостей
Для начала создадим новый проект на TypeScript. Откройте терминал и выполните следующие команды:
mkdir oauth-jwt-ts
cd oauth-jwt-ts
npm init -y
npm install express jsonwebtoken bcrypt dotenv cors
npm install -D typescript ts-node @types/express @types/jsonwebtoken @types/bcrypt @types/cors @types/node nodemon
Создайте файл tsconfig.json с базовой конфигурацией:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Структура папок будет выглядеть так:
src/— исходный кодsrc/controllers/— контроллерыsrc/middleware/— middleware для проверки токеновsrc/models/— модели данныхsrc/routes/— маршруты.env— переменные окружения
2. Реализация базовой аутентификации с JWT
Начнем с создания простого механизма регистрации и входа с использованием JWT. Создадим файл src/config.ts для хранения констант:
import dotenv from 'dotenv';
dotenv.config();
export const config = {
jwtSecret: process.env.JWT_SECRET || 'your-secret-key',
jwtExpiresIn: process.env.JWT_EXPIRES_IN || '15m',
refreshTokenExpiresIn: process.env.REFRESH_TOKEN_EXPIRES_IN || '7d',
port: process.env.PORT || 3000
};
Теперь создадим middleware для проверки JWT токена. Файл src/middleware/auth.middleware.ts:
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { config } from '../config';
export interface AuthRequest extends Request {
user?: { userId: string; email: string };
}
export const authenticateToken = (req: AuthRequest, res: Response, next: NextFunction) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ message: 'Токен не предоставлен' });
}
try {
const decoded = jwt.verify(token, config.jwtSecret) as { userId: string; email: string };
req.user = decoded;
next();
} catch (error) {
return res.status(403).json({ message: 'Недействительный или просроченный токен' });
}
};
Создадим простой контроллер для регистрации и входа. Файл src/controllers/auth.controller.ts:
import { Request, Response } from 'express';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { config } from '../config';
// В реальном проекте здесь будет работа с базой данных
const users: Array<{ id: string; email: string; password: string }> = [];
export const register = async (req: Request, res: Response) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ message: 'Email и пароль обязательны' });
}
// Проверка, существует ли пользователь
const existingUser = users.find(u => u.email === email);
if (existingUser) {
return res.status(409).json({ message: 'Пользователь с таким email уже существует' });
}
// Хешируем пароль
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
// Создаем пользователя (в реальности — сохраняем в БД)
const newUser = {
id: String(users.length + 1),
email,
password: hashedPassword
};
users.push(newUser);
// Генерируем JWT токен
const token = jwt.sign(
{ userId: newUser.id, email: newUser.email },
config.jwtSecret,
{ expiresIn: config.jwtExpiresIn }
);
res.status(201).json({
message: 'Пользователь успешно зарегистрирован',
token,
user: { id: newUser.id, email: newUser.email }
});
} catch (error) {
res.status(500).json({ message: 'Ошибка сервера' });
}
};
export const login = async (req: Request, res: Response) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ message: 'Email и пароль обязательны' });
}
// Ищем пользователя
const user = users.find(u => u.email === email);
if (!user) {
return res.status(401).json({ message: 'Неверный email или пароль' });
}
// Проверяем пароль
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).