OAuth 2.0 простым языком: как работает авторизация через соцсети

Каждый раз, когда вы нажимаете «Войти через Google» или «Авторизация через VK», за кулисами работает протокол OAuth 2.0. Он кажется сложным, но на деле укладывается в 4 простых шага. Разберём по косточкам — с реальным кодом на Python и JavaScript.

Зачем нужен OAuth 2.0

OAuth 2.0 — это протокол делегированной авторизации. Он позволяет вашему приложению получить доступ к данным пользователя на стороннем сервисе без знания его пароля. Пользователь сам решает, какие данные открыть.

Без OAuth пришлось бы просить у пользователя логин и пароль от Google — это небезопасно и ни один здравомыслящий человек на это не согласится.

Участники процесса

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

Authorization Code Flow: пошагово

Это самый распространённый и безопасный флоу. Именно он используется на 95% сайтов с «Войти через…».

Шаг 1: Редирект на провайдера

Ваше приложение отправляет пользователя на страницу авторизации провайдера:

const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', 'https://mysite.ru/callback');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'openid email profile');
authUrl.searchParams.set('state', generateRandomState());

// Перенаправляем пользователя
window.location.href = authUrl.toString();

Параметр state — это защита от CSRF-атак. Генерируйте случайную строку и сохраняйте в сессии.

Шаг 2: Пользователь даёт согласие

Провайдер показывает экран: «Приложение X запрашивает доступ к вашему email и профилю». Пользователь нажимает «Разрешить».

Шаг 3: Получение authorization code

Провайдер редиректит обратно на ваш redirect_uri с одноразовым кодом:

https://mysite.ru/callback?code=4/0AX4XfWh...&state=abc123

Этот код живёт несколько минут и может быть использован только один раз.

Шаг 4: Обмен кода на токены

Ваш сервер отправляет POST-запрос напрямую к провайдеру (server-to-server, без участия браузера):

import httpx

async def exchange_code(code: str) -> dict:
    async with httpx.AsyncClient() as client:
        response = await client.post(
            "https://oauth2.googleapis.com/token",
            data={
                "code": code,
                "client_id": CLIENT_ID,
                "client_secret": CLIENT_SECRET,
                "redirect_uri": "https://mysite.ru/callback",
                "grant_type": "authorization_code",
            },
        )
        response.raise_for_status()
        return response.json()

# Ответ:
# {
#   "access_token": "ya29.a0AfH6SM...",
#   "refresh_token": "1//0eXy...",
#   "expires_in": 3600,
#   "token_type": "Bearer",
#   "scope": "openid email profile"
# }

Access Token и Refresh Token

Токен Время жизни Назначение
Access Token 15 мин — 1 час Доступ к API провайдера
Refresh Token Недели — месяцы Получение нового Access Token без участия пользователя

Когда Access Token протухает, используйте Refresh Token:

async def refresh_access_token(refresh_token: str) -> dict:
    async with httpx.AsyncClient() as client:
        response = await client.post(
            "https://oauth2.googleapis.com/token",
            data={
                "refresh_token": refresh_token,
                "client_id": CLIENT_ID,
                "client_secret": CLIENT_SECRET,
                "grant_type": "refresh_token",
            },
        )
        response.raise_for_status()
        return response.json()

PKCE: защита для публичных клиентов

Если ваш клиент — мобильное приложение или SPA (где нельзя безопасно хранить client_secret), используйте PKCE (Proof Key for Code Exchange).

Суть: вместо секрета приложение генерирует одноразовый code_verifier и отправляет его хеш (code_challenge) при запросе авторизации. При обмене кода на токен отправляет оригинальный code_verifier.

// Генерация PKCE-параметров
function generateCodeVerifier() {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return base64UrlEncode(array);
}

async function generateCodeChallenge(verifier) {
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const digest = await crypto.subtle.digest('SHA-256', data);
  return base64UrlEncode(new Uint8Array(digest));
}

function base64UrlEncode(buffer) {
  return btoa(String.fromCharCode(...buffer))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
}

// Добавляем к URL авторизации:
const verifier = generateCodeVerifier();
const challenge = await generateCodeChallenge(verifier);

authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');

// Сохраняем verifier в sessionStorage
sessionStorage.setItem('pkce_verifier', verifier);

При обмене кода на токен вместо client_secret передаёте code_verifier.

Получение данных пользователя

После получения Access Token можно запросить профиль:

