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

Вы открываете приложение, оно отправляет вас на отдельную страницу логина Keycloak, вы вводите пароль — и попадаете обратно уже залогиненным. Между этими двумя кликами происходит обмен, который и называется Authorization Code Flow. Разберём его по шагам и поймём, зачем там нужна добавка под названием PKCE.

Зачем вообще отдельная страница логина

Простой вопрос: почему приложение не спрашивает пароль само, в своей форме? Тогда оно бы видело ваш пароль. А значит, доверять пароль пришлось бы каждому приложению, в которое вы заходите. Один взломанный сайт — и пароль утёк.

Идея OAuth2 и OpenID Connect другая: пароль знает только Keycloak (это сервер авторизации, по-английски Identity Provider, IdP). Приложение никогда не видит ваш пароль. Вместо пароля оно получает от Keycloak токены — короткие подписанные «пропуска», которые подтверждают, кто вы.

Осталось решить главную задачу: как передать токен приложению так, чтобы его не перехватили по дороге. Ответ на это и есть Authorization Code Flow.

Главные роли

Чтобы дальше было понятно, договоримся о терминах:

  • Resource Owner — это вы, пользователь.
  • Client — приложение, которое хочет вас пустить (фронтенд, мобильное приложение, бэкенд).
  • Authorization Server — Keycloak. Хранит пользователей и пароли, выдаёт токены.
  • Resource Server — ваш API, который проверяет токен и отдаёт данные.

В Keycloak всё живёт внутри realm — это изолированное пространство со своими пользователями и настройками. А каждое приложение регистрируется как client внутри realm.

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

Идея в том, чтобы не передавать токен прямо в браузер через адресную строку. Сначала приложение получает одноразовый код (authorization code), а уже потом меняет его на токены отдельным запросом «из-под капота».

Вот как это выглядит целиком:

 Браузер / приложение        Keycloak              Ваш API
        │                       │                     │
   1. жму «Войти»               │                     │
        ├── редирект ──────────►│                     │
        │   /authorize?...      │                     │
        │                       │                     │
        │   2. показывает форму │                     │
        │◄──────────────────────┤                     │
        │                       │                     │
   3. ввожу логин/пароль ──────►│                     │
        │                       │  проверяет          │
        │                       │                     │
        │   4. редирект назад   │                     │
        │◄──────────────────────┤                     │
        │   redirect_uri?code=ABC                     │
        │                       │                     │
        │   5. меняю code на токены                   │
        ├── POST /token ───────►│                     │
        │   code=ABC + ...      │                     │
        │                       │                     │
        │   6. access + refresh + id token            │
        │◄──────────────────────┤                     │
        │                       │                     │
   7. зову API с access token ─────────────────────► │
        │                       │   проверяет подпись │
        │◄──────────────────────────────────────────┤

Теперь словами, по шагам:

  1. Старт. Вы жмёте «Войти». Приложение перенаправляет браузер на адрес авторизации Keycloak. В URL — параметры: client_id (кто спрашивает), redirect_uri (куда вернуть), response_type=code (хочу код), scope=openid ... (что хочу узнать), state (защита от подмены запроса).
  2. Логин. Keycloak показывает свою страницу входа. Пароль вводится здесь, на стороне Keycloak — приложение его не видит.
  3. Проверка. Keycloak сверяет логин и пароль, при необходимости спрашивает второй фактор.
  4. Возврат с кодом. Keycloak перенаправляет браузер обратно на redirect_uri и добавляет в адрес code=ABC.... Это и есть authorization code — короткоживущий одноразовый код, сам по себе бесполезный.
  5. Обмен кода на токены. Приложение делает отдельный POST-запрос на token endpoint Keycloak и присылает туда этот код. Важно: это запрос напрямую к Keycloak, не через адресную строку браузера.
  6. Токены. В ответ Keycloak отдаёт три вещи: access token (пропуск к API), refresh token (чтобы обновить access, когда тот протухнет) и ID token (кто вы — для OpenID Connect).
  7. Работа с API. Приложение зовёт ваш API, кладя access token в заголовок Authorization: Bearer .... API проверяет подпись токена и пускает.

Почему именно так, в два этапа (сначала код, потом токены)? Потому что код летит через браузер (виден в адресной строке, в истории, в логах прокси), а токены — нет. Даже если кто-то подсмотрит код, без второго шага он бесполезен.

Зачем нужен PKCE

