Learning Platform
Глоссарий Troubleshooting
Урок 09.03 · 28 мин
Начальный
OAuth2OAuth 2.1PKCEAuthorizationБезопасность

OAuth 2.0 / 2.1: делегированный доступ без передачи паролей

JWT — это формат токена. OAuth 2.0 — протокол, описывающий, как клиент получает токен от authorization server, не передавая пароль пользователя ни себе, ни ресурсу. Это фундаментальная задача: «можно ли разрешить приложению X читать мою почту в Gmail, не отдавая ему пароль от Google?»

Без OAuth ответ был «нет» или «отдай пароль и моли богов». С OAuth — «да, и в любой момент могу отозвать доступ конкретному приложению, не меняя пароль».

OAuth 2.0 (RFC 6749, 2012) — основа всех современных delegate-сценариев: «войти через Google», доступ Slack-app к workspace, GitHub Apps, Stripe Connect. OAuth 2.1 (черновик, финализация в 2026) — это консолидация best practices: deprecation Implicit и Password flows, обязательный PKCE для public clients, более строгие redirect URI checks.

В этом уроке пройдём роли, ключевые flows, sequence diagram Authorization Code и его защитные механизмы.


Четыре роли OAuth 2.0

OAuth 2.0 определяет четыре роли. Когда вы читаете спецификацию или интегрируетесь — важно их различать.

Четыре роли OAuth 2.0
Resource OwnerВладелец данных. Обычно -- конечный пользователь. Это человек, у которого есть аккаунт на Google и который решает, разрешить ли третьему сервису доступ
ClientПриложение, которое хочет получить доступ от имени пользователя. Например: Notion, который хочет читать Google Calendar пользователя
Authorization ServerСервер, который аутентифицирует пользователя и выдаёт токены. Для Google: accounts.google.com. Знает пароли пользователей, выдаёт scoped tokens
Resource ServerAPI, к которому клиент хочет обратиться. Для Google Calendar: googleapis.com/calendar. Принимает access token, возвращает данные

В малых системах authorization server и resource server — одно приложение. В больших (Google, AWS) — разделены: auth-сервер — на отдельном поддомене, выдаёт токены, не знает про конкретные ресурсы; resource-серверов десятки, они валидируют токены и отдают данные.


HTTP stateless: куки, сессии и state management

OAuth 2.0 vs 2.1: что меняется

OAuth 2.1 — не революция, а очистка 2.0 от устаревших и небезопасных паттернов. Финализация ожидается в 2026, но best practices уже применяются в новых проектах.

Главные изменения OAuth 2.1
Implicit flowOAuth 2.0 имел Implicit Grant для SPA: токен возвращался в URL. Уязвим к утечкам через Referer и истории браузера
Authorization Code + PKCEЗамена Implicit для SPA и mobile. PKCE добавляет защиту против перехвата authorization code
Password flowOAuth 2.0 разрешал клиенту запрашивать у пользователя пароль и обменивать его на токен. Противоречит самой идее OAuth ('не отдавай пароль приложению')
Authorization Code или DeviceИспользуем стандартные flows вместо костыля с паролем
PKCEРаньше -- только для mobile/SPA. Теперь -- обязательно для ВСЕХ public и confidential клиентов с Authorization Code
Refresh tokensОбязательная sender-constrained binding (DPoP) для public clients или короткий срок жизни. Защита от утечек

В практике 2026 года: даже если интегрируетесь по OAuth 2.0, используйте Authorization Code с PKCE везде, где можно. Implicit и Password не используйте никогда.


Три основных flow и где их применять

OAuth 2.0 определяет несколько grant types (flows). Из них в современной практике актуальны три.

Три актуальных flow
Authorization Code + PKCEГлавный flow для приложений, действующих от имени пользователя. Web apps, SPA, mobile, desktop. Самый безопасный и универсальный
Client CredentialsМашина-машина. Никакого пользователя в loop -- клиент приложение хочет доступ от своего имени. Backend ETL, batch jobs, internal services
Device CodeДля устройств без удобного браузера: TV, CLI, IoT. Пользователь идёт на отдельный URL с короткого кода и подтверждает доступ. GitHub CLI, Netflix on TV

