Вы открываете приложение, оно отправляет вас на отдельную страницу логина 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 ─────────────────────► │
│ │ проверяет подпись │
│◄──────────────────────────────────────────┤
Теперь словами, по шагам:
- Старт. Вы жмёте «Войти». Приложение перенаправляет браузер на адрес авторизации Keycloak. В URL — параметры:
client_id(кто спрашивает),redirect_uri(куда вернуть),response_type=code(хочу код),scope=openid ...(что хочу узнать),state(защита от подмены запроса). - Логин. Keycloak показывает свою страницу входа. Пароль вводится здесь, на стороне Keycloak — приложение его не видит.
- Проверка. Keycloak сверяет логин и пароль, при необходимости спрашивает второй фактор.
- Возврат с кодом. Keycloak перенаправляет браузер обратно на
redirect_uriи добавляет в адресcode=ABC.... Это и есть authorization code — короткоживущий одноразовый код, сам по себе бесполезный. - Обмен кода на токены. Приложение делает отдельный
POST-запрос на token endpoint Keycloak и присылает туда этот код. Важно: это запрос напрямую к Keycloak, не через адресную строку браузера. - Токены. В ответ Keycloak отдаёт три вещи: access token (пропуск к API), refresh token (чтобы обновить access, когда тот протухнет) и ID token (кто вы — для OpenID Connect).
- Работа с API. Приложение зовёт ваш API, кладя access token в заголовок
Authorization: Bearer .... API проверяет подпись токена и пускает.
Почему именно так, в два этапа (сначала код, потом токены)? Потому что код летит через браузер (виден в адресной строке, в истории, в логах прокси), а токены — нет. Даже если кто-то подсмотрит код, без второго шага он бесполезен.
Зачем нужен PKCE
Тут вскрывается проблема. Шаг 4 — код прилетает в браузер. Что если на устройстве есть вредоносная программа или другое приложение, которое перехватит этот код в момент возврата? Тогда злоумышленник сам пойдёт на шаг 5 и выменяет код на токены. Особенно это касается публичных клиентов — SPA в браузере и мобильных приложений, у которых нет надёжного места, чтобы спрятать секретный пароль клиента (client_secret): код приложения открыт, секрет всё равно вытащат.
PKCE (Proof Key for Code Exchange, читается «пикси») закрывает эту дыру. Идея — связать шаг старта и шаг обмена одноразовым секретом, который знает только само приложение:
- Перед стартом приложение генерирует случайную строку —
code_verifier. Это секрет, он никуда не уходит, остаётся внутри приложения. - Считает от неё хеш:
code_challenge = BASE64URL(SHA256(code_verifier)). Метод хеширования называется S256. - На шаге 1 (редирект на логин) приложение шлёт в Keycloak
code_challengeиcode_challenge_method=S256. Keycloak запоминает challenge рядом с выданным кодом. - На шаге 5 (обмен кода) приложение присылает уже сам
code_verifier. - 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/SameSitecookie.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 лежат роли и как их применять.