Перейти к основному содержимому

OIDC и OAuth для разработчика

Разработчику

Кнопка "Войти через Google", "Войти через GitHub" или "Войти через Yandex" встречается почти в каждом современном приложении. За ней стоят два связанных стандарта: OAuth 2.0 и OpenID Connect (OIDC). Эта статья объясняет, как они работают, что должен знать разработчик при интеграции и какие ошибки приводят к утечкам аккаунтов.

OAuth 2.0 — протокол делегирования доступа. Он отвечает на вопрос: "Может ли приложение X читать мои репозитории на GitHub?" Пользователь явно даёт разрешение, и приложение получает access token для обращения к API от его имени.

OpenID Connect (OIDC) — надстройка над OAuth 2.0 для аутентификации. Она отвечает на вопрос: "Кто этот пользователь?" Результат — ID Token (обычно JWT) с данными о личности: идентификатор, email, имя.

В большинстве случаев кнопка "Войти через …" реализуется через OIDC Authorization Code Flow с PKCE (Proof Key for Code Exchange). Это стандарт де-факто для SPA, мобильных приложений и современных веб-клиентов.

Базовая теория аутентификации и авторизации — в статье Авторизация и аутентификация. Passkeys как дополнение к паролю — в Passkeys и WebAuthn.


Простая аналогия

Представьте гостиницу:

  • Пользователь (Resource Owner) — гость, владелец номера.
  • Клиент (Client) — ваше приложение, которое хочет что-то сделать от имени гостя.
  • Сервер авторизации (Authorization Server, IdP) — стойка регистрации. Проверяет личность и выдаёт "пропуск".
  • Сервер ресурсов (Resource Server) — номер или сейф. Принимает только действительный пропуск (access token).

OAuth не передаёт пароль пользователя вашему приложению. Пользователь вводит пароль только на странице IdP (Google, Keycloak, Auth0). Ваше приложение получает токены для доступа — без пароля и логина в своей базе.


Роли в протоколе

РольКто этоПример
Resource OwnerПользователь, владелец данныхIvan Petrov
ClientПриложение, запрашивающее доступSPA на React, backend на Go, iOS-приложение
Authorization Server (IdP)Выдаёт токены после входаGoogle, GitHub, Keycloak, Auth0, Yandex ID
Resource ServerAPI, защищённое access tokenapi.myapp.com, GitHub REST API

IdP (Identity Provider) — провайдер идентичности. Он хранит учётные записи, проверяет пароли или MFA и выпускает токены.

Client бывает двух типов:

  • Public client — не может хранить секрет (SPA в браузере, мобильное приложение). Обязателен PKCE.
  • Confidential client — backend-сервер с client_secret, хранящимся в Vault или переменных окружения. Может обменивать code на токены на сервере.

Подробнее о хранении секретов — практикум Vault.


Основные потоки OAuth

OAuth 2.0 описывает несколько "потоков" (flows) — способов получить токен. Для новых проектов важны два.

Authorization Code Flow

Самый безопасный и рекомендуемый поток. Работает в несколько шагов:

  1. Клиент перенаправляет пользователя на страницу входа IdP.
  2. Пользователь входит и соглашается с запрашиваемыми правами (consent).
  3. IdP перенаправляет обратно с одноразовым authorization code в URL.
  4. Клиент обменивает code на токены через защищённый POST-запрос к /token.

Code живёт несколько секунд и бесполезен без client_secret (у confidential client) или без PKCE verifier (у public client).

Client Credentials Flow

Сервис обращается к API от своего имени, без участия пользователя. Пример: nightly job читает внутренний API. Используется client_id + client_secret, пользовательский consent не нужен.

Устаревшие потоки

Implicit flow и Resource Owner Password Credentials считаются небезопасными для нового кода. Не используйте их в 2025–2026. Если видите response_type=token в URL — это повод для рефакторинга.


Authorization Code + PKCE

