Безопасная аутентификация в TypeScript: гайд для профи

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

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

Начать курс

Ваши данные под прицелом: почему аутентификация в 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                

Блоги

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