Каждый раз, когда вы нажимаете «Войти через 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
Есть идея? Реализуем
Разрабатываем проекты, которые решают задачи бизнеса — от лендинга до сложного сервиса. Расскажите о своей задаче, подберём решение.