PKCE (произносится "pixy") защищает public client от перехвата authorization code. Атакующий, перехвативший code из redirect URL, не сможет обменять его на токены без секретного code_verifier.

Как работает PKCE

ШагЧто происходит
1Клиент генерирует случайный code_verifier (43–128 символов)
2Вычисляет code_challenge = BASE64URL(SHA256(code_verifier))
3Отправляет code_challenge и метод S256 в /authorize
4При обмене code передаёт оригинальный code_verifier
5IdP проверяет, что хеш verifier совпадает с challenge

code_verifier никогда не попадает в URL redirect — только в POST на /token. Перехват redirect не даёт токенов.

Пример параметров authorize

GET https://accounts.google.com/o/oauth2/v2/auth
?client_id=YOUR_CLIENT_ID
&redirect_uri=https://app.example.com/callback
&response_type=code
&scope=openid%20profile%20email
&state=random-csrf-token-32chars
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256

state — случайная строка для защиты от CSRF. Клиент сохраняет её до redirect и сверяет при возврате. Без проверки state злоумышленник может подставить свой code в сессию жертвы.


Токены и их назначение

ТокенФорматКто используетНазначение
ID TokenJWTКлиент (браузер, приложение)Идентичность пользователя: sub, email, name
Access TokenJWT или opaqueResource Server (API)Доступ к защищённым endpoint
Refresh TokenOpaque stringConfidential client или BFFПолучение нового access token без повторного входа

ID Token (JWT)

JWT (JSON Web Token) — подписанный JSON с тремя частями: header.payload.signature. Клиент может прочитать payload, но не может подделать подпись.

Типичные claims в ID Token:

ClaimЗначениеЗачем нужен
issIssuer — URL IdPПроверка, кто выпустил токен
subSubject — ID пользователяУникальный идентификатор в IdP
audAudience — ваш client_idТокен предназначен именно вам
expExpiration — Unix timestampСрок действия
iatIssued atВремя выпуска
emailEmail (если scope email)Отображение в UI

Пример декодированного payload (без подписи):

{
"iss": "https://accounts.google.com",
"sub": "1234567890",
"aud": "my-app-client-id.apps.googleusercontent.com",
"email": "ivan@example.com",
"email_verified": true,
"name": "Ivan Petrov",
"exp": 1718450000,
"iat": 1718446400
}

Клиент проверяет aud, iss, exp перед доверием ID Token. Подпись проверяется по JWKS IdP (публичные ключи по URL вроде /.well-known/jwks.json).

Access Token

Access Token предъявляется API в заголовке:

Authorization: Bearer eyJhbGciOiJSUzI1NiIs...

API валидирует токен: подпись, exp, aud (если JWT), scopes. ID Token на backend API не подставляют — это разные токены для разных задач.

Opaque access token — случайная строка без структуры. API вызывает introspection endpoint IdP, чтобы узнать, активен ли токен и какие у него scopes.

Refresh Token

Позволяет получить новый access token без повторного входа пользователя. Хранится только у confidential client или на BFF-сервере. Никогда не кладите refresh token в localStorage SPA.

Рекомендуется refresh token rotation: при каждом обновлении IdP выдаёт новый refresh token и инвалидирует старый. При повторном использовании старого — блокировка всей цепочки (признак кражи).


Scopes и принцип минимальных прав

Scope — строка, описывающая запрашиваемое право. IdP показывает их пользователю на экране consent.

ScopeЧто даётКогда запрашивать
openidОбязателен для OIDC, включает ID TokenВсегда при входе
profileИмя, аватарОтображение в UI
emailEmail и флаг verifiedРегистрация, уведомления
offline_accessRefresh tokenДолгие сессии (осторожно)
repo (GitHub)Доступ к репозиториямТолько если реально нужен

Запрашивайте минимум scopes. Пользователь видит список прав и может отказать. Лишние scopes снижают конверсию и увеличивают риск при утечке токена.

