Appearance
Направление: Web-приложение
Authorization Code Flow с PKCE — стандартный вариант для приложений с браузером на стороне пользователя: классические веб-сайты, мобильные приложения с системным браузером/WebView.
Предполагается, что клиент уже создан (02-client.md) и вы знаете client_id, client_secret и хотя бы один зарегистрированный redirect_uri.
Только confidential-клиенты. /par и /token в этой системе всегда проверяют client_secret — публичных клиентов (только PKCE, без секрета) не поддерживается. Это значит, что весь этот флоу, кроме самого редиректа на /authorize (Шаг 1), должен выполняться на вашей стороне сервера, а не в браузере пользователя. Если ваше приложение — SPA без собственного бэкенда, client_secret нельзя встраивать в клиентский код (он окажется виден в devtools/network любому пользователю) — заведите тонкий серверный компонент (backend-for-frontend), который хранит секрет и выполняет Шаг 2 и обмен refresh_token вместо браузера; сама SPA работает с уже полученной сессией через свой бэкенд, а не напрямую с этим SSO.
PKCE обязателен по умолчанию для всех клиентов на /authorize и /par. Отключить его можно точечно для конкретного клиента полем requirePkce: false в админке — это нужно только confidential-клиентам, чьи встроенные OIDC-библиотеки не умеют отправлять code_challenge (например, LiteLLM). Если PKCE у вашего клиента отключён, шаги 0 и связанные с ним параметры ниже можно пропустить — Шаг 1 отправляется без code_challenge/code_challenge_method, а на Шаге 2 code_verifier не передаётся.
Шаг 0 — сгенерировать PKCE-пару
На каждый запрос входа генерируется новая пара: случайная строка code_verifier и её хэш code_challenge.
js
const codeVerifier = base64url(crypto.randomBytes(32));
const codeChallenge = base64url(sha256(codeVerifier));code_verifier храните на своей стороне (сессия/cookie) до шага 2 — на клиент он не отправляется.
Шаг 1 — редирект пользователя на /authorize
GET /authorize
?response_type=code
&client_id=<ваш client_id>
&redirect_uri=<ваш redirect_uri, должен точно совпадать с зарегистрированным>
&scope=openid profile email offline_access
&state=<случайная строка против CSRF>
&code_challenge=<code_challenge>
&code_challenge_method=S256| Параметр | Обязателен | Заметка |
|---|---|---|
response_type | ✓ | Всегда code |
redirect_uri | ✓ | Точное совпадение с одним из значений в настройках клиента |
scope | ✓ | Минимум openid. Добавьте offline_access, если нужен refresh_token |
state | рекомендован | Проверяйте на своей стороне при возврате |
nonce | — | Если проверяете привязку ID Token к конкретному запросу |
prompt=consent | — | Принудительно показать экран согласия, даже если пользователь уже давал его раньше |
Если у клиента задана организация с политикой входа (mfaRequired, allowedEmailDomains и т.д.) — все проверки выполняются здесь автоматически, до того как пользователь попадёт на ваш redirect_uri. Что при этом видит пользователь — appendix-login-policies.md.
Результат: редирект на redirect_uri?code=<code>&state=<state>, либо на redirect_uri?error=... при отказе (см. таблицу ошибок в 06-operations.md).
Pushed Authorization Requests (опционально)
Если не хотите передавать параметры авторизации через query string браузера (логи прокси, history, Referer), отправьте их напрямую на сервер до редиректа:
POST /par (те же параметры, что в Шаге 1, плюс client_secret)В ответ — одноразовый request_uri, который живёт expires_in секунд (обычно 90). Дальше редирект делается с ним вместо полного набора параметров:
GET /authorize?client_id=<client_id>&request_uri=<request_uri>Не обязателен — прямой способ (Шаг 1 целиком) продолжает работать как раньше.
Шаг 2 — обмен кода на токены
Код действителен 10 минут, одноразовый.
bash
curl -X POST http://localhost:3002/token \
-d "grant_type=authorization_code" \
-d "code=<code из шага 1>" \
-d "redirect_uri=<тот же redirect_uri, что в шаге 1>" \
-d "client_id=<client_id>" \
-d "client_secret=<client_secret>" \
-d "code_verifier=<code_verifier из шага 0>"json
{
"access_token": "<JWT>",
"token_type": "Bearer",
"expires_in": 3600,
"id_token": "<JWT>",
"refresh_token": "<opaque>",
"scope": "openid profile email offline_access"
}refresh_token приходит только если запрашивался scope offline_access. Дальше — обычная работа с сессией на своей стороне: сохраните access_token/refresh_token, свяжите с локальной сессией пользователя.
sid в ID Token. Сохраните его — это идентификатор серверной сессии SSO. Он понадобится, чтобы сматчить входящий logout_token (back-channel logout ниже) со своей локальной сессией.
Шаг 3 — обновление токена
bash
curl -X POST http://localhost:3002/token \
-d "grant_type=refresh_token" \
-d "refresh_token=<refresh_token>" \
-d "client_id=<client_id>" \
-d "client_secret=<client_secret>"Возвращает новую пару access_token/refresh_token (rotation) — старый refresh_token немедленно отзывается. Не переиспользуйте отозванный refresh token — повторный вызов с ним вернёт invalid_grant.
Экран согласия
При первом входе (и повторно, если запрошен prompt=consent или добавлен новый scope) пользователь видит экран согласия с названием и логотипом вашего приложения (см. брендирование в 02-client.md). Отказ пользователя — редирект с error=access_denied на ваш redirect_uri. Список выданных пользователем согласий и их отзыв — на его стороне, /settings/authorized-apps.
Logout
RP-Initiated Logout
Приложение само инициирует выход, отправив пользователя на:
GET /logout?id_token_hint=<id_token>&post_logout_redirect_uri=<uri>&state=<state>id_token_hint— ранее полученный ID Token (подпись проверяется, истечение допускается).post_logout_redirect_uri— обязан быть заранее внесён вpostLogoutRedirectUrisклиента, иначе игнорируется.state— вернётся как query-параметр при редиректе обратно.
Back-Channel Logout
Если у клиента задан backchannelLogoutUri, при завершении сессии (в том числе через /account/sessions со стороны пользователя) SSO отправит server-to-server POST:
POST <backchannelLogoutUri>
Content-Type: application/x-www-form-urlencoded
logout_token=<подписанный RS256 JWT>json
{
"iss": "http://localhost:3002",
"sub": "<userId>",
"aud": "<client_id>",
"sid": "<sessionId>",
"events": { "http://schemas.openid.net/event/backchannel-logout": {} }
}На своей стороне: проверьте подпись через /jwks (что он возвращает — 02-client.md), iss/aud, наличие events, отсутствие nonce, и завершите локальную сессию с этим sid — тем самым, который вы сохранили на Шаге 2. Таймаут доставки — 3 секунды; недоступность вашего эндпоинта не блокирует выход пользователя.
Front-Channel Logout
Резервный канал для тех, кто не может принимать server-to-server вызовы. Если задан frontchannelLogoutUri, он подгружается скрытым <iframe sandbox> на странице /logout. Работает только для текущей сессии — при удалённом завершении чужой сессии (см. ниже) front-channel технически невозможен, браузера той сессии уже нет.
«Выйти со всех устройств»
Пользователь может завершить сразу все свои сессии через /account/sessions → «Выйти со всех устройств» (POST /account/logout-everywhere). Для вашего приложения это неотличимо от нескольких обычных back-channel-уведомлений подряд — по одному на каждую активную сессию пользователя.
Дальше
Весь флоу выше одной последовательностью реальных curl-запросов, от логина до /userinfo и обновления токена, — reference/appendix-curl-walkthrough.md.
Справочник кодов ошибок, лимиты запросов и план на компрометацию client_secret — 06-operations.md.