Вы нажимаете «Войти», приложение вдруг уносит вас на чужую страницу с логотипом Keycloak, вы вводите там пароль — и оказываетесь обратно в приложении уже залогиненным. Между этими двумя кликами происходит целая цепочка обменов, и если не знать, что там внутри, всё выглядит как магия. Эта магия называется Authorization Code Flow, и сейчас мы разберём её медленно, по одному шагу, объясняя на каждом шаге не только «что происходит», но и «зачем именно так».
Почему приложение не спрашивает пароль само
Начнём с самого первого вопроса, который обычно никто не задаёт вслух: почему приложение не показывает свою собственную форму логина и не спрашивает пароль прямо у себя? Так ведь было бы проще — одна форма, никаких перенаправлений.
Проблема в том, что тогда приложение увидит ваш пароль. А приложений, в которые вы заходите одним аккаунтом, может быть десяток. Доверить настоящий пароль каждому из них — значит размножить его по всем этим серверам. Достаточно взломать один из них или встроить в него зловредный код — и пароль утёк сразу от всего.
Идея OAuth2 и OpenID Connect устроена иначе: пароль знает только один сервер — Keycloak. По-английски такой сервер называют Identity Provider (IdP) или сервером авторизации. Все остальные приложения пароль не видят вообще никогда. Вместо пароля они получают от Keycloak токены — короткие подписанные «пропуска», которые подтверждают, что вы — это вы, и что вам разрешено то-то и то-то.
Отсюда вырастает главная техническая задача: как доставить токен от Keycloak до приложения так, чтобы по дороге его никто не перехватил? Ответ на этот вопрос и есть Authorization Code Flow.
Кто здесь кто
Чтобы дальше не путаться, договоримся о четырёх ролях. Это стандартные термины OAuth2, они будут встречаться везде:
- Resource Owner — это вы, живой пользователь. Владелец данных, к которым кто-то хочет получить доступ.
- Client — приложение, которое хочет вас пустить и потом ходить за данными: фронтенд в браузере, мобильное приложение или серверный бэкенд.
- Authorization Server — Keycloak. Хранит пользователей и пароли, проверяет вход, выдаёт токены.
- Resource Server — ваш API, который проверяет присланный токен и отдаёт данные, если токен в порядке.
В Keycloak всё это живёт внутри realm — изолированного пространства со своими пользователями, ролями и настройками. Каждое приложение регистрируется внутри realm как client. Подробнее эта модель разобрана в отдельной статье про realm, client и роли.
Что на схеме: кто кому что передаёт в общих чертах, чтобы держать картину в голове.
Главная идея: сначала код, потом токены
Прежде чем разбирать шаги, поймём центральную хитрость, ради которой всё и затеяно.
Самый наивный способ — чтобы Keycloak просто вернул токен прямо в браузер, в адресную строку. Но адресная строка — публичное место: URL попадает в историю браузера, в журналы серверов, в логи прокси, его видно соседнему расширению. Класть туда токен — всё равно что писать пароль на стикере и носить на лбу.
Поэтому Authorization Code Flow разбивает доставку на два этапа:
- Сначала Keycloak возвращает в браузер не токен, а authorization code — одноразовый короткоживущий код. Сам по себе он бесполезен: предъявить его API нельзя, данные по нему не получишь.
- Потом приложение отдельным запросом (не через адресную строку, а напрямую на сервер Keycloak) меняет этот код на настоящие токены.
Смысл такого разделения: код летит через «грязный» канал (браузер), а токены — через «чистый» (прямой серверный запрос). Даже если кто-то подсмотрит код в адресной строке, без второго шага он ничего не получит.
Поток по шагам
Теперь разберём весь поток целиком, по одному шагу, с пояснением «зачем» на каждом.
Что на схеме: полная последовательность — от нажатия «Войти» до обмена кода на токены. Это та самая картина, которую держат в голове, когда говорят «Authorization Code Flow».
А теперь те же шаги словами.
Шаг 1. Вы нажимаете «Войти». Пока ничего особенного — обычный клик в приложении.
Шаг 2. Приложение перенаправляет браузер на Keycloak. Оно формирует адрес /authorize сервера Keycloak и кладёт в него параметры запроса. Разберём их, потому что каждый зачем-то нужен:
client_id— кто спрашивает. По нему Keycloak понимает, какое именно приложение пришло.redirect_uri— куда вернуть пользователя после входа. Keycloak пустит редирект только на адрес, заранее прописанный в настройках клиента, — это защита от того, чтобы вас увели на чужой сайт.response_type=code— «я хочу authorization code» (а не токен сразу). Именно эта строчка включает безопасный двухэтапный поток.scope=openid ...— что приложение хочет узнать. Значениеopenidобязательно, если нужен вход через OpenID Connect.state— случайная строка, которую приложение запоминает и сверяет на возврате. Защищает от подделки запроса (CSRF) — чтобы вернувшийся ответ точно соответствовал тому запросу, что вы начали.
Шаг 3. Keycloak показывает свою страницу входа. Браузер оказывается на домене Keycloak. Это важный момент: форма пароля — на стороне Keycloak, не приложения. Приложение в этот момент пароль не видит и видеть не может.
Шаг 4. Вы вводите логин и пароль. Keycloak их проверяет, при необходимости спрашивает второй фактор. Всё это происходит внутри Keycloak.
Шаг 5. Keycloak возвращает браузер обратно — с кодом. Вход успешен, и Keycloak перенаправляет браузер на тот самый redirect_uri, дописав в адрес code=ABC.... Это и есть authorization code. Подчеркнём ещё раз: это не токен, а одноразовый код, который сам по себе ничего не открывает.
Шаг 6. Код попадает в приложение. Браузер открывает redirect_uri приложения, и приложение достаёт код из адреса. Заодно оно сверяет state — тот ли это запрос, что начинали.
Шаг 7. Приложение меняет код на токены. Оно делает отдельный POST-запрос на /token — token endpoint Keycloak — и кладёт туда полученный код. Ключевой момент: этот запрос идёт напрямую на сервер Keycloak, минуя адресную строку браузера. Confidential-клиент (бэкенд с секретом) тут же предъявляет свой client_secret, доказывая, что он — это он.
Шаг 8. Keycloak отдаёт токены. В ответ на корректный обмен приходят три разных токена, и у каждого своя роль — их легко перепутать, поэтому разберём отдельно ниже.
После этого приложение уже может ходить в ваш API, прикладывая access_token — но это уже за рамками самого flow.
Три токена и их роли
В ответе на шаге 8 приходят сразу три токена. Это самое частое место путаницы, поэтому разложим строго:
- access_token — пропуск к API. Приложение кладёт его в заголовок
Authorization: Bearer <access_token>каждого запроса к вашему API. Это единственный токен, который ходит в API. - id_token — «удостоверение личности» для самого приложения: кто залогинился (имя, email, идентификатор). Это часть OpenID Connect. id_token нужен клиенту, чтобы знать, кого он впустил; в API его не отправляют.
- refresh_token — «талон на обновление». Когда access_token протухнет (а живёт он недолго, обычно минуты), приложение шлёт refresh_token обратно на
/tokenKeycloak и получает свежий access_token, не дёргая пользователя заново. refresh_token ходит только обратно в Keycloak, больше никуда.
Что на схеме: куда какой токен направляется. Видно, что три токена движутся в три разные стороны.
Отдельно отметим: бывает, что access_token не JWT, а opaque — непрозрачная строка, которую API не может проверить локально и ходит к Keycloak спросить «этот токен ещё живой?» (introspection). Это не «четвёртый токен», а другой формат того же access_token: by-value (JWT, проверяется на месте по JWKS) против by-reference (opaque, проверяется запросом в Keycloak). Деталей касаться не будем — это отдельная тема про устройство и проверку токенов.
Зачем нужен PKCE
Вернёмся к шагу 5–6. Код прилетает в браузер. А что, если на устройстве затаилось вредоносное приложение или зловредное расширение, которое перехватит этот код в момент возврата? Тогда злоумышленник сам пойдёт на шаг 7 и выменяет украденный код на токены — и зайдёт под вами.
Особенно остро это для публичных клиентов — SPA в браузере и мобильных приложений. У них нет надёжного места, чтобы спрятать секрет: весь код приложения открыт, и любой client_secret, зашитый внутрь, рано или поздно вытащат. То есть на шаге 7 публичный клиент не может доказать «я — это я» через секрет. Значит, перехватчику кода ничто не мешает обменять код самому.
PKCE (Proof Key for Code Exchange, произносят «пикси») закрывает именно эту дыру. Идея — связать старт (шаг 2) и обмен (шаг 7) одноразовым секретом, который рождается внутри приложения и наружу никогда не выходит:
- Перед стартом приложение генерирует случайную строку —
code_verifier. Это и есть секрет; он остаётся внутри приложения и никуда не отправляется. - От него считается хеш:
code_challenge = BASE64URL(SHA256(code_verifier)). Метод хеширования называется S256. - На шаге 2 (redirect на логин) приложение отправляет в Keycloak
code_challengeиcode_challenge_method=S256. Keycloak запоминает этот challenge рядом с кодом, который потом выдаст. - На шаге 7 (обмен кода) приложение присылает уже сам
code_verifier. - Keycloak заново считает хеш от присланного verifier и сверяет с запомненным challenge. Совпало — выдаёт токены. Не совпало — отказ.
Что на схеме: где появляется challenge, а где verifier. Видно, что секрет (verifier) уходит только на финальном прямом запросе.
В чём суть защиты: перехватчик кода (шаг 5–6) знает только код и, возможно, challenge — но не знает code_verifier, ведь тот никогда не покидал приложение. А без verifier обменять украденный код на токены невозможно.
Важно использовать именно метод S256, а не plain (где challenge просто равен verifier без хеширования) — plain почти не даёт защиты, потому что в редиректе тогда виден сам секрет.
В Keycloak PKCE включается в настройках клиента: Advanced → Proof Key for Code Exchange Code Challenge Method → S256. Для публичных клиентов это обязательная настройка.
Почему implicit flow больше не используют
Раньше для SPA существовал упрощённый вариант — implicit flow. В нём Keycloak возвращал в браузер сразу access_token прямо в адресной строке (response_type=token), без промежуточного кода и без второго запроса. Сделали так когда-то потому, что браузеры тогда не умели нормально слать запросы на другой домен.
Проблема ровно та, с которой мы начали: токен летит через адрес страницы. Он оседает в истории браузера, в журналах серверов и прокси, его видно в момент редиректа. И аккуратно обновлять его (через refresh_token) тоже было нельзя.
Сегодня браузеры спокойно делают кросс-доменные запросы (через CORS), поэтому костыль больше не нужен. Implicit flow считается устаревшим и небезопасным. В OAuth 2.1 — следующей редакции стандарта — его убирают совсем, а Authorization Code Flow с PKCE становится способом по умолчанию для всех клиентов, включая бэкенд.
Вывод простой: всегда используйте Authorization Code Flow с PKCE. Опцию implicit flow в настройках клиента Keycloak (Implicit Flow Enabled) держите выключенной.
Где хранить токены после получения
Допустим, токены получены. Куда их положить? От этого напрямую зависит безопасность, и тут есть две разные стратегии.
Вариант 1: токены на бэкенде (рекомендуется для веба). Весь Authorization Code Flow проводит ваш серверный бэкенд. Полученные access_token и refresh_token он держит у себя, в серверной сессии. Браузеру отдаётся только сессионная cookie — с флагами HttpOnly, Secure, SameSite. JavaScript на странице до такой cookie не доберётся в принципе. Каждый запрос фронтенда идёт через бэкенд, тот сам подставляет нужный токен и зовёт API. Этот приём называют BFF (Backend for Frontend). Так токены вообще не лежат в браузере — и даже при XSS-атаке их нечего красть.
Вариант 2: токены в браузере (только если бэкенда нет совсем). Чистый SPA без своего сервера вынужден хранить токены прямо в браузере — в памяти страницы или в localStorage. Это уязвимо: любой зловредный скрипт, попавший на страницу через XSS, прочитает localStorage и заберёт токены. PKCE защищает только сам обмен кода — но не хранение токенов после него. Поэтому localStorage — наименее желательный вариант; как минимум держите access_token коротким, а refresh_token — в HttpOnly-cookie.
Простое правило: есть бэкенд — храните токены там (BFF), а в браузер отдавайте только защищённую cookie. Это резко сужает поверхность для атаки.
Как это включается в Spring
Если приложение на Spring выступает как client, берётся стартер spring-boot-starter-oauth2-client. Он сам проводит Authorization Code Flow, складывает токены в серверную сессию и заворачивает фронтенд в cookie. Для публичного клиента (без секрета — client-authentication-method: none) Spring сам добавляет PKCE — ничего руками считать не нужно:
spring:
security:
oauth2:
client:
provider:
keycloak:
issuer-uri: https://keycloak.example.com/realms/my-realm
registration:
keycloak:
client-id: my-spa
authorization-grant-type: authorization_code
client-authentication-method: none
scope: openid, profile, email
Включить защиту — буквально одна строка oauth2Login():
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(a -> a.anyRequest().authenticated())
.oauth2Login(Customizer.withDefaults())
.build();
}
А ваш API (resource server) про логин вообще ничего не знает. Его задача проще: получить access_token в заголовке Authorization: Bearer ..., проверить его подпись по открытым ключам Keycloak (JWKS, адрес которых берётся из issuer-uri) и пустить. Это отдельная тема — см. статью про интеграцию со Spring Security.
Коротко
- Пароль знает только Keycloak; приложение пароль не видит и получает вместо него токены.
- Поток идёт в два этапа: сначала браузер приносит одноразовый authorization code, потом приложение меняет code на токены отдельным прямым запросом на
/token. - Код летит через браузер, токены — нет; даже перехваченный код бесполезен без второго шага.
- На обмене приходят три токена: access_token (в API, через
Authorization: Bearer), id_token (кто залогинился — для клиента, в API не шлётся), refresh_token (только обратно в Keycloak, чтобы обновить access_token). - PKCE связывает старт и обмен секретом
code_verifier: на старте уходит хеш (code_challenge+S256), а сам verifier — только при обмене. Перехваченный код без verifier не обменять. - PKCE обязателен для публичных клиентов — SPA и мобильных, у которых негде спрятать
client_secret. - Implicit flow устарел (отдавал токен прямо в адрес страницы); в OAuth 2.1 его убрали. Всегда — Code Flow с PKCE.
- Токены лучше хранить на бэкенде (BFF), отдавая в браузер только
HttpOnly/Secure/SameSitecookie;localStorageуязвим к XSS. - В Spring клиентскую часть закрывает
spring-boot-starter-oauth2-client+oauth2Login(); API проверяет токен отдельно.
Что почитать дальше
- OAuth2 и OIDC простыми словами — роли, access/refresh/id_token и чем доступ отличается от «кто ты», если нужна база перед этой статьёй.
- Realm, client, роли и пользователи в Keycloak — что такое realm и client, public против confidential, и как роли попадают в токен.
- Keycloak и Spring Security: проверка токенов — как ваш API проверяет подпись access_token по JWKS.
- Токены Keycloak: проверка, refresh, отзыв и ошибки — устройство JWT, обновление через refresh_token, logout и частые ошибки.