Ваши данные под прицелом: почему аутентификация в TypeScript — это не шутка
Представьте: вы создали крутое приложение на TypeScript. Оно летает, типы строгие, код красивый. Но есть одна маленькая деталь — аутентификация. И тут начинается самое интересное. Одна ошибка — и данные пользователей утекают, как вода сквозь пальцы. Знакомо?
В 2024 году безопасная аутентификация — это не просто «прикрутить логин». Это целая наука. И TypeScript здесь — ваш лучший друг. Он не даст вам случайно передать неправильный тип данных, подскажет, где вы забыли проверить токен, и спасет от глупых ошибок во время компиляции.
В этой статье мы разберем, как построить безопасную аутентификацию в TypeScript приложениях с нуля. Никакой воды — только реальные примеры, лучшие практики и чуточка черного юмора (куда без него).
JWT: любовь с первого токена
JSON Web Token (JWT) — это стандарт де-факто для аутентификации в современных веб-приложениях. Но, как говорил дядя Бен, «с большой силой приходит большая ответственность». JWT может быть как щитом, так и бомбой замедленного действия.
Как НЕ надо делать: антипример
Самый частый грех новичков — хранить токен в localStorage и передавать его в открытую. Да, это работает. Но это как оставить ключи от квартиры под ковриком — вроде удобно, но любой прохожий может зайти. XSS-атака — и все ваши токены у злоумышленника.
Правильная работа с JWT в TypeScript
Давайте напишем безопасную обертку для работы с токенами. TypeScript здесь поможет нам не перепутать типы и не забыть про валидацию.
import jwt from 'jsonwebtoken';
interface TokenPayload {
userId: string;
role: 'user' | 'admin';
iat?: number;
exp?: number;
}
// Генерируем токен с четкой типизацией
function generateAccessToken(payload: TokenPayload): string {
return jwt.sign(
{ ...payload },
process.env.JWT_SECRET as string,
{ expiresIn: '15m' }
);
}
// Верифицируем и сразу кастуем к нашему интерфейсу
function verifyAccessToken(token: string): TokenPayload | null {
try {
return jwt.verify(
token,
process.env.JWT_SECRET as string
) as TokenPayload;
} catch (error) {
console.error('Токен не прошел проверку:', error);
return null;
}
}
Обратите внимание: мы всегда проверяем, что JWT_SECRET существует и является строкой. TypeScript заставляет нас это делать — никаких undefined в рантайме!
Хеширование паролей: bcrypt спешит на помощь
Хранить пароли в открытом виде — это преступление. Даже если ваша база данных защищена лучше, чем Fort Knox, один взлом — и вы в новостях как очередной «слив паролей». Хеширование паролей — ваш спасательный круг.
Почему bcrypt, а не MD5?
MD5 умер. SHA-256 — тоже не вариант. Современные GPU могут перебрать миллиарды хешей в секунду. Bcrypt же специально создан медленным. Он использует соль (случайные данные) и adjustable cost factor. Чем выше cost — тем дольше считать хеш. И это хорошо!
Реализация в TypeScript
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 12; // Оптимальное значение в 2024 году
async function hashPassword(password: string): Promise<string> {
const salt = await bcrypt.genSalt(SALT_ROUNDS);
return bcrypt.hash(password, salt);
}
async function comparePassword(
password: string,
hash: string
): Promise<boolean> {
return bcrypt.compare(password, hash);
}
// Типизированная функция для регистрации
interface CreateUserInput {
email: string;
password: string;
name: string;
}
async function createUser(input: CreateUserInput): Promise<Omit<User, 'passwordHash'>> {
const passwordHash = await hashPassword(input.password);
// Сохраняем в БД (тут ваш код)
const user = await db.users.create({
data: {
email: input.email,
passwordHash, // Никогда не сохраняем пароль в открытом виде!
name: input.name
}
});
// Возвращаем пользователя без хеша пароля
const { passwordHash: _, ...safeUser } = user;
return safeUser;
}
Видите Omit<User, 'passwordHash'>? Это TypeScript гарантирует, что мы случайно не вернем хеш пароля клиенту. Компилятор не даст нам ошибиться.
Защита от XSS и CSRF: два сапога — пара
XSS (Cross-Site Scripting) и CSRF (Cross-Site Request Forgery) — это классический дуэт злодеев. Первый ворует данные, второй заставляет пользователя делать то, что он не хотел. Вместе они — сила. Но мы сильнее.
Правильная настройка HTTP-only кук
Забудьте про localStorage для токенов. Используйте HTTP-only куки. Они недоступны из JavaScript — XSS становится бесполезным.
import express from 'express';
import cookieParser from 'cookie-parser';
const app = express();
app.use(cookieParser());
// Устанавливаем токен в HTTP-only куку
function setAuthCookie(res: express.Response, token: string): void {
res.cookie('auth_token', token, {
httpOnly: true, // Недоступно из JS
secure: true, // Только по HTTPS
sameSite: 'strict', // Защита от CSRF
maxAge: 15 * 60 * 1000 // 15 минут
});
}
// Middleware для проверки аутентификации
function authMiddleware(
req: express.Request,
res: express.Response,
next: express.NextFunction
): void {
const token = req.cookies?.auth_token;
if (!token) {
res.status(401).json({ error: 'Не авторизован' });
return;
}
const payload = verifyAccessToken(token);
if (!payload) {
res.status(401).json({ error: 'Токен истек или невалиден' });
return;
}
// Добавляем пользователя в request
(req as any).user = payload;
next();
}
CSRF-токены: последняя линия обороны
Даже с sameSite: 'strict' иногда нужна дополнительная защита. Особенно если ваше API использует разные домены. CSRF-токен — это секретное значение, которое сервер вставляет в форму, а клиент отправляет обратно. Если значения не совпадают — запрос отклоняется.
import crypto from 'crypto';
// Генерируем CSRF токен
function generateCsrfToken(): string {
return crypto.randomBytes(32).toString('hex');
}
// Middleware для проверки CSRF
function csrfProtection(
req: express.Request,
res: express.Response,
next: express.NextFunction
): void {
const csrfToken = req.headers['x-csrf-token'];
const storedToken = req.session?.csrfToken;
if (!csrfToken || !storedToken || csrfToken !== storedToken