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 Server | API, защищённое access token | api.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
Самый безопасный и рекомендуемый поток. Работает в несколько шагов:
- Клиент перенаправляет пользователя на страницу входа IdP.
- Пользователь входит и соглашается с запрашиваемыми правами (consent).
- IdP перенаправляет обратно с одноразовым authorization code в URL.
- Клиент обменивает 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 |
| 5 | IdP проверяет, что хеш 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 Token | JWT | Клиент (браузер, приложение) | Идентичность пользователя: sub, email, name |
| Access Token | JWT или opaque | Resource Server (API) | Доступ к защищённым endpoint |
| Refresh Token | Opaque string | Confidential client или BFF | Получение нового access token без повторного входа |
ID Token (JWT)
JWT (JSON Web Token) — подписанный JSON с тремя частями: header.payload.signature. Клиент может прочитать payload, но не может подделать подпись.
Типичные claims в ID Token:
| Claim | Значение | Зачем нужен |
|---|---|---|
iss | Issuer — URL IdP | Проверка, кто выпустил токен |
sub | Subject — ID пользователя | Уникальный идентификатор в IdP |
aud | Audience — ваш client_id | Токен предназначен именно вам |
exp | Expiration — Unix timestamp | Срок действия |
iat | Issued at | Время выпуска |
email | Email (если 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 |
email | Email и флаг verified | Регистрация, уведомления |
offline_access | Refresh 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-ить codejwks_uri— публичные ключи для проверки JWTuserinfo_endpoint— дополнительные claims о пользователе
Используйте discovery вместо хардкода URL. При смене endpoint у провайдера ваш код продолжит работать.
Backend-for-Frontend (BFF)
Для SPA часто строят BFF (Backend-for-Frontend) — тонкий backend-слой между браузером и IdP.
Шаги BFF-потока
- SPA вызывает
GET /auth/loginна BFF. - BFF генерирует
state,code_verifier, сохраняет в server-side session. - BFF редиректит браузер на IdP
/authorize. - IdP возвращает на
https://bff.example.com/auth/callback?code=... - BFF обменивает code на токены (с
client_secret). - BFF сохраняет refresh token в зашифрованной server-side session.
- BFF ставит HttpOnly Secure SameSite cookie для SPA.
- 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 match | IdP принимает похожие URL | Account takeover через подмену redirect |
| Access token в URL fragment | Токен в history, logs, Referer | Утечка токена |
Нет проверки state | CSRF на OAuth callback | Привязка чужой сессии к жертве |
| JWT без проверки signature | Любой payload принимается | Полный обход авторизации |
| Implicit flow в новом коде | response_type=token | Токен в URL, устаревший стандарт |
| Refresh token в localStorage | XSS крадёт долгоживущий токен | Длительный доступ атакующего |
| Слишком широкие 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:
- Удалить server-side session (BFF).
- Вызвать IdP end session (опционально
id_token_hint). - Очистить cookie на клиенте.
- Инвалидировать 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 |
| MFA | TOTP, WebAuthn, SMS |
| Fine-grained roles | Realm roles, client roles |
| User federation | LDAP, Active Directory |
Типичная схема: Keycloak — единый IdP для всех внутренних сервисов; внешние пользователи входят через social login, сотрудники — через LDAP.
Отладка интеграции
| Симптом | Вероятная причина | Что проверить |
|---|---|---|
invalid_redirect_uri | Несовпадение URI | Консоль IdP и код |
invalid_grant | Code уже использован или истёк | Обменять code один раз, быстро |
invalid_client | Неверный secret или client_id | Vault, env vars |
| CORS error на /token | Прямой запрос из браузера | Используйте BFF или PKCE правильно |
aud mismatch | Разные client_id | Настройки API и IdP |
| Бесконечный redirect loop | Cookie blocked, SameSite | Настройки cookie, HTTPS |
Инструменты: jwt.io (только декодирование), OAuth 2.0 Playground Google, логи Keycloak Events.
Интеграция с популярными провайдерами
Google
- Создайте проект в Google Cloud Console.
- APIs & Services → Credentials → OAuth 2.0 Client ID.
- Тип: Web application. Добавьте authorized redirect URIs.
- Scopes:
openid,profile,emailдля login; дополнительные — в Google API Console.
Discovery URL: https://accounts.google.com/.well-known/openid-configuration
GitHub
- Settings → Developer settings → OAuth Apps → New OAuth App.
- Authorization callback URL — exact match.
- Scopes:
read:userдля login;repo— только при необходимости доступа к коду.
GitHub поддерживает OIDC для GitHub Actions (не путать с OAuth App для пользователей).
Yandex ID
- Yandex OAuth — зарегистрируйте приложение.
- Права: login, email, avatar.
- Redirect URI — HTTPS, exact match.
Подходит для проектов с аудиторией в РФ. Проверяйте актуальность документации на yandex.ru/dev/id.
Keycloak (self-hosted)
- Создайте Realm, Client (type: OpenID Connect).
- Access Type:
publicдля SPA,confidentialдля backend. - Valid Redirect URIs, Web Origins для CORS.
- Mappers для custom claims (roles, groups).
Keycloak удобен, когда нужен единый IdP для десятков внутренних сервисов и social login как брокер.
Machine-to-Machine (M2M)
Сервисы без пользователя используют Client Credentials Flow:
| Параметр | Рекомендация |
|---|---|
| TTL access token | 5–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 IdP | Auth0 Organizations, Keycloak realms per customer |
| Home realm discovery | Email domain → нужный IdP (@corp.com → corp Okta) |
| Just-in-time provisioning | Создание user при первом входе из claims |
При OIDC federation ваше приложение — один client, но пользователи приходят из разных upstream IdP.
Тестирование OAuth интеграции
| Тест | Ожидание |
|---|---|
| Valid login | ID Token + access token, session создана |
| Invalid redirect_uri | Ошибка от IdP, code не выдан |
| Expired code | invalid_grant при обмене |
| Missing state | Клиент отклоняет callback |
| Tampered JWT | API возвращает 401 |
| Revoked refresh | Повторный login required |
| Wrong aud on API | 401/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
| Шаг | Действие | Срок |
|---|---|---|
| 1 | Rotate client_secret в IdP | 15 мин |
| 2 | Update secret в Vault, redeploy BFF | 30 мин |
| 3 | Revoke active refresh tokens client | 1 ч |
| 4 | Audit token endpoint logs | 4 ч |
| 5 | Check for abnormal API usage | 24 ч |
| 6 | Post-mortem, short TTL tokens | 1 неделя |
M2M clients — rotation без downtime через dual secret window в Keycloak.
Runbook — массовый сбой login (IdP outage)
| Шаг | Действие |
|---|---|
| 1 | Status page — "Login temporarily unavailable" |
| 2 | Existing sessions продолжают работать (JWT TTL) |
| 3 | Failover Keycloak replica или switch to backup IdP |
| 4 | Extend JWT TTL только при явном риске — документировать |
| 5 | Post-incident — RTO login < 30 min target |
Держите break-glass local admin только для emergency, audit каждого использования.
Compliance — OIDC и 152-ФЗ
| Требование | Реализация в OIDC | Артефакт |
|---|---|---|
| Идентификация субъекта | sub claim stable | IdP config export |
| Учёт входов | IdP event log → SIEM | Retention policy |
| Минимизация данных | Scopes openid email only | Client registration |
| Трансграничная передача | IdP hosted in РФ | DPA с провайдером |
| Отзыв доступа | Disable user + token revoke | HR offboarding runbook |
Yandex ID и корпоративный Keycloak on-prem закрывают локализацию. Social login через foreign IdP для internal apps — согласовать с DPO.
Расширенный пример — SPA + BFF + Keycloak в РФ
Fintech web app, пользователи в РФ, PCI scope на payment API.
| Компонент | Выбор |
|---|---|
| Frontend | React SPA, no tokens in localStorage |
| BFF | Node.js, HttpOnly cookie session |
| IdP | Keycloak HA on Selectel |
| Social | Yandex ID через broker |
| API | JWT validation aud=payments-api |
| CI | GitLab OIDC → cloud deploy role |
Flow:
- SPA redirect на BFF
/auth/login. - BFF PKCE к Keycloak, cookie
sessionпосле callback. - API calls через BFF proxy с injected service token или user JWT.
- Logout — BFF вызывает Keycloak end_session + clear cookie.
Pen test checklist — CSRF на BFF, cookie flags, refresh rotation.
Сравнение IdP для проектов в РФ
| IdP | Hosting | MFA | Federation | Типичный кейс |
|---|---|---|---|---|
| Keycloak | Self-hosted | TOTP, WebAuthn | SAML, OIDC | Enterprise internal |
| Yandex ID | Cloud RU | Yandex app | OAuth broker | B2C РФ |
| VK ID | Cloud RU | VK app | OAuth | B2C молодёжная аудитория |
| Google Workspace | Global | Yes | OIDC | Mixed, DPO review |
| Active Directory FS | On-prem | AD MFA | SAML | Legacy enterprise |
Default для regulated — Keycloak on-prem plus WebAuthn для admin.
Сравнение библиотек OIDC client
| Библиотека | Язык | PKCE | BFF-friendly | Примечание |
|---|---|---|---|---|
| openid-client | Node.js | Да | Да | Зрелая |
| Authlib | Python | Да | Да | Flask/FastAPI |
| Spring Authorization Client | Java | Да | Да | Enterprise |
| go-oidc | Go | Да | Да | Microservices |
| oidc-client-ts | Browser | Да | 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 / RuStore | Universal Links, assetlinks.json |
| ГОСТ TLS | Termination на gateway, не в app |
| Биометрия | Local only, не отправлять в IdP |
| Offline mode | Short-lived access, refresh on reconnect |
Custom URL scheme myapp://callback — fallback, prefer HTTPS redirect с PKCE.
Runbook — invalid_grant при обмене code
| Шаг | Действие |
|---|---|
| 1 | Проверить code одноразовый — повторный submit |
| 2 | Clock skew client/server — NTP |
| 3 | redirect_uri exact match при обмене |
| 4 | PKCE code_verifier совпадает с challenge |
| 5 | Code TTL ~60 sec — медленный network |
Логируйте token endpoint errors без secret values — только error code и client_id.
Compliance — session fixation
| Контроль | Реализация |
|---|---|
| New session после login | Regenerate session ID |
| Binding session to user agent | Optional fingerprint |
| Concurrent session limit | IdP policy |
| Idle timeout | 15–30 min internal apps |
Проверяйте на pentest — OAuth flow без session rotation после callback.
Расширенный пример — corporate SSO для 5000 users
| Компонент | Масштаб |
|---|---|
| Keycloak cluster | 3 nodes HA |
| Session store | Redis cluster |
| LDAP federation | Active Directory read-only |
| Token TTL | Access 15 min, refresh 8 h |
| Admin MFA | WebAuthn mandatory |
Load test token endpoint — 100 req/s sustained перед go-live. Rate limit brute force на /token.
Compliance — журналирование входов для аудита
| Событие | Поля | Retention |
|---|---|---|
| login success | sub, ip, user_agent | 1 год |
| login fail | username attempt, ip | 90 д |
| token grant | client_id, scope | 1 год |
| admin action | admin sub, target | 3 года |
Экспорт в SIEM on-prem. Доступ к логам — role security-auditor only.
Связанные материалы
- Passkeys / WebAuthn — альтернатива и дополнение паролю, phishing-resistant фактор.
- Авторизация и аутентификация — базовые понятия.
- Уязвимости API — IDOR, broken auth.
- 12 советов по безопасности API.
- OAuth 2.0 RFC 6749 — полная спецификация OAuth.
- OIDC Core — спецификация OpenID Connect.
- RFC 7636 PKCE — Proof Key for Code Exchange.