← Назад к вопросам

Реализовывал ли авторизацию на Node.js

2.0 Middle🔥 231 комментариев
#Node.js и JavaScript#Безопасность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI30 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Реализация авторизации и аутентификации на Node.js

Да, я реализовывал различные схемы аутентификации и авторизации в Node.js. Это один из самых критичных компонентов любого приложения, потому что ошибки здесь приводят к взлому учётных записей пользователей.

JWT (JSON Web Tokens) — самый распространённый подход

Мой стандартный паттерн для REST API:

const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

// 1. Регистрация пользователя
router.post('/auth/register', async (req, res, next) => {
  try {
    const { email, password } = req.body;
    
    // Валидация
    if (!email || !password) {
      return res.status(400).json({ error: 'Missing fields' });
    }
    
    // Проверка, не существует ли уже
    const exists = await User.findOne({ email });
    if (exists) {
      return res.status(409).json({ error: 'User already exists' });
    }
    
    // Хеширование пароля (НИКОГДА не храним в открытом виде)
    const hashedPassword = await bcrypt.hash(password, 12);
    
    // Сохраняем в БД
    const user = await User.create({
      email,
      password: hashedPassword,
      createdAt: new Date()
    });
    
    res.status(201).json({ userId: user.id });
  } catch (error) {
    next(error);
  }
});

// 2. Логин (получение JWT токена)
router.post('/auth/login', async (req, res, next) => {
  try {
    const { email, password } = req.body;
    
    // Находим пользователя
    const user = await User.findOne({ email });
    if (!user) {
      // Не говорим, что пользователь не найден (security best practice)
      return res.status(401).json({ error: 'Invalid credentials' });
    }
    
    // Проверяем пароль
    const isPasswordValid = await bcrypt.compare(password, user.password);
    if (!isPasswordValid) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }
    
    // Генерируем JWT токен
    const accessToken = jwt.sign(
      {
        userId: user.id,
        email: user.email,
        role: user.role
      },
      process.env.JWT_SECRET,
      { expiresIn: '1h' } // Короткая жизнь для безопасности
    );
    
    // Refresh token (долгоживущий)
    const refreshToken = jwt.sign(
      { userId: user.id },
      process.env.JWT_REFRESH_SECRET,
      { expiresIn: '7d' }
    );
    
    // Сохраняем refresh token в БД (для revocation)
    await RefreshToken.create({
      userId: user.id,
      token: refreshToken,
      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
    });
    
    res.json({
      accessToken,
      refreshToken,
      expiresIn: 3600
    });
  } catch (error) {
    next(error);
  }
});

Middleware для проверки JWT токена

// Middleware, который проверяет токен во всех защищённых маршрутах
const authenticateToken = (req, res, next) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // "Bearer TOKEN"
  
  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }
  
  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) {
      // Проверяем, истёк ли токен
      if (err.name === 'TokenExpiredError') {
        return res.status(401).json({ error: 'Token expired' });
      }
      return res.status(403).json({ error: 'Invalid token' });
    }
    
    req.user = user; // Добавляем данные пользователя в request
    next();
  });
};

// Использование
router.get('/profile', authenticateToken, async (req, res) => {
  const user = await User.findById(req.user.userId);
  res.json(user);
});

// Endpoint для обновления access token
router.post('/auth/refresh', async (req, res) => {
  const { refreshToken } = req.body;
  
  if (!refreshToken) {
    return res.status(401).json({ error: 'No refresh token' });
  }
  
  try {
    const verified = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
    
    // Проверяем, не был ли токен отозван
    const tokenRecord = await RefreshToken.findOne({ token: refreshToken });
    if (!tokenRecord) {
      return res.status(401).json({ error: 'Token revoked' });
    }
    
    // Генерируем новый access token
    const user = await User.findById(verified.userId);
    const newAccessToken = jwt.sign(
      { userId: user.id, email: user.email, role: user.role },
      process.env.JWT_SECRET,
      { expiresIn: '1h' }
    );
    
    res.json({ accessToken: newAccessToken });
  } catch (error) {
    res.status(401).json({ error: 'Invalid refresh token' });
  }
});

OAuth 2.0 (Вход через Google, GitHub, etc.)

const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;