Тут вскрывается проблема. Шаг 4 — код прилетает в браузер. Что если на устройстве есть вредоносная программа или другое приложение, которое перехватит этот код в момент возврата? Тогда злоумышленник сам пойдёт на шаг 5 и выменяет код на токены. Особенно это касается публичных клиентов — SPA в браузере и мобильных приложений, у которых нет надёжного места, чтобы спрятать секретный пароль клиента (client_secret): код приложения открыт, секрет всё равно вытащат.

PKCE (Proof Key for Code Exchange, читается «пикси») закрывает эту дыру. Идея — связать шаг старта и шаг обмена одноразовым секретом, который знает только само приложение:

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

Смысл: перехватчик кода (шаг 4) не знает code_verifier — тот никогда не покидал приложение. А значит, украденный код обменять не сможет. Используйте именно метод S256, а не plain (где challenge равен verifier без хеширования) — plain защиты почти не даёт.

В Keycloak PKCE включается в настройках клиента: Advanced → Proof Key for Code Exchange Code Challenge Method → S256. Для публичных клиентов (SPA, мобильные) это обязательная настройка.

Почему implicit flow устарел

Раньше для SPA существовал упрощённый вариант — implicit flow. В нём Keycloak возвращал в браузер сразу access token прямо в адресной строке (response_type=token), без промежуточного кода и без второго запроса. Сделали так когда-то потому, что браузеры не умели нормально слать кросс-доменные запросы.

Проблема очевидна: токен летит через адрес страницы. Он попадает в историю браузера, в логи серверов и прокси, его видно в момент редиректа. И обновлять его аккуратно (через refresh token) тоже было нельзя.

Сегодня браузеры спокойно делают кросс-доменные запросы (CORS), поэтому костыль больше не нужен. Implicit flow считается устаревшим и небезопасным. В OAuth 2.1 — следующей редакции стандарта — implicit flow убирают совсем, а Authorization Code Flow с PKCE становится способом по умолчанию для всех клиентов, включая бэкенд.

Вывод простой: всегда используйте Authorization Code Flow с PKCE. Implicit flow в настройках клиента Keycloak (Implicit Flow Enabled) держите выключенным.

Где хранить токены

Допустим, токены получены. Куда их положить? От этого напрямую зависит безопасность, и здесь есть две разные стратегии.

Вариант 1: токены на бэкенде (рекомендуется для веба). Authorization Code Flow целиком проходит ваш серверный бэкенд. Полученные access и refresh токены он держит у себя, в серверной сессии. Браузеру отдаётся только сессионная cookie — с флагами HttpOnly, Secure, SameSite. JavaScript на странице до такой cookie не доберётся. Каждый запрос фронтенда идёт через бэкенд, тот подставляет нужный токен и зовёт API. Этот приём называют BFF (Backend for Frontend). Так токены вообще не лежат в браузере, и даже при XSS-атаке их не украсть.

Вариант 2: токены в браузере (только если без бэкенда никак). Чистый SPA без своего сервера вынужден хранить токены прямо в браузере — в памяти страницы или в localStorage. Это уязвимо: любой вредоносный скрипт, попавший на страницу через XSS, прочитает localStorage и заберёт токены. PKCE защищает только сам обмен кода, но не хранение токенов после. Поэтому хранение в localStorage — наименее желательный вариант; как минимум держите access token коротким, а refresh — в HttpOnly-cookie.

Простое правило: есть бэкенд — храните токены там (BFF), а в браузер отдавайте только защищённую cookie. Это сильно сужает поверхность для атаки.

Как это выглядит в Spring

Если приложение на Spring выступает как клиент, берётся стартер 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

Включить защиту — одна строка:

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
        .authorizeHttpRequests(a -> a.anyRequest().authenticated())
        .oauth2Login(Customizer.withDefaults())
        .build();
}

А ваш API (Resource Server) ничего про логин не знает — он только проверяет подпись присланного access token, забирая открытые ключи Keycloak по issuer-uri. Это уже отдельная тема — см. статью про проверку JWT.

Коротко

  • Authorization Code Flow — стандартный безопасный способ логина через Keycloak: пароль знает только Keycloak, приложение получает токены.
  • Поток в два этапа: сначала браузер приносит одноразовый code, потом приложение меняет code на токены отдельным запросом к token endpoint.
  • Код летит через браузер, токены — нет; даже перехваченный код бесполезен без второго шага.
  • PKCE связывает старт и обмен секретом code_verifier: приложение шлёт хеш (code_challenge + S256) на старте и сам 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 проверяет токен отдельно.

Что почитать дальше

  • JWT validation — Spring Security oauth2ResourceServer и JWK Set cache — как API проверяет подпись access token.
  • RBAC — маппинг ролей JWT и @PreAuthorize — где в токене Keycloak лежат роли и как их применять.