OAuth 2.0 и JWT авторизация на TypeScript: полное руководство
Современные веб-приложения и API требуют надежных механизмов аутентификации и авторизации. Два наиболее популярных стандарта — OAuth 2.0 и JWT (JSON Web Tokens) — часто используются вместе для создания безопасных и масштабируемых систем. В этой статье мы разберем, как реализовать OAuth 2.0 и JWT авторизацию на TypeScript, используя Express.js и популярные библиотеки.
Что такое OAuth 2.0 и JWT?
OAuth 2.0 — это протокол авторизации, который позволяет приложениям получать ограниченный доступ к ресурсам пользователя без передачи его учетных данных. Он широко используется для входа через социальные сети (Google, GitHub, Facebook).
JWT (JSON Web Token) — это компактный и самодостаточный токен, который содержит информацию о пользователе и может быть проверен без обращения к базе данных. JWT часто используется как формат access-токенов в OAuth 2.0.
Вместе они образуют мощный механизм: OAuth 2.0 управляет процессом выдачи токенов, а JWT обеспечивает их безопасную передачу и верификацию.
Настройка проекта и установка зависимостей
Создадим новый проект на TypeScript. Убедитесь, что у вас установлен Node.js (версия 18+).
mkdir oauth-jwt-typescript
cd oauth-jwt-typescript
npm init -y
Установите необходимые зависимости:
npm install express jsonwebtoken passport passport-google-oauth20 dotenv bcryptjs
npm install -D typescript @types/express @types/jsonwebtoken @types/passport @types/passport-google-oauth20 @types/bcryptjs ts-node nodemon
Создайте файл tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist"
},
"include": ["src/**/*"]
}
Структура проекта:
src/
├── config/
│ └── passport.ts
├── controllers/
│ └── authController.ts
├── middleware/
│ └── authMiddleware.ts
├── models/
│ └── user.ts
├── routes/
│ └── authRoutes.ts
├── services/
│ └── jwtService.ts
└── app.ts
Реализация JWT-авторизации
JWT-токены состоят из трех частей: заголовок (header), полезная нагрузка (payload) и подпись (signature). Рассмотрим базовую реализацию на TypeScript.
Создадим сервис для работы с JWT (src/services/jwtService.ts):
import jwt from 'jsonwebtoken';
import dotenv from 'dotenv';
dotenv.config();
const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET || 'access-secret-key';
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || 'refresh-secret-key';
export interface TokenPayload {
userId: string;
email: string;
role: string;
}
export const generateAccessToken = (payload: TokenPayload): string => {
return jwt.sign(payload, ACCESS_SECRET, { expiresIn: '15m' });
};
export const generateRefreshToken = (payload: TokenPayload): string => {
return jwt.sign(payload, REFRESH_SECRET, { expiresIn: '7d' });
};
export const verifyAccessToken = (token: string): TokenPayload => {
return jwt.verify(token, ACCESS_SECRET) as TokenPayload;
};
export const verifyRefreshToken = (token: string): TokenPayload => {
return jwt.verify(token, REFRESH_SECRET) as TokenPayload;
};
Создадим middleware для проверки JWT (src/middleware/authMiddleware.ts):
import { Request, Response, NextFunction } from 'express';
import { verifyAccessToken } from '../services/jwtService';
export interface AuthRequest extends Request {
user?: {
userId: string;
email: string;
role: string;
};
}
export const authenticateJWT = (req: AuthRequest, res: Response, next: NextFunction): void => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
res.status(401).json({ message: 'Токен не предоставлен' });
return;
}
const token = authHeader.split(' ')[1];
try {
const decoded = verifyAccessToken(token);
req.user = decoded;
next();
} catch (error) {
res.status(403).json({ message: 'Недействительный или просроченный токен' });
}
};
Интеграция OAuth 2.0 (на примере Google)
Настроим Passport.js для аутентификации через Google. Создайте файл src/config/passport.ts:
import passport from 'passport';
import { Strategy as GoogleStrategy, Profile } from 'passport-google-oauth20';
import dotenv from 'dotenv';
dotenv.config();
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID || '',
clientSecret: process.env.GOOGLE_CLIENT_SECRET || '',
callbackURL: 'http://localhost:3000/auth/google/callback',
},
async (accessToken: string, refreshToken: string, profile: Profile, done: Function) => {
try {
// Здесь можно найти или создать пользователя в БД
const user = {
googleId: profile.id,
email: profile.emails?.[0]?.value || '',
displayName: profile.displayName,
};
return done(null, user);
} catch (error) {
return done(error, undefined);
}
}
)
);
passport.serializeUser((user: any, done) => {
done(null, user);
});
passport.deserializeUser((user: any, done) => {
done(null, user);
});
export default passport;
Создадим контроллер аутентификации (src/controllers/authController.ts):
import { Request, Response } from 'express';
import { generateAccessToken, generateRefreshToken, verifyRefreshToken } from '../services/jwtService';
export const googleCallback = (req: Request, res: Response): void => {
const user = req.user as any;
if (!user) {
res.status(401).json({ message: 'Ошибка аутентификации' });
return;
}
const tokenPayload = {
userId: user.googleId,
email: user.email,
role: 'user',
};
const accessToken = generateAccessToken(tokenPayload);
const refreshToken = generateRefreshToken(tokenPayload);
res.json({
message: 'Успешная аутентификация через Google',
accessToken,
refreshToken,
user: {
id: user.googleId,
email: user.email,
name: user.displayName,
},
});
};
export const refreshToken = (req: Request,