OAuth 2.0 и JWT авторизация на TypeScript: для начинающих
Современные веб-приложения редко обходятся без механизмов аутентификации и авторизации. Два самых популярных стандарта — OAuth 2.0 (протокол делегирования доступа) и JWT (JSON Web Token, компактный способ передачи данных). В этой статье мы разберем, как реализовать OAuth 2.0 и JWT авторизацию на TypeScript, создав простой, но рабочий сервер. Материал ориентирован на начинающих разработчиков, знакомых с основами TypeScript и Node.js.
Что такое OAuth 2.0 и JWT?
Прежде чем писать код, важно понять теорию.
OAuth 2.0
Это протокол, который позволяет пользователю предоставить третьему приложению ограниченный доступ к своим ресурсам (например, к профилю на GitHub или Google) без передачи логина и пароля. В OAuth 2.0 есть несколько ролей:
- Resource Owner — владелец ресурса (пользователь).
- Client — приложение, которое хочет получить доступ.
- Authorization Server — сервер, выдающий токены после успешной аутентификации.
- Resource Server — сервер, хранящий защищенные данные.
Самый распространенный сценарий — "Authorization Code Flow", где пользователь перенаправляется на страницу провайдера, логинится, а затем сервер обменивает полученный код на токен.
JWT (JSON Web Token)
JWT — это строка в формате header.payload.signature, которая содержит данные (claims) и подписана секретным ключом. Она используется как Access Token (короткоживущий) или Refresh Token (долгоживущий) в OAuth 2.0. Преимущество JWT — он самодостаточен: сервер может проверить его без обращения к базе данных.
Настройка проекта и установка зависимостей
Создадим новый проект на TypeScript. Убедитесь, что у вас установлены Node.js и npm.
mkdir oauth-jwt-typescript
cd oauth-jwt-typescript
npm init -y
npm install express jsonwebtoken dotenv cors cookie-parser
npm install -D typescript @types/node @types/express @types/jsonwebtoken @types/cors @types/cookie-parser ts-node nodemon
npx tsc --init
В файле tsconfig.json раскомментируйте строку "outDir": "./dist" и установите "rootDir": "./src". Создайте структуру папок:
src/
├── index.ts
├── auth.ts
├── middleware.ts
└── types.ts
.env
В файле .env укажите:
ACCESS_TOKEN_SECRET=your-secret-key-123
REFRESH_TOKEN_SECRET=your-refresh-secret-456
PORT=3000
Реализация сервера с OAuth 2.0 и JWT
1. Базовая структура сервера (index.ts)
Напишем простой Express-сервер с маршрутами для логина, получения токенов и защищенного эндпоинта.
import express from 'express';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import dotenv from 'dotenv';
import { login, refresh, logout } from './auth';
import { authenticateToken } from './middleware';
dotenv.config();
const app = express();
app.use(express.json());
app.use(cors());
app.use(cookieParser());
// Публичный маршрут
app.get('/', (req, res) => {
res.send('OAuth 2.0 + JWT Server is running');
});
// Аутентификация (имитация OAuth 2.0)
app.post('/login', login);
app.post('/token', refresh); // Обновление Access Token
app.delete('/logout', logout);
// Защищенный маршрут
app.get('/protected', authenticateToken, (req, res) => {
res.json({ message: 'Доступ разрешен', user: (req as any).user });
});
app.listen(process.env.PORT, () => {
console.log(`Server running on port ${process.env.PORT}`);
});
2. Типы данных (types.ts)
export interface TokenPayload {
userId: number;
username: string;
}
export interface User {
id: number;
username: string;
password: string; // в реальном проекте здесь хранится хэш
}
3. Логика аутентификации (auth.ts)
Здесь мы эмулируем OAuth 2.0: после успешного логина генерируем Access и Refresh токены. Access Token живет 15 минут, Refresh — 7 дней.
import { Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import { TokenPayload, User } from './types';
// Имитация базы данных
const users: User[] = [
{ id: 1, username: 'alice', password: 'password123' },
{ id: 2, username: 'bob', password: 'secret456' },
];
// Хранилище refresh токенов (в реальности — БД)
const refreshTokens: string[] = [];
export const login = (req: Request, res: Response) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username && u.password === password);
if (!user) {
return res.status(401).json({ error: 'Неверные учетные данные' });
}
const payload: TokenPayload = { userId: user.id, username: user.username };
const accessToken = jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET!, { expiresIn: '15m' });
const refreshToken = jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET!, { expiresIn: '7d' });
refreshTokens.push(refreshToken);
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000
});
res.json({ accessToken });
};
export const refresh = (req: Request, res: Response) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken || !refreshTokens.includes(refreshToken)) {
return res.status(403).json({ error: 'Refresh token недействителен' });
}
try {
const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET!) as TokenPayload;
const newAccessToken = jwt.sign(
{ userId: decoded.userId, username: decoded.username },
process.env.ACCESS_TOKEN_SECRET!,
{ expiresIn: '15m' }
);
res.json({ accessToken: newAccessToken });
} catch (err) {
res.status(403).json({