Junior DE чаще всего сталкивается с Client Credentials — backend-ETL читает данные из external API от имени своего сервиса. Иногда с Authorization Code, когда нужно делать что-то от имени конкретного пользователя (например, OAuth-приложение для Slack).


Authorization Code flow с PKCE: пошагово

Это главный flow OAuth — пользователь логинится у auth-сервера, разрешает приложению доступ, приложение получает токен. С PKCE флоу выглядит так:

Authorization Code Flow с PKCE
User
Client App
Auth Server
Resource API
Generate code_verifier + code_challengeRedirect to /authorize?client_id=X&redirect_uri=Y&code_challenge=...&state=Z&scope=calendarLogin + consent (calendar.read)Redirect to redirect_uri?code=ABC&state=ZVerify state matchesPOST /token: code=ABC + code_verifier=...{access_token, refresh_token, expires_in}GET /calendar/events Authorization: Bearer ACCESS_TOKEN200 + JSON event list

Шесть точек, на которые обратить внимание:

  1. PKCE (шаги m1, m6) — клиент генерирует пару code_verifier/code_challenge. Auth-сервер связывает их с issued code. Перехватчик code не сможет обменять его на токен без verifier.
  2. state (шаги m2, m5) — случайная строка, отправляемая туда и обратно. Защищает от CSRF: если злоумышленник пытается обманом заставить пользователя «подтвердить» доступ для чужого приложения, state не совпадёт.
  3. scope (шаг m2) — список запрошенных разрешений (calendar.read, email, profile). Пользователь видит их в consent-экране и решает, дать ли все.
  4. Authorization code (шаг m4) — одноразовый, короткоживущий (5-10 минут), может быть обменян только один раз.
  5. redirect_uri (m2, m4) — должен быть зарегистрирован в auth-сервере заранее. Защита от перенаправления на чужой сайт.
  6. Token endpoint (шаг m6) — POST с client_id, code, code_verifier, redirect_uri. Возвращает access + refresh tokens.

PKCE: что внутри

PKCE (Proof Key for Code Exchange, RFC 7636) — это защита от перехвата authorization code. Сценарий атаки без PKCE:

  1. Mobile app открывает браузер для OAuth flow
  2. После одобрения браузер редиректит на myapp://callback?code=ABC
  3. Если на устройстве есть другое приложение, зарегистрированное на ту же URL-схему myapp://, оно может перехватить redirect и забрать code
  4. Перехватчик обменивает code на токен, получая доступ

С PKCE:

  1. App генерирует code_verifier (случайная строка) и хранит её локально
  2. Вычисляет code_challenge = base64url(SHA256(code_verifier)) и отправляет в auth-сервер вместе с запросом authorization
  3. Auth-сервер сохраняет code_challenge рядом с issued code
  4. Когда app обменивает code на токен, шлёт code_verifier в открытом виде
  5. Auth-сервер хеширует verifier и сравнивает с сохранённым challenge — если совпадает, выдаёт токен

Перехватчик кода не знает code_verifier (он остался в памяти app) — поэтому обмен не сработает.

import secrets
import hashlib
import base64

# 1. Генерация verifier
code_verifier = secrets.token_urlsafe(64)  # 64+ символов
print("verifier:", code_verifier)

# 2. Вычисление challenge
challenge_bytes = hashlib.sha256(code_verifier.encode()).digest()
code_challenge = base64.urlsafe_b64encode(challenge_bytes).rstrip(b"=").decode()
print("challenge:", code_challenge)

# 3. authorization URL включает challenge
auth_url = (
    f"https://auth.example.com/authorize?"
    f"client_id={CLIENT_ID}&"
    f"redirect_uri={REDIRECT_URI}&"
    f"response_type=code&"
    f"scope=read:user&"
    f"state={STATE}&"
    f"code_challenge={code_challenge}&"
    f"code_challenge_method=S256"
)

В OAuth 2.1 PKCE обязательно для всех клиентов (не только public). В 2026 — best practice везде, где есть Authorization Code.