Для API-first архитектуры ваш backend — confidential client. Он хранит client_secret в Vault, обменивает code на токены на сервере и отдаёт SPA только session cookie.


Discovery и метаданные IdP

OIDC IdP публикует конфигурацию по стандартному URL:

GET https://accounts.google.com/.well-known/openid-configuration

Ответ содержит:

  • authorization_endpoint — куда редиректить пользователя
  • token_endpoint — куда POST-ить code
  • jwks_uri — публичные ключи для проверки JWT
  • userinfo_endpoint — дополнительные claims о пользователе

Используйте discovery вместо хардкода URL. При смене endpoint у провайдера ваш код продолжит работать.


Backend-for-Frontend (BFF)

Для SPA часто строят BFF (Backend-for-Frontend) — тонкий backend-слой между браузером и IdP.

Шаги BFF-потока

  1. SPA вызывает GET /auth/login на BFF.
  2. BFF генерирует state, code_verifier, сохраняет в server-side session.
  3. BFF редиректит браузер на IdP /authorize.
  4. IdP возвращает на https://bff.example.com/auth/callback?code=...
  5. BFF обменивает code на токены (с client_secret).
  6. BFF сохраняет refresh token в зашифрованной server-side session.
  7. BFF ставит HttpOnly Secure SameSite cookie для SPA.
  8. SPA делает запросы к BFF; BFF проксирует к API с access token.

Преимущества BFF

  • Refresh token никогда не попадает в браузер.
  • Меньше поверхность XSS: кража cookie сложнее без HttpOnly.
  • client_secret остаётся на сервере.
  • Единая точка logout и session revocation.

Пример JWT для API — практикум REST/WebSocket.


SPA без BFF (только PKCE)

Если BFF нет, SPA использует PKCE и хранит access token в памяти (переменная JavaScript, не localStorage). При перезагрузке страницы пользователь входит заново, либо используется silent refresh через скрытый iframe (сложнее, не всегда работает с third-party cookies).

ПодходХранение токенаРиск XSS
localStorageПостоянноВысокий — любой скрипт читает
In-memoryДо перезагрузкиСредний — пока вкладка открыта
HttpOnly cookie через BFFНа сервереНизкий — JS не видит cookie

Мобильные приложения

Мобильный client — тоже public client. Используйте PKCE и custom URL scheme или App Links / Universal Links для redirect:

com.myapp://oauth/callback?code=abc&state=xyz

На Android и iOS зарегистрируйте схему в манифесте. Не используйте http://localhost в production mobile.

Для особо чувствительных приложений (банкинг) рассмотрите native SDK IdP или passkeys — Passkeys и WebAuthn.


Валидация JWT на API

Backend при получении access token (JWT) обязан проверить:

ПроверкаЧто ломается при пропуске
Подпись (JWKS)Подделка любого payload
expИспользование просроченного токена
issТокен от чужого IdP
audТокен, выпущенный для другого client
Scopes / rolesДоступ к чужим ресурсам

Пример псевдокода (Go-подобный):

token, err := jwt.Parse(accessToken, func(t *jwt.Token) (interface{}, error) {
return jwks.GetKey(t.Header["kid"])
})
claims := token.Claims
if claims["aud"] != expectedAudience { return ErrInvalidAudience }
if claims["exp"] < time.Now().Unix() { return ErrExpired }

Никогда не декодируйте JWT только через base64 без проверки подписи. Библиотека jwt-decode в браузере — только для чтения claims на клиенте, не для авторизации на сервере.


Типовые ошибки и их последствия