async def get_user_info(access_token: str) -> dict:
    async with httpx.AsyncClient() as client:
        response = await client.get(
            "https://www.googleapis.com/oauth2/v2/userinfo",
            headers={"Authorization": f"Bearer {access_token}"},
        )
        response.raise_for_status()
        return response.json()

# Ответ:
# {
#   "id": "1234567890",
#   "email": "user@gmail.com",
#   "name": "Иван Петров",
#   "picture": "https://lh3.googleusercontent.com/..."
# }

Полный пример: обработчик callback на сервере

from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import RedirectResponse
import httpx

app = FastAPI()

@app.get("/auth/google")
async def auth_google(request: Request):
    state = secrets.token_urlsafe(32)
    request.session["oauth_state"] = state

    params = {
        "client_id": CLIENT_ID,
        "redirect_uri": f"{BASE_URL}/auth/google/callback",
        "response_type": "code",
        "scope": "openid email profile",
        "state": state,
    }
    url = f"https://accounts.google.com/o/oauth2/v2/auth?{urlencode(params)}"
    return RedirectResponse(url)


@app.get("/auth/google/callback")
async def auth_callback(request: Request, code: str, state: str):
    saved_state = request.session.get("oauth_state")
    if state != saved_state:
        raise HTTPException(400, "Invalid state parameter")

    # Обмен кода на токены
    async with httpx.AsyncClient() as client:
        token_resp = await client.post(
            "https://oauth2.googleapis.com/token",
            data={
                "code": code,
                "client_id": CLIENT_ID,
                "client_secret": CLIENT_SECRET,
                "redirect_uri": f"{BASE_URL}/auth/google/callback",
                "grant_type": "authorization_code",
            },
        )
        tokens = token_resp.json()

        # Получение профиля
        user_resp = await client.get(
            "https://www.googleapis.com/oauth2/v2/userinfo",
            headers={"Authorization": f"Bearer {tokens['access_token']}"},
        )
        user_info = user_resp.json()

    # Создаём или обновляем пользователя в базе
    user = await upsert_user(
        provider="google",
        provider_id=user_info["id"],
        email=user_info["email"],
        name=user_info["name"],
    )

    # Устанавливаем сессию
    request.session["user_id"] = user.id
    return RedirectResponse("/dashboard")

Типичные ошибки

1. Не проверяете параметр state

Без проверки state ваш callback уязвим к CSRF. Атакующий может подставить свой authorization code и привязать свой аккаунт к жертве.

2. Храните токены в localStorage

localStorage доступен любому JS-коду на странице. XSS-уязвимость = утечка токенов. Храните токены в httpOnly cookies или на сервере.

3. Не обрабатываете отзыв доступа

Пользователь может отозвать доступ в настройках Google. Ваш Refresh Token перестанет работать. Всегда обрабатывайте ошибку invalid_grant.

4. Используете Implicit Flow

Implicit Flow (response_type=token) считается устаревшим. Токен передаётся через фрагмент URL и может утечь через историю браузера или Referer. Используйте Authorization Code Flow + PKCE.

5. Один redirect_uri для dev и prod

Всегда регистрируйте отдельные redirect_uri для каждого окружения. Не используйте http://localhost в продакшене.

Чеклист интеграции OAuth

  • Зарегистрировали приложение у провайдера и получили client_id / client_secret
  • Указали корректные redirect_uri в настройках приложения
  • Генерируете и проверяете параметр state
  • Используете PKCE для SPA и мобильных приложений
  • Храните client_secret только на сервере (в .env)
  • Сохраняете Refresh Token в базе данных
  • Обрабатываете ошибки: invalid_grant, access_denied, истёкшие токены
  • Запрашиваете минимально необходимые scopes

Есть идея? Реализуем

Разрабатываем проекты, которые решают задачи бизнеса — от лендинга до сложного сервиса. Расскажите о своей задаче, подберём решение.

Написать в Telegram

29.03.2026

Нужна консультация?

Оставьте свои контактные данные, или свяжитесь с нами удобным для вас способом

Привет! Меня зовут Багира. Пишите, я все передам хозяевам!

Привет! Меня зовут Багира. Пишите, я все передам хозяевам!

Нажимая кнопку «Принять», вы соглашаетесь на сбор cookie. Мы используем их для обеспечения функционирования веб-сайта, аналитики действий и улучшения качества обслуживания. Если Вы не хотите, чтобы эти данные обрабатывались, отключите cookie в настройках браузера или прекратите использовать сайт.
Принять