Client Credentials flow: machine-to-machine

Это самый простой flow. Никакого пользователя — клиент идентифицирует себя, обменивает credentials на токен.

import requests

response = requests.post(
    "https://auth.example.com/oauth/token",
    data={
        "grant_type": "client_credentials",
        "client_id": "my-etl-service",
        "client_secret": "super-secret",
        "scope": "read:transactions",
    },
    timeout=10,
)
response.raise_for_status()
token_data = response.json()
print(token_data)
# {
#   "access_token": "eyJhbGc...",
#   "token_type": "Bearer",
#   "expires_in": 3600
# }

access_token = token_data["access_token"]

# Теперь зовём API
api_response = requests.get(
    "https://api.example.com/v1/transactions",
    headers={"Authorization": f"Bearer {access_token}"},
)

Используется когда:

  • Backend читает данные из API партнёра (ETL, синхронизация)
  • Сервис-к-сервису внутри одной компании, без контекста пользователя
  • Cron-jobs, batch processing

В отличие от Authorization Code, нет refresh-токена — когда access истекает, повторно вызываете client_credentials.

NOTE

Client Credentials — это OAuth-обёртка над тем же по сути паттерном, что API key. Разница: вы получаете короткоживущий access-токен (security benefit) вместо вечно живущего ключа. Большие платформы (Google Cloud, AWS, Azure) предлагают этот flow для service accounts.


Device Code flow: для CLI и TV

Сценарий: вы на TV хотите авторизоваться в Netflix. На TV нет удобной клавиатуры для ввода пароля. Решение — Device Code:

  1. TV запрашивает у auth-сервера device code и user code (короткий, типа BHWX-RDFL)
  2. TV показывает: «Откройте на телефоне netflix.com/activate и введите BHWX-RDFL»
  3. Пользователь делает это на телефоне, логинится, подтверждает доступ
  4. TV в это время поллит auth-сервер каждые 5 секунд: «авторизация подтверждена?»
  5. Когда пользователь подтвердил — TV получает access token
import time
import requests

# 1. Запрос device code
response = requests.post(
    "https://github.com/login/device/code",
    data={"client_id": "my-cli-tool", "scope": "repo"},
    headers={"Accept": "application/json"},
)
data = response.json()
print(f"Open {data['verification_uri']} and enter: {data['user_code']}")
device_code = data["device_code"]
interval = data["interval"]

# 2. Поллинг до подтверждения
while True:
    time.sleep(interval)
    poll = requests.post(
        "https://github.com/login/oauth/access_token",
        data={
            "client_id": "my-cli-tool",
            "device_code": device_code,
            "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
        },
        headers={"Accept": "application/json"},
    )
    result = poll.json()
    if "access_token" in result:
        print("Got token!")
        access_token = result["access_token"]
        break
    if result.get("error") == "authorization_pending":
        continue
    raise RuntimeError(f"Auth error: {result}")

GitHub CLI (gh auth login), Cloudflare Wrangler, kubectl с OIDC — все используют этот flow для удобной авторизации без ввода паролей в терминале.


Scopes: гранулярный доступ

Scopes — это разрешения, которые клиент запрашивает у пользователя. Имена scope не стандартизированы — каждый API определяет свой набор.

Примеры из крупных API:

  • Google: https://www.googleapis.com/auth/calendar.readonly, https://www.googleapis.com/auth/drive.file
  • GitHub: repo, user, admin:org, gist
  • Slack: channels:read, chat:write, users:read.email

Принцип least privilege: запрашивайте минимум, нужный для функционирования. Если вашему сервису нужно только читать публичные events Google Calendar — запрашивайте calendar.events.readonly, не calendar (полный доступ).

Scope-design на стороне auth-сервера
Грубые scopesfull_access, admin -- пользователь видит 'это приложение хочет ВСЁ'. Снижает доверие, отказы согласия выше
vs
Тонкие scopesusers.read, orders.write, payments.read -- пользователь видит конкретно, что разрешает. Больше доверия, легче анализировать инциденты
Read vs writeРазделять минимум на read/write. Пользователь не должен давать write-доступ приложению, которое только показывает данные
Hierarchyusers.* подразумевает users.read и users.write. Удобно для админских инструментов, требующих всех прав