// Настройка Passport
passport.use(
  new GoogleStrategy(
    {
      clientID: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      callbackURL: '/auth/google/callback'
    },
    async (accessToken, refreshToken, profile, done) => {
      try {
        // Проверяем, есть ли уже пользователь с этим Google ID
        let user = await User.findOne({ googleId: profile.id });
        
        if (!user) {
          // Создаём нового пользователя
          user = await User.create({
            googleId: profile.id,
            email: profile.emails[0].value,
            name: profile.displayName,
            avatar: profile.photos[0]?.value
          });
        }
        
        return done(null, user);
      } catch (error) {
        return done(error);
      }
    }
  )
);

// Маршруты
router.get('/auth/google',
  passport.authenticate('google', { scope: ['profile', 'email'] })
);

router.get('/auth/google/callback',
  passport.authenticate('google', { failureRedirect: '/login' }),
  (req, res) => {
    const token = jwt.sign(
      { userId: req.user.id, email: req.user.email },
      process.env.JWT_SECRET,
      { expiresIn: '1h' }
    );
    
    // Редирект на фронтенд с токеном
    res.redirect(`/auth/callback?token=${token}`);
  }
);

Роли и разрешения (RBAC)

// Middleware для проверки ролей
const authorize = (allowedRoles) => {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Not authenticated' });
    }
    
    if (!allowedRoles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    
    next();
  };
};

// Использование
router.delete('/users/:id',
  authenticateToken,
  authorize(['admin']),
  async (req, res) => {
    await User.deleteById(req.params.id);
    res.json({ ok: true });
  }
);

// Более сложный пример с permission-based access
const checkPermission = (requiredPermission) => {
  return async (req, res, next) => {
    const user = await User.findById(req.user.userId);
    const role = await Role.findById(user.roleId);
    
    if (!role.permissions.includes(requiredPermission)) {
      return res.status(403).json({ error: 'Permission denied' });
    }
    
    next();
  };
};

router.post('/posts',
  authenticateToken,
  checkPermission('create_posts'),
  createPostHandler
);

Защита от атак

// 1. Rate limiting на auth endpoint (защита от brute force)
const rateLimit = require('express-rate-limit');

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 минут
  max: 5, // 5 попыток
  message: 'Too many login attempts, please try again later'
});

router.post('/auth/login', loginLimiter, loginHandler);

// 2. CSRF protection
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: false });

router.post('/auth/logout', csrfProtection, (req, res) => {
  // Отозываем refresh token
  res.json({ ok: true });
});

// 3. Secure cookies для refresh token (вместо localStorage)
res.cookie('refreshToken', refreshToken, {
  httpOnly: true,      // JS не может получить
  secure: true,        // Только HTTPS
  sameSite: 'strict'   // CSRF protection
});

// 4. Password reset с одноразовым токеном
router.post('/auth/forgot-password', async (req, res) => {
  const { email } = req.body;
  
  const user = await User.findOne({ email });
  if (!user) {
    // Не говорим, существует ли пользователь
    return res.json({ message: 'Check your email' });
  }
  
  // Генерируем одноразовый токен с коротким TTL
  const resetToken = jwt.sign(
    { userId: user.id, type: 'password-reset' },
    process.env.JWT_SECRET,
    { expiresIn: '15m' } // Только 15 минут!
  );
  
  // Сохраняем хеш токена (не сам токен)
  await PasswordReset.create({
    userId: user.id,
    tokenHash: await bcrypt.hash(resetToken, 10)
  });
  
  // Отправляем ссылку на email
  await sendEmail(user.email, `Reset password: /reset/${resetToken}`);
  
  res.json({ message: 'Password reset link sent' });
});

Лучшие практики, которые я применяю

  • Никогда не храню пароли в открытом виде — только bcrypt hash
  • JWT с коротким TTL (1 час) + refresh tokens (7 дней)
  • Refresh tokens в httpOnly cookies, не в localStorage
  • Rate limiting на auth endpoints
  • HTTPS always — токены только по защищённому каналу
  • Логирование попыток входа — detect suspicious activity
  • 2FA для критичных операций (изменение email, выход из всех устройств)
  • Регулярная смена JWT_SECRET — старые токены становятся невалидными
  • CORS + CSRF protection — защита от атак

Это основные паттерны, которые я использую в production приложениях.

Реализовывал ли авторизацию на Node.js | PrepBro