← назад к разделу

Вы нажимаете «Войти», приложение вдруг уносит вас на чужую страницу с логотипом 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 и роли.

Что на схеме: кто кому что передаёт в общих чертах, чтобы держать картину в голове.

diagram

Главная идея: сначала код, потом токены

Прежде чем разбирать шаги, поймём центральную хитрость, ради которой всё и затеяно.

Самый наивный способ — чтобы Keycloak просто вернул токен прямо в браузер, в адресную строку. Но адресная строка — публичное место: URL попадает в историю браузера, в журналы серверов, в логи прокси, его видно соседнему расширению. Класть туда токен — всё равно что писать пароль на стикере и носить на лбу.

Поэтому Authorization Code Flow разбивает доставку на два этапа:

  1. Сначала Keycloak возвращает в браузер не токен, а authorization code — одноразовый короткоживущий код. Сам по себе он бесполезен: предъявить его API нельзя, данные по нему не получишь.
  2. Потом приложение отдельным запросом (не через адресную строку, а напрямую на сервер Keycloak) меняет этот код на настоящие токены.

Смысл такого разделения: код летит через «грязный» канал (браузер), а токены — через «чистый» (прямой серверный запрос). Даже если кто-то подсмотрит код в адресной строке, без второго шага он ничего не получит.

Поток по шагам

Теперь разберём весь поток целиком, по одному шагу, с пояснением «зачем» на каждом.

Что на схеме: полная последовательность — от нажатия «Войти» до обмена кода на токены. Это та самая картина, которую держат в голове, когда говорят «Authorization Code Flow».

diagram

А теперь те же шаги словами.

Шаг 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 обратно на /token Keycloak и получает свежий access_token, не дёргая пользователя заново. refresh_token ходит только обратно в Keycloak, больше никуда.

Что на схеме: куда какой токен направляется. Видно, что три токена движутся в три разные стороны.

diagram

Отдельно отметим: бывает, что 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) одноразовым секретом, который рождается внутри приложения и наружу никогда не выходит:

  1. Перед стартом приложение генерирует случайную строку — code_verifier. Это и есть секрет; он остаётся внутри приложения и никуда не отправляется.
  2. От него считается хеш: code_challenge = BASE64URL(SHA256(code_verifier)). Метод хеширования называется S256.
  3. На шаге 2 (redirect на логин) приложение отправляет в Keycloak code_challenge и code_challenge_method=S256. Keycloak запоминает этот challenge рядом с кодом, который потом выдаст.
  4. На шаге 7 (обмен кода) приложение присылает уже сам code_verifier.
  5. Keycloak заново считает хеш от присланного verifier и сверяет с запомненным challenge. Совпало — выдаёт токены. Не совпало — отказ.

Что на схеме: где появляется challenge, а где verifier. Видно, что секрет (verifier) уходит только на финальном прямом запросе.

diagram

В чём суть защиты: перехватчик кода (шаг 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/SameSite cookie; 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 и частые ошибки.