Authorization Code Flow и PKCE
Authorization Code Flow — это последовательность, с помощью которой приложение получает токены после входа пользователя. Вместо того чтобы сразу возвращать токен в браузер, сервер авторизации сначала возвращает короткоживущий одноразовый код. Приложение затем обменивает этот код на токены.
PKCE — дополнительная защита этого обмена. Она доказывает серверу, что код обменивает именно то приложение, которое начало вход, а не тот, кто случайно перехватил код.
Коротко:
пользователь входит
-> приложение получает временный code
-> приложение доказывает право на этот code
-> сервер выдаёт токены
Что означают названия
Authorization Code
Authorization Code — временный код авторизации. Это не Access Token и не подтверждение готовой сессии. Код нужен только для одного действия: обмена на токены через token endpoint.
Код возвращается на Redirect URI приложения. Если код попадёт в чужие руки, он всё равно не должен быть достаточным для получения токенов. Именно эту задачу дополнительно решает PKCE.
Flow
Flow означает последовательность сообщений между приложением, браузером пользователя и сервером авторизации. Вход не является одним HTTP-запросом: пользователь должен пройти аутентификацию, приложение должно получить callback, а затем завершить обмен кода.
PKCE
PKCE расшифровывается как Proof Key for Code Exchange, то есть «ключ-доказательство для
обмена кода». В начале flow приложение создаёт секретную случайную строку code_verifier.
Затем из неё вычисляется code_challenge, который можно передать серверу:
code_verifier = случайная строка, хранится у приложения code_challenge = BASE64URL(SHA256(code_verifier))
На первом шаге сервер получает только code_challenge. На втором шаге приложение возвращает
исходный code_verifier. Сервер сам вычисляет challenge ещё раз и сравнивает результаты.
Важно: code_verifier не должен передаваться в первом authorization request. Если отправить
его сразу, проверка потеряет смысл.
Как проходит вход
Рассмотрим сценарий с браузерным приложением.
- Пользователь нажимает кнопку «Войти».
- Приложение создаёт
code_verifier,stateиnonce. - Приложение вычисляет
code_challengeиз verifier. - Приложение перенаправляет браузер на authorization endpoint.
- Пользователь входит и, если нужно, подтверждает запрошенные scopes.
- Сервер возвращает браузер на зарегистрированный Redirect URI с
codeиstate. - Приложение проверяет, что полученный
stateсовпадает с сохранённым. - Приложение отправляет
codeи исходныйcode_verifierна token endpoint. - Сервер проверяет code, Redirect URI, client и соответствие verifier.
- После успешной проверки сервер возвращает токены.
В Authorization Code Flow пароль пользователя не передаётся приложению. Пароль вводится на стороне authorization server, а приложение получает только результат согласованного протокола.
Стандарт
На уровне протокола это договор между клиентом и authorization server:
- клиент заранее сообщает challenge, но не раскрывает исходный verifier;
- authorization server связывает выданный code с этим challenge;
- при обмене клиент предъявляет verifier;
- сервер выдаёт токены только после успешной проверки связи между двумя значениями.
Authorization Code Flow отвечает за разделение авторизации и выдачи токенов на два связанных этапа. PKCE добавляет к этому обмену доказательство владения verifier. Поэтому code можно передать через браузерный callback, не превращая сам callback в канал передачи готовых токенов.
┌──────────┐ ┌──────────────────────┐
│ Клиент │ │ Authorization Server │
└────┬─────┘ └──────────┬───────────┘
│ │
│ code_verifier │
│ code_challenge = S256(code_verifier) │
│ │
│ 1. Запрос авторизации │
│ ────────────────────────────────────────>│
│ │
│ 2. Вход пользователя │
│<─────────────────────────────────────────│
│ authorization code │
│ │
│ 3. code + code_verifier │
│ ────────────────────────────────────────>│
│ │
│ сервер проверяет: │
│ S256(code_verifier) == code_challenge│
│ │
│ 4. ID Token + Access Token │
│<─────────────────────────────────────────│
code_challenge передаётся в первом запросе, а исходный code_verifier остаётся у клиента до
обмена кода. Поэтому одного перехваченного authorization code недостаточно: без verifier сервер
не должен выдавать токены.
Представьте, что authorization code — это номерок, который выдают на входе, а code_verifier —
секретная часть квитанции, оставшаяся у настоящего клиента. Перехватчик может увидеть номерок,
но не может завершить обмен без второй части.
Практика
Public client должен использовать PKCE, потому что его код выполняется на устройстве или
в браузере. client_secret нельзя считать секретом, если он попадает в frontend bundle.
Зачем flow разделён на два шага
Authorization code короткоживущий и предназначен только для однократного обмена. Если его
перехватят, злоумышленнику дополнительно понадобится code_verifier, который остаётся
у исходного клиента. Поэтому code не заменяет токен и не должен использоваться приложением
как доказательство успешной сессии.
У этого flow есть три разные проверки:
stateсвязывает callback с запросом, который начал именно этот клиент;nonceсвязывает ID Token с конкретной попыткой аутентификации;- PKCE связывает authorization code с клиентом, который создал verifier.
Эти значения решают разные задачи и не являются взаимозаменяемыми.
Public и Confidential
Для Public client секрет хранить негде: пользователь может посмотреть JavaScript и сетевые
запросы. Поэтому приложение использует client_id и PKCE. Confidential client выполняет обмен
на backend и дополнительно подтверждает себя client_secret.
Выбор типа подключения должен следовать архитектуре, а не названию фреймворка. Next.js с серверным callback может быть Confidential, а Next.js SPA-сценарий — Public. Критерий один: где реально выполняется обмен code и где хранится секрет.
Что делает SDK
SDK Authoriza:
- создаёт state, nonce и PKCE-пару;
- сохраняет данные незавершённого flow;
- проверяет callback;
- получает discovery и ключи JWKS;
- проверяет ID Token;
- сохраняет сессию;
- обновляет Access Token через Refresh Token;
- сообщает ошибку через
onError.
Ошибки
INVALID_STATE обычно означает потерю данных flow, повторное использование callback или
несоответствие state. Ошибка nonce указывает на то, что ID Token не связан с начатым flow.
Ошибка redirect URI означает, что URL запроса не совпал с зарегистрированным в App.
Не исправляйте такие ошибки отключением проверок. Сначала сравните client ID, issuer, Redirect URI и состояние storage.
Как проверить результат
- при старте входа в браузере появляется authorization request с
code_challenge; - callback содержит
codeиstate; - SDK отклоняет изменённый state;
- SDK отклоняет ID Token с неверным nonce;
- после обмена code пользователь появляется в
auth.user; - повторный вызов
login()при активной сессии не запускает новый flow.
Параметры запроса и их назначение
В authorization request обычно участвуют client_id, redirect_uri, response_type=code,
scope, state, nonce, code_challenge и code_challenge_method=S256.
Каждый параметр отвечает за свою часть безопасности:
client_idвыбирает зарегистрированный App;redirect_uriопределяет допустимый callback;response_type=codeвыбирает возврат временного кода;scopeопределяет запрошенные данные и возможности;stateсвязывает callback с начатым запросом;nonceсвязывает ID Token с этим flow;code_challengeсвязывает code с исходным клиентом.
Нельзя удалять параметры только потому, что «вход работает и без них» в тестовой среде. Некоторые проверки проявляются только при атаке или в случае параллельных вкладок.
Параллельные попытки входа
Пользователь может дважды нажать кнопку входа или открыть несколько вкладок. Клиент должен понимать, какой flow активен, и не перезаписывать его данные непредсказуемо. SDK Authoriza хранит flow отдельно и не запускает конфликтующие операции без обработки состояния.
После callback его параметры нужно убрать из адресной строки. Это снижает риск повторной обработки code и не оставляет OAuth-параметры в истории страницы.
Отказ и ошибки провайдера
Пользователь может отменить вход. В callback вместо code тогда приходит OAuth error,
например login_required или другой код, предусмотренный сервером. Приложение должно показать
понятное состояние и не создавать сессию по одному наличию state.
Проверка вручную без SDK
Если используется OIDC-библиотека напрямую, не копируйте реализацию SDK частями. Проверьте, что выбранная библиотека поддерживает discovery, PKCE, state, nonce, callback validation, token parsing и refresh. Если хотя бы одной критичной части нет, лучше выбрать другую библиотеку.
Пример результата для приложения
После callback приложение не должно считать наличие code признаком входа. SDK завершает flow,
проверяет ответ и только затем публикует состояние:
const auth = createAuthoriza({
clientId: 'YOUR_CLIENT_ID',
redirectUri: 'https://example.com/auth/callback',
});
const user = await auth.getUser();
if (user) {
// Открываем защищённую часть приложения.
}
Не запускайте второй обмен code вручную поверх SDK. Это может привести к повторному использованию authorization code или конфликту с сохранённым state.
Как это реализовано в Authoriza SDK
SDK Authoriza автоматически создаёт и проверяет state, nonce и PKCE-пару, обрабатывает callback и обменивает code на токены. Для Public connection секрет не нужен. Redirect URI должен заранее присутствовать в настройках App.
Чем Authoriza может быть полезна
Если вы хотите использовать готовую реализацию этого flow для JavaScript и TypeScript, SDK Authoriza берёт на себя повторяющиеся проверки и управление сессией. После понимания процесса откройте руководство SDK.