JS Tower
Node.jsМодуль 6: Безопасность

OAuth 2.0

Вход через Google, GitHub, Passport.js

Цель урока

В этом уроке ты научишься:

  • Понимать OAuth 2.0 flow
  • Настраивать Passport.js
  • Реализовывать вход через Google/GitHub

OAuth 2.0 Flow

  1. Пользователь нажимает "Войти через Google"
  2. Редирект на Google
  3. Пользователь разрешает доступ
  4. Google редиректит обратно с кодом
  5. Сервер обменивает код на токен
  6. Сервер получает данные пользователя

Passport.js

Установка

npm install passport passport-google-oauth20 passport-github2

Настройка

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

// Google Strategy
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 {
    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
      });
    }
    
    done(null, user);
  } catch (error) {
    done(error, null);
  }
}));

// GitHub Strategy
passport.use(new GitHubStrategy({
  clientID: process.env.GITHUB_CLIENT_ID,
  clientSecret: process.env.GITHUB_CLIENT_SECRET,
  callbackURL: '/auth/github/callback'
}, async (accessToken, refreshToken, profile, done) => {
  try {
    let user = await User.findOne({ githubId: profile.id });
    
    if (!user) {
      user = await User.create({
        githubId: profile.id,
        email: profile.emails?.[0]?.value,
        name: profile.displayName || profile.username,
        avatar: profile.photos[0]?.value
      });
    }
    
    done(null, user);
  } catch (error) {
    done(error, null);
  }
}));

Маршруты

const express = require('express');
const passport = require('passport');
const jwt = require('jsonwebtoken');

const app = express();
app.use(passport.initialize());

// Google
app.get('/auth/google',
  passport.authenticate('google', { scope: ['profile', 'email'] })
);

app.get('/auth/google/callback',
  passport.authenticate('google', { session: false }),
  (req, res) => {
    const token = jwt.sign(
      { id: req.user._id },
      process.env.JWT_SECRET,
      { expiresIn: '7d' }
    );
    
    // Редирект на фронтенд с токеном
    res.redirect(`${process.env.FRONTEND_URL}/auth/callback?token=${token}`);
  }
);

// GitHub
app.get('/auth/github',
  passport.authenticate('github', { scope: ['user:email'] })
);

app.get('/auth/github/callback',
  passport.authenticate('github', { session: false }),
  (req, res) => {
    const token = jwt.sign(
      { id: req.user._id },
      process.env.JWT_SECRET,
      { expiresIn: '7d' }
    );
    
    res.redirect(`${process.env.FRONTEND_URL}/auth/callback?token=${token}`);
  }
);

Модель пользователя

const userSchema = new mongoose.Schema({
  email: { type: String, unique: true, sparse: true },
  password: { type: String, select: false },
  name: String,
  avatar: String,
  googleId: { type: String, unique: true, sparse: true },
  githubId: { type: String, unique: true, sparse: true }
});

Получение credentials

Google

  1. Перейди на Google Cloud Console
  2. Создай проект
  3. APIs & Services → Credentials
  4. Create Credentials → OAuth client ID
  5. Добавь Authorized redirect URI: http://localhost:3000/auth/google/callback

GitHub

  1. Перейди в Settings → Developer settings → OAuth Apps
  2. New OAuth App
  3. Authorization callback URL: http://localhost:3000/auth/github/callback

.env

GOOGLE_CLIENT_ID=your-client-id
GOOGLE_CLIENT_SECRET=your-client-secret
GITHUB_CLIENT_ID=your-client-id
GITHUB_CLIENT_SECRET=your-client-secret
FRONTEND_URL=http://localhost:5173

Практика

Задание: Связывание аккаунтов

Задача: Позволь пользователю связать Google и GitHub.

Решение:

// Middleware для связывания
function linkAccount(provider) {
  return async (accessToken, refreshToken, profile, done) => {
    try {
      const providerIdField = `${provider}Id`;
      
      // Если пользователь авторизован, связываем
      if (this.req.user) {
        const user = await User.findByIdAndUpdate(
          this.req.user.id,
          { [providerIdField]: profile.id },
          { new: true }
        );
        return done(null, user);
      }
      
      // Иначе ищем или создаём
      let user = await User.findOne({ [providerIdField]: profile.id });
      
      if (!user) {
        user = await User.create({
          [providerIdField]: profile.id,
          email: profile.emails?.[0]?.value,
          name: profile.displayName
        });
      }
      
      done(null, user);
    } catch (error) {
      done(error, null);
    }
  };
}

Проверь себя

  1. Что такое OAuth 2.0?
  2. Зачем нужен callback URL?
  3. Как связать несколько провайдеров?