В коде scope передаются в шагах authorization (от клиента к auth-серверу) и могут попадать в JWT-payload (scope: "read:users write:orders"). Resource-сервер на каждый запрос проверяет scope: «у этого токена есть write:orders для эндпоинта POST /orders?» — если нет, 403.


State и nonce: защита от CSRF и replay

state — случайная строка, передаваемая в /authorize и возвращаемая в callback. Защита от CSRF.

Сценарий атаки без state:

  1. Атакующий начинает OAuth flow для своего аккаунта Google и получает authorization code
  2. Атакующий заставляет жертву (через ссылку, фишинг) сделать GET на наш redirect_uri?code=ATTACKER_CODE
  3. Наш сервер обменивает code на токен и привязывает аккаунт атакующего к сессии жертвы
  4. Жертва теперь использует Google-аккаунт атакующего, не зная об этом

С state:

  1. Мы генерируем state и сохраняем в сессии жертвы
  2. Передаём в /authorize
  3. В callback проверяем, что присланный state совпадает с сохранённым
  4. Атакующий не знает state жертвы — атака не работает
import secrets

# При начале flow
state = secrets.token_urlsafe(32)
session["oauth_state"] = state

auth_url = f"...&state={state}"

# В callback
def callback(request):
    received_state = request.GET["state"]
    expected_state = session["oauth_state"]
    if received_state != expected_state:
        raise SecurityError("State mismatch -- possible CSRF")
    # ...continue with code exchange

nonce — аналогичная защита, но для OpenID Connect (OAuth-расширение для аутентификации). Nonce включается в id_token и проверяется при получении.


OAuth 2.0 vs OpenID Connect: одна буква разницы

OAuth 2.0 — это про авторизацию (что клиенту разрешено делать). Возвращает access_token, который — opaque или JWT — валидируется resource-сервером.

OpenID Connect (OIDC) — это надстройка над OAuth 2.0 для аутентификации (кто пользователь). Дополнительно к access_token возвращается id_token — JWT с claims о пользователе (sub, email, name, picture).

«Войти через Google/Apple/GitHub» — это OIDC, не голый OAuth. Разработчик получает id_token, парсит его, узнаёт email пользователя — и создаёт сессию в своём приложении.

# OIDC scope включает 'openid'
auth_url = f"...&scope=openid+profile+email&..."

# Token endpoint вернёт ещё id_token
{
  "access_token": "...",
  "id_token": "eyJhbGc...",  # JWT с email, sub, name
  "refresh_token": "...",
  "expires_in": 3600
}

# Декодируем id_token (с проверкой подписи!)
import jwt
user_info = jwt.decode(id_token, public_key, algorithms=["RS256"], audience=CLIENT_ID)
print(user_info["email"], user_info["sub"])

Junior DE редко занимается OIDC напрямую (это больше про user-facing applications), но знать про разницу полезно — особенно когда читаете чужие интеграции и видите id_token.


Какие библиотеки использовать

Писать OAuth flow руками — реально (как мы видели), но в продакшене обычно используют библиотеки.

Python-библиотеки для OAuth
requests-oauthlibДополнение к requests. Поддерживает OAuth 1 и 2. Подходит для standalone клиентов и backend-сервисов
authlibСовременная библиотека: OAuth 2.0 (client + server), OIDC, JWT. Хорошо документирована, активно поддерживается
google-authОфициальная библиотека Google. Удобна для интеграций с Google Cloud, Google Workspace. Service accounts, user OAuth -- всё внутри
msalMicrosoft Authentication Library. Для Microsoft Entra ID (бывший Azure AD), OneDrive, Microsoft 365 интеграций

Для большинства DE-задач (Client Credentials с external API партнёра) достаточно простого requests.post к token endpoint и requests.get с Bearer-заголовком. Сложности OAuth начинаются при user-facing flows (Authorization Code) — там стоит использовать готовую библиотеку.


