OAuth 2.0

Зачем нужно

OAuth 2.0 — протокол авторизации, позволяющий приложению получить доступ к ресурсам пользователя на другом сервисе без передачи пароля. Когда вы нажимаете «Войти через Google» — это OAuth. Приложение получает ограниченный токен доступа, а пароль остаётся у Google.

Где используется

  • Вход через соцсети (Google, GitHub, VK, Telegram)
  • Доступ к API от имени пользователя (GitHub API, Google Drive)
  • Микросервисы — авторизация между сервисами
  • Мобильные приложения

Роли в OAuth

Resource Owner   — пользователь (владелец данных)
Client           — приложение, которое хочет доступ
Authorization Server — сервер авторизации (Google, GitHub)
Resource Server  — API с защищёнными ресурсами

Authorization Code Flow (основной)

Самый безопасный flow для серверных приложений:

1. Пользователь нажимает "Войти через GitHub"
      ↓
2. Приложение → редирект на GitHub:
   https://github.com/login/oauth/authorize?
     client_id=abc123&
     redirect_uri=http://myapp.com/callback&
     scope=read:user&
     state=random123
      ↓
3. Пользователь логинится на GitHub, даёт согласие
      ↓
4. GitHub → редирект обратно с code:
   http://myapp.com/callback?code=xyz789&state=random123
      ↓
5. Сервер приложения → обменивает code на token:
   POST https://github.com/login/oauth/access_token
   { client_id, client_secret, code, redirect_uri }
      ↓
6. GitHub → возвращает access_token
      ↓
7. Приложение → запрашивает данные пользователя:
   GET https://api.github.com/user
   Authorization: Bearer access_token

Реализация (Express + GitHub)

const express = require('express');
const app = express;

const CLIENT_ID = process.env.GITHUB_CLIENT_ID;
const CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET;
const REDIRECT_URI = 'http://localhost:3000/auth/github/callback';

// Шаг 1: Редирект на GitHub
app.get('/auth/github', (req, res) => {
  const state = Math.random.toString(36).substring(7);
  // Сохранить state в session для проверки (защита от CSRF)

  const url = new URL('https://github.com/login/oauth/authorize');
  url.searchParams.set('client_id', CLIENT_ID);
  url.searchParams.set('redirect_uri', REDIRECT_URI);
  url.searchParams.set('scope', 'read:user user:email');
  url.searchParams.set('state', state);

  res.redirect(url.toString());
});

// Шаг 2: Callback — обмен code → token
app.get('/auth/github/callback', async (req, res) => {
  const { code, state } = req.query;
  // Проверить state (защита от CSRF)

  // Обмен code на access_token
  const tokenResponse = await fetch(
    'https://github.com/login/oauth/access_token',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
      },
      body: JSON.stringify({
        client_id: CLIENT_ID,
        client_secret: CLIENT_SECRET,
        code,
        redirect_uri: REDIRECT_URI,
      }),
    }
  );

  const { access_token } = await tokenResponse.json();

  // Шаг 3: Получить данные пользователя
  const userResponse = await fetch('https://api.github.com/user', {
    headers: { Authorization: `Bearer ${access_token}` },
  });

  const githubUser = await userResponse.json();

  // Создать/найти пользователя в своей БД
  // Выдать свой JWT или создать сессию
  // ...

  res.redirect('/dashboard');
});

PKCE (для SPA и мобильных приложений)

Proof Key for Code Exchange — расширение для клиентов без server secret:

// Шаг 0: Генерация code_verifier и code_challenge
function generatePKCE() {
  // Случайная строка 43-128 символов
  const verifier = crypto.randomUUID + crypto.randomUUID;

  // SHA-256 хеш verifier, закодированный в Base64URL
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);

  return crypto.subtle.digest('SHA-256', data).then(hash => {
    const challenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
      .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');

    return { verifier, challenge };
  });
}

// Шаг 1: Редирект с code_challenge
const { verifier, challenge } = await generatePKCE;
sessionStorage.setItem('pkce_verifier', verifier);

const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'openid profile');
authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('state', 'random123');

window.location.href = authUrl.toString();

// Шаг 2: Callback — обмен code + verifier на token
const code = new URLSearchParams(window.location.search).get('code');
const verifier = sessionStorage.getItem('pkce_verifier');

const tokenResponse = await fetch('https://auth.example.com/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    client_id: CLIENT_ID,
    code,
    redirect_uri: REDIRECT_URI,
    code_verifier: verifier,  // Сервер проверяет SHA256(verifier) = challenge
  }),
});

const { access_token, refresh_token } = await tokenResponse.json();

Другие Flow

Implicit Flow (устаревший)

Токен возвращается сразу в URL (без обмена code):
redirect_uri#access_token=abc&token_type=bearer

❌ Не рекомендуется — токен виден в URL, истории браузера, referer
✅ Заменён на Authorization Code + PKCE

Client Credentials Flow

Для server-to-server (без пользователя):

POST /oauth/token
{
  grant_type: "client_credentials",
  client_id: "app_id",
  client_secret: "app_secret",
  scope: "read write"
}

→ { access_token: "..." }

Используется: микросервисы, cron-задачи, бэкенд-интеграции

Scopes (области доступа)

// GitHub scopes
read:user       — читать профиль
user:email      — читать email
repo            — доступ к репозиториям
gist            — доступ к gist

// Google scopes
openid                         — OpenID Connect
profile                        — имя, фото
email                          — email
https://www.googleapis.com/auth/drive.readonly — Google Drive

Частые ошибки

  1. Нет проверки state — CSRF-атака: злоумышленник подставляет свой code
  2. Implicit Flow вместо PKCE — токен в URL небезопасен
  3. Client secret на клиенте — secret нельзя хранить в SPA/мобильном приложении
  4. Широкие scopes — запрашивают repo когда нужен только read:user
  5. Нет обработки ошибок — redirect может вернуть error=access_denied
  6. Хранят access_token бессрочно — нужен refresh flow

Практика

  1. Зарегистрировать OAuth App на GitHub
  2. Реализовать Authorization Code Flow на Express
  3. Получить профиль пользователя через GitHub API
  4. Добавить PKCE flow для SPA
  5. Реализовать «Войти через Google» с OpenID Connect

Связанные темы

  • JWT — JWT как access token в OAuth
  • Cookies и сессии — сессия после OAuth-логина
  • CORS — кросс-доменные запросы к OAuth-провайдерам
  • HTTP протокол — Authorization header с Bearer token

Ресурсы