ОшибкаКак проявляетсяПоследствие
redirect_uri без exact matchIdP принимает похожие URLAccount takeover через подмену redirect
Access token в URL fragmentТокен в history, logs, RefererУтечка токена
Нет проверки stateCSRF на OAuth callbackПривязка чужой сессии к жертве
JWT без проверки signatureЛюбой payload принимаетсяПолный обход авторизации
Implicit flow в новом кодеresponse_type=tokenТокен в URL, устаревший стандарт
Refresh token в localStorageXSS крадёт долгоживущий токенДлительный доступ атакующего
Слишком широкие scopesТокен с admin при loginРасширенный ущерб при утечке
Hardcoded client_secret в SPAСекрет в bundle.jsПолная компрометация client

Подробнее об уязвимостях API — 8.07/128, 2.09/132.


Настройка redirect_uri

IdP требует точного совпадения redirect URI. Типичная ошибка — зарегистрировать https://app.example.com/callback, а в коде отправлять https://app.example.com/callback/ (слэш в конце). Запрос будет отклонён или, хуже, при неаккуратной настройке — примет похожий URL атакующего.

Чек-лист redirect:

  • HTTPS в production (HTTP только для localhost в dev).
  • Один зарегистрированный URI на окружение (dev, staging, prod).
  • Без wildcard в production (https://*.example.com — опасно).
  • Проверка state на каждом callback.

Logout и отзыв сессии

OIDC поддерживает end session endpoint. При logout приложение редиректит на IdP, который завершает SSO-сессию.

Для полного logout:

  1. Удалить server-side session (BFF).
  2. Вызвать IdP end session (опционально id_token_hint).
  3. Очистить cookie на клиенте.
  4. Инвалидировать refresh token (revocation endpoint, если есть).

Без шага 2 пользователь нажмёт "Войти через Google" и войдёт без пароля (активная SSO-сессия у IdP).


OIDC в CI/CD

GitHub Actions может получать доступ к AWS без статических ключей через OIDC federation.

В workflow:

permissions:
id-token: write
contents: read

steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/github-deploy
aws-region: eu-central-1

Trust policy в AWS привязывает роль к конкретному репозиторию и ветке. Статические AWS_ACCESS_KEY_ID в secrets больше не нужны.

Тот же паттерн — DevSecOps, Supply chain. Аналогично работают GitLab CI, Azure DevOps и другие платформы с OIDC.


Keycloak как self-hosted IdP

Для корпоративных проектов часто разворачивают Keycloak — open source IdP с OIDC, SAML, социальными провайдерами.

ВозможностьОписание
RealmsИзолированные пространства пользователей
Identity brokering"Войти через Google" внутри Keycloak
MFATOTP, WebAuthn, SMS
Fine-grained rolesRealm roles, client roles
User federationLDAP, Active Directory

Типичная схема: Keycloak — единый IdP для всех внутренних сервисов; внешние пользователи входят через social login, сотрудники — через LDAP.


Отладка интеграции

СимптомВероятная причинаЧто проверить
invalid_redirect_uriНесовпадение URIКонсоль IdP и код
invalid_grantCode уже использован или истёкОбменять code один раз, быстро
invalid_clientНеверный secret или client_idVault, env vars
CORS error на /tokenПрямой запрос из браузераИспользуйте BFF или PKCE правильно
aud mismatchРазные client_idНастройки API и IdP
Бесконечный redirect loopCookie blocked, SameSiteНастройки cookie, HTTPS

Инструменты: jwt.io (только декодирование), OAuth 2.0 Playground Google, логи Keycloak Events.


Интеграция с популярными провайдерами

Google

  1. Создайте проект в Google Cloud Console.
  2. APIs & Services → Credentials → OAuth 2.0 Client ID.
  3. Тип: Web application. Добавьте authorized redirect URIs.
  4. Scopes: openid, profile, email для login; дополнительные — в Google API Console.

Discovery URL: https://accounts.google.com/.well-known/openid-configuration

GitHub

  1. Settings → Developer settings → OAuth Apps → New OAuth App.
  2. Authorization callback URL — exact match.
  3. Scopes: read:user для login; repo — только при необходимости доступа к коду.

GitHub поддерживает OIDC для GitHub Actions (не путать с OAuth App для пользователей).

Yandex ID

  1. Yandex OAuth — зарегистрируйте приложение.
  2. Права: login, email, avatar.
  3. Redirect URI — HTTPS, exact match.

Подходит для проектов с аудиторией в РФ. Проверяйте актуальность документации на yandex.ru/dev/id.

Keycloak (self-hosted)

  1. Создайте Realm, Client (type: OpenID Connect).
  2. Access Type: public для SPA, confidential для backend.
  3. Valid Redirect URIs, Web Origins для CORS.
  4. Mappers для custom claims (roles, groups).

Keycloak удобен, когда нужен единый IdP для десятков внутренних сервисов и social login как брокер.


Machine-to-Machine (M2M)

Сервисы без пользователя используют Client Credentials Flow:

ПараметрРекомендация
TTL access token5–15 минут
ScopesМинимум для операции
client_secretТолько Vault, rotation
AuditЛогировать каждый token request

Не используйте user password grant для M2M. Не давайте M2M токену scopes пользовательского admin.


Multi-tenant и organizations

B2B SaaS часто требует: пользователь входит через корпоративный IdP заказчика (SAML или OIDC federation).

ПодходОписание
Multi-tenant IdPAuth0 Organizations, Keycloak realms per customer
Home realm discoveryEmail domain → нужный IdP (@corp.com → corp Okta)
Just-in-time provisioningСоздание user при первом входе из claims

При OIDC federation ваше приложение — один client, но пользователи приходят из разных upstream IdP.


Тестирование OAuth интеграции

ТестОжидание
Valid loginID Token + access token, session создана
Invalid redirect_uriОшибка от IdP, code не выдан
Expired codeinvalid_grant при обмене
Missing stateКлиент отклоняет callback
Tampered JWTAPI возвращает 401
Revoked refreshПовторный login required
Wrong aud on API401/403

Используйте OWASP ZAP или ручные тесты по OAuth 2.0 Security BCP.


Чек-лист разработчика

  • Authorization Code + PKCE для SPA и mobile
  • Confidential client + secret в Vault для backend
  • Проверка state на каждом callback
  • Exact match redirect_uri
  • Минимальные scopes
  • JWT signature + exp + aud + iss на API
  • Refresh token только server-side (BFF)
  • HttpOnly Secure cookie вместо localStorage
  • Logout с end session IdP
  • Нет Implicit flow и Password grant

Runbook — компрометация client_secret

ШагДействиеСрок
1Rotate client_secret в IdP15 мин
2Update secret в Vault, redeploy BFF30 мин
3Revoke active refresh tokens client1 ч
4Audit token endpoint logs4 ч
5Check for abnormal API usage24 ч
6Post-mortem, short TTL tokens1 неделя

M2M clients — rotation без downtime через dual secret window в Keycloak.


Runbook — массовый сбой login (IdP outage)

ШагДействие
1Status page — "Login temporarily unavailable"
2Existing sessions продолжают работать (JWT TTL)
3Failover Keycloak replica или switch to backup IdP
4Extend JWT TTL только при явном риске — документировать
5Post-incident — RTO login < 30 min target

Держите break-glass local admin только для emergency, audit каждого использования.


Compliance — OIDC и 152-ФЗ

ТребованиеРеализация в OIDCАртефакт
Идентификация субъектаsub claim stableIdP config export
Учёт входовIdP event log → SIEMRetention policy
Минимизация данныхScopes openid email onlyClient registration
Трансграничная передачаIdP hosted in РФDPA с провайдером
Отзыв доступаDisable user + token revokeHR offboarding runbook

Yandex ID и корпоративный Keycloak on-prem закрывают локализацию. Social login через foreign IdP для internal apps — согласовать с DPO.


Расширенный пример — SPA + BFF + Keycloak в РФ

Fintech web app, пользователи в РФ, PCI scope на payment API.

КомпонентВыбор
FrontendReact SPA, no tokens in localStorage
BFFNode.js, HttpOnly cookie session
IdPKeycloak HA on Selectel
SocialYandex ID через broker
APIJWT validation aud=payments-api
CIGitLab OIDC → cloud deploy role

Flow:

  1. SPA redirect на BFF /auth/login.
  2. BFF PKCE к Keycloak, cookie session после callback.
  3. API calls через BFF proxy с injected service token или user JWT.
  4. Logout — BFF вызывает Keycloak end_session + clear cookie.

Pen test checklist — CSRF на BFF, cookie flags, refresh rotation.


Сравнение IdP для проектов в РФ

IdPHostingMFAFederationТипичный кейс
KeycloakSelf-hostedTOTP, WebAuthnSAML, OIDCEnterprise internal
Yandex IDCloud RUYandex appOAuth brokerB2C РФ
VK IDCloud RUVK appOAuthB2C молодёжная аудитория
Google WorkspaceGlobalYesOIDCMixed, DPO review
Active Directory FSOn-premAD MFASAMLLegacy enterprise

Default для regulated — Keycloak on-prem plus WebAuthn для admin.


Сравнение библиотек OIDC client

БиблиотекаЯзыкPKCEBFF-friendlyПримечание
openid-clientNode.jsДаДаЗрелая
AuthlibPythonДаДаFlask/FastAPI
Spring Authorization ClientJavaДаДаEnterprise
go-oidcGoДаДаMicroservices
oidc-client-tsBrowserДаSPA onlyС BFF предпочтительнее server

Не собирайте OAuth flow вручную — используйте проверенную библиотеку с auto discovery.


OIDC в GitLab CI для облака РФ

GitLab 16+ поддерживает OIDC id_token для Yandex Cloud и других провайдеров.

deploy:
id_tokens:
YC_OIDC_TOKEN:
aud: https://gitlab.example.com
script:
- yc iam create-token --jwt-file $YC_OIDC_TOKEN
- yc managed-kubernetes cluster get-credentials ...

Преимущества:

  • Нет static service account key в CI variables;
  • TTL token минуты;
  • Audit в GitLab и cloud IAM.

Подробнее — OIDC в CI/CD выше и GitOps.


Региональная специфика — мобильные приложения в РФ

ТребованиеПрактика
App Store / RuStoreUniversal Links, assetlinks.json
ГОСТ TLSTermination на gateway, не в app
БиометрияLocal only, не отправлять в IdP
Offline modeShort-lived access, refresh on reconnect

Custom URL scheme myapp://callback — fallback, prefer HTTPS redirect с PKCE.


Runbook — invalid_grant при обмене code

ШагДействие
1Проверить code одноразовый — повторный submit
2Clock skew client/server — NTP
3redirect_uri exact match при обмене
4PKCE code_verifier совпадает с challenge
5Code TTL ~60 sec — медленный network

Логируйте token endpoint errors без secret values — только error code и client_id.


Compliance — session fixation

КонтрольРеализация
New session после loginRegenerate session ID
Binding session to user agentOptional fingerprint
Concurrent session limitIdP policy
Idle timeout15–30 min internal apps

Проверяйте на pentest — OAuth flow без session rotation после callback.


Расширенный пример — corporate SSO для 5000 users

КомпонентМасштаб
Keycloak cluster3 nodes HA
Session storeRedis cluster
LDAP federationActive Directory read-only
Token TTLAccess 15 min, refresh 8 h
Admin MFAWebAuthn mandatory

Load test token endpoint — 100 req/s sustained перед go-live. Rate limit brute force на /token.


Compliance — журналирование входов для аудита

СобытиеПоляRetention
login successsub, ip, user_agent1 год
login failusername attempt, ip90 д
token grantclient_id, scope1 год
admin actionadmin sub, target3 года

Экспорт в SIEM on-prem. Доступ к логам — role security-auditor only.


Связанные материалы


Содержание