Полный пример: Client Credentials в production-стиле

Соберём ETL-клиент, который получает токен по Client Credentials, кэширует его до истечения, автоматически перезапрашивает.

import time
import threading
from typing import Optional
import requests


class OAuthClient:
    def __init__(
        self,
        token_url: str,
        client_id: str,
        client_secret: str,
        scope: str,
    ):
        self.token_url = token_url
        self.client_id = client_id
        self.client_secret = client_secret
        self.scope = scope
        self._access_token: Optional[str] = None
        self._expires_at: float = 0
        self._lock = threading.Lock()

    def get_token(self) -> str:
        # Refresh за 60 секунд до истечения
        with self._lock:
            if self._access_token is None or time.time() >= self._expires_at - 60:
                self._refresh_token()
            return self._access_token

    def _refresh_token(self):
        response = requests.post(
            self.token_url,
            data={
                "grant_type": "client_credentials",
                "client_id": self.client_id,
                "client_secret": self.client_secret,
                "scope": self.scope,
            },
            timeout=10,
        )
        response.raise_for_status()
        data = response.json()
        self._access_token = data["access_token"]
        self._expires_at = time.time() + data["expires_in"]


def fetch_data(oauth: OAuthClient, base_url: str):
    token = oauth.get_token()
    response = requests.get(
        f"{base_url}/v1/transactions",
        headers={"Authorization": f"Bearer {token}"},
        timeout=30,
    )
    response.raise_for_status()
    return response.json()

Что здесь правильно:

  • Кэширование токена — не запрашиваем каждый раз
  • Threading lock — безопасно из нескольких потоков
  • Refresh за 60 секунд до экспирации — защита от race с in-flight запросами
  • Никаких токенов в логах
  • Timeout на все запросы

Итоги урока

OAuth 2.0 — протокол делегированного доступа. Четыре роли: Resource Owner (пользователь), Client (приложение), Authorization Server (выдаёт токены), Resource Server (отдаёт данные).

OAuth 2.1 — консолидация best practices: deprecated Implicit и Password flows, обязательный PKCE, sender-constrained refresh.

Три актуальных flow:

  • Authorization Code + PKCE — для всех user-facing приложений (web, SPA, mobile, desktop)
  • Client Credentials — машинно-машинные сценарии (ETL, backend-to-backend)
  • Device Code — TV, CLI, IoT

Защитные механизмы: PKCE (от перехвата code), state (от CSRF), nonce (от replay в OIDC), scopes (least privilege).

OAuth ≠ OpenID Connect: OAuth — авторизация (что разрешено), OIDC — аутентификация (кто пользователь, через id_token).

В Python: requests-oauthlib, authlib, google-auth, msal. Для простых Client Credentials достаточно сырого requests.

В следующем уроке — финальный security checklist для всего раздела аутентификации: что делать и не делать в практике Junior DE.


Проверка знанийKnowledge check
Команда DE выбирает OAuth flow для сценария: 'наш ETL-сервис каждый час должен скачивать список транзакций из API партнёра -- никакого пользователя в loop, только машина-машина'. Какой flow выбрать и почему НЕ Authorization Code?
ОтветAnswer
Правильный выбор -- Client Credentials flow. Authorization Code не подходит, потому что: (1) AC требует interactive consent от пользователя в браузере, чего в machine-to-machine сценарии нет; (2) AC возвращает refresh_token + access_token и предполагает работу 'от имени пользователя'; (3) AC requires redirect_uri и callback handling, что overkill для backend-сервиса. Client Credentials решает задачу элегантно: POST на token endpoint с client_id/client_secret/scope, получаем access_token (короткоживущий), используем для API. При экспирации -- повторный запрос (нет refresh, не нужен). Безопасно: client_secret хранится в secrets manager, никогда не передаётся в URL/логах. Альтернативы -- API key (даёт долгоживущий токен, риск утечки) или service account JWT (Google-стиль: подписать JWT приватным ключом, обменять на access_token). Client Credentials -- стандартный путь.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. Какие четыре роли определяет OAuth 2.0?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4