Прошлая статья закончилась на бэкенде: он проверяет токен и по ролям решает, пускать ли запрос. Но пользователь бэкенда не видит — он видит экран входа, и логинится он не обязательно паролем из вашего realm. Две вещи, которые превращают «поднятый Keycloak» в «Keycloak под ваш продукт»: как выглядит форма логина и через что на неё можно войти. Первое — кастомные темы, второе — федерация (identity brokering): вход через внешние провайдеры, от Яндекса и VK до корпоративных IdP.
Обе темы разобраны на живом стенде digital-cookbook: security/keycloak (Keycloak 26.7.0 + PostgreSQL): на нём собрана минимальная login-тема в стилистике khorost.tech и настроен брокер к внешнему OIDC-провайдеру, brokered-логин прогнан end-to-end. Все конфиги и вывод команд ниже — с этого прогона. А вот ЕСИА (Госуслуги) в публичном стенде воспроизвести нельзя, и об этом — честно, без имитации «живого» подключения.
В статье
- Две задачи: бренд и внешние учётки
- Тема логина: как устроена
- theme.properties и неаддитивное свойство styles
- Брендинг «Полдень»: CSS, логотип, шрифты
- Локализация: messages без правки шаблонов
- Подключение темы: volume + loginTheme
- FreeMarker против keycloakify: честное сравнение
- Identity brokering: Keycloak как брокер
- Стенд: brokered-логин на mock-OIDC
- Тонкость issuer: разные хосты для браузера и сервера
- Реальный прогон brokered-логина
- Яндекс и VK: реальные РФ-провайдеры
- ЕСИА: почему только концептуально
- Social login как простой контраст
- Мост к «своему» входу и РФ-углу
- Итог
- Источники
Две задачи: бренд и внешние учётки
«Экраны логина и внешние провайдеры» — это две разные задачи, которые решаются в Keycloak в разных местах, но обе нужны, чтобы IdP не торчал наружу как чужеродный сервис.
- Тема (кастомизация). Дефолтная форма Keycloak — тёмно-синяя, с логотипом Keycloak. Пользователь, которого перебросили с вашего сайта на такой экран, видит разрыв: только что был ваш бренд — и вдруг чужой сервис просит пароль. Кастомная тема убирает этот разрыв: форма логина выглядит как часть продукта.
- Федерация (identity brokering). Пользователь не хочет заводить ещё один пароль. Он хочет войти через учётку, которая у него уже есть — Яндекс, VK, корпоративный аккаунт. Keycloak умеет быть брокером: перенаправить на внешний IdP, принять результат, завести и связать локальную учётку. Ваш бэкенд по-прежнему получает стандартный токен Keycloak — откуда пользователь пришёл, ему знать не нужно.
Начнём с внешнего вида, потом перейдём к тому, через что можно войти.
Тема логина: как устроена
Тема Keycloak — это каталог с ресурсами, а не код. Структура фиксированная: themes/<name>/<type>/, где type — один из login, account, admin, email. Нас интересует login. Внутри — theme.properties (манифест темы), каталог resources/ (CSS, картинки, шрифты) и messages/ (переводы). Минимальная тема для брендинга — это theme.properties плюс один CSS-файл; ни одного шаблона копировать не обязательно.
Работает это через наследование. Тема объявляет parent, и всё, чего она сама не переопределяет — FreeMarker-шаблоны *.ftl, базовые сообщения, вёрстка PatternFly, — берётся от родителя. Наша тема на стенде наследует штатную keycloak и переопределяет ровно три вещи: добавляет свой CSS, кладёт SVG-логотип и меняет одно сообщение (заголовок формы). Каталог на стенде:
themes/
khorost/
login/
theme.properties # parent=keycloak + styles + import
resources/
css/khorost.css # брендинг «Полдень» поверх базовой темы
img/khorost-logo.svg # орбитальный логотип khorost.tech (inline SVG)
messages/
messages_en.properties # override loginAccountTitle (демо локализации)
messages_ru.properties # русская локаль темыКлючевая мысль: вы не форкаете тему Keycloak целиком, вы кладёте тонкий слой поверх штатной. Ни один .ftl-шаблон на стенде не скопирован — а значит, при обновлении мажора Keycloak (где шаблоны меняются) расходиться будет нечему. Это осознанный выбор в пользу минимализма: чем меньше вы переопределили, тем меньше сломается при апгрейде.
theme.properties и неаддитивное свойство styles
Весь манифест темы на стенде — три строки, и в них спрятана тонкость, на которой легко споткнуться.
parent=keycloak
import=common/keycloak
styles=css/login.css css/khorost.cssparent=keycloak— наследуем штатную login-тему со всеми.ftl-шаблонами и базовыми стилями.import=common/keycloak— подключаем общие vendor-ресурсы (шрифты pficon, служебные скрипты), как у родителя.styles=...— а вот здесь тонкость: свойствоstylesне аддитивное. Значение ребёнка не добавляется к родительскому, а полностью его заменяет. У темыkeycloakэтоstyles=css/login.css. Если написать простоstyles=css/khorost.css, базовыйlogin.cssперестанет подключаться, и форма поедет — пропадёт вся вёрстка PatternFly, поверх которой мы наводим бренд.
Поэтому в styles перечислены оба файла явно: сначала базовый css/login.css (он резолвится по цепочке наследования ресурсов из родителя), затем наш css/khorost.css — последним, чтобы его правила переопределяли базовые по каскаду CSS. Свойство stylesCommon (наборы PatternFly v4/v3) мы не трогаем — оно наследуется как есть.
Это тот класс поведения, который в документации есть, но замечаешь его только когда форма внезапно теряет стили. Правило простое: при переопределении styles всегда перечисляйте и родительские файлы, которые хотите сохранить.
Брендинг «Полдень»: CSS, логотип, шрифты
Сам брендинг — это обычный CSS поверх классов PatternFly, которыми размечена штатная форма. Задача темы khorost.tech — привести экран входа к стилистике сайта (советский sci-fi журнал «Полдень. XXI век»: кремово-бежевый фон, тонкие линии, приглушённая офсетная палитра, терракотовый акцент, геометрическая чистота без скруглений). Фрагмент, задающий фон страницы и карточку формы:
:root {
--kh-bg: #ece3d0; /* кремово-бежевый фон */
--kh-card: #f7f2e7; /* тёплая бумага карточки */
--kh-ink: #2b2b2b; /* чернильный текст */
--kh-accent: #b4552d; /* терракота — акцент */
/* Шрифты — ЧЕСТНО системный стек: брендовые woff2 сайта в репо стенда не тащим */
--kh-font-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
/* Фон: заменяем дефолтную картинку Keycloak на кремовый офсет с «чертёжной сеткой» */
.login-pf body {
background: var(--kh-bg);
background-image:
repeating-linear-gradient(0deg, rgba(43,43,43,.035) 0 1px, transparent 1px 40px),
repeating-linear-gradient(90deg, rgba(43,43,43,.035) 0 1px, transparent 1px 40px);
color: var(--kh-ink);
font-family: var(--kh-font-sans);
}
/* Карточка формы: тёплая бумага, терракотовая линия сверху, острые углы */
.card-pf {
background: var(--kh-card);
border: 1px solid #d3c7ae;
border-top: 3px solid var(--kh-accent);
border-radius: 0; /* геометрическая чистота */
max-width: 480px;
}
/* Кнопка входа: сплошная терракота, капитель, острые углы */
.pf-c-button.pf-m-primary {
background-color: var(--kh-accent);
border-radius: 0;
text-transform: uppercase;
letter-spacing: 2px;
}Логотип подключён не картинкой в шаблоне, а фоном в CSS — background-image: url(...khorost-logo.svg) на #kc-header-wrapper (шапку формы, которая у demo-realm пуста, потому что displayNameHtml не задан). Сам логотип — SVG-файл (resources/img/khorost-logo.svg: орбитальная марка «планета + кольцо + спутник» и вордмарк khorost.tech с подписью IDP · KEYCLOAK), подключённый как внешний CSS-ресурс — не инлайн в HTML и не растровый бинарник. Как вектор он правится текстом и не зависит от плотности пикселей.
Про шрифты — честно: тема держит стилистику «Полдень» на палитре, тонких линиях и геометрии, а гарнитура — системный стек (system-ui, …), а не брендовые Work Sans / Instrument Sans сайта: их бинарные .woff2 в репо стенда не тащим и @font-face не заводим.
Что реально видно на экране входа после подключения темы (проверено скриншотом на стенде): кремовый фон с еле заметной чертёжной сеткой, орбитальный логотип khorost.tech в шапке, карточка тёплой бумаги с терракотовой линией сверху и острыми углами, поля ввода с терракотовым фокусом, кнопка SIGN IN капителью с разрядкой, и ниже — блок «Or sign in with → Mock OIDC (external IdP)» (тот самый identity brokering, к которому перейдём во второй половине статьи). Визуально — ничего общего с дефолтной тёмно-синей темой Keycloak.
Важная граница: что можно и чего нельзя менять темой. Цвета, шрифты, логотип, тексты, расположение блоков CSS-ом — можно и без правки шаблонов. Добавить кастомное поле в форму регистрации, изменить логику экрана, встроить свой виджет — уже нельзя одним CSS: нужно переопределять .ftl-шаблон (и держать его копию, рискуя разойтись с апстримом), либо переходить на keycloakify (о нём — ниже). Для чистого брендинга CSS достаточно; для продуктового UX с логикой — нет.
Локализация: messages без правки шаблонов
Тексты формы логина — это не строки в шаблоне, а ключи сообщений, и тема может нести собственные переводы. Файлы — messages/messages_<locale>.properties; ключи наследуются от base/keycloak, тема переопределяет только нужные. На стенде мы поменяли ровно один ключ — заголовок формы:
# Русская локаль темы. Показывается, только если в realm включена
# интернационализация и добавлен язык ru (Realm settings -> Localization).
loginAccountTitle=Вход — khorost.tech
doLogIn=Войти
usernameOrEmail=Пользователь или e-mail
password=ПарольЗдесь два практических вывода. Первый: брендинг заголовка сделан через сообщение, а не через правку login.ftl — это чистый приём, тема остаётся тонким слоем без единого скопированного шаблона. Второй: русская локаль появляется на форме, только если в realm включена интернационализация (Realm settings → Localization → Internationalization) и добавлен язык ru; тогда на форме возникает переключатель локали, а строки берутся из messages_ru.properties темы поверх штатных. То есть кастомная тема несёт свои переводы, но показывает их только когда realm разрешил многоязычность.
Подключение темы: volume + loginTheme
Тема готова — осталось два шага: положить её туда, где Keycloak её найдёт, и указать realm использовать именно её.
Шаг 1 — примонтировать каталог темы в контейнер Keycloak (в проде — запечь в образ или положить на постоянный том):
keycloak:
volumes:
- ./realm:/opt/keycloak/data/import:ro
- ./themes:/opt/keycloak/themes:ro # <-- тема доступна KeycloakШаг 2 — назначить тему realm. На стенде проще всего прописать её прямо в импортируемом realm-JSON — поле верхнего уровня loginTheme:
{
"realm": "demo",
"loginTheme": "khorost",
...
}Альтернатива — через админку (Realm settings → Themes → Login theme = khorost), но тогда изменение нужно пере-экспортировать в realm-файл, иначе оно потеряется при пересоздании стенда. Прямая правка JSON надёжнее для воспроизводимого стенда.
Одна тонкость про кеширование тем, которая различает dev и prod:
- в
start-devтемы не кешируются — правки CSS, логотипа, сообщений видны после обычного F5, без пересборки образа и без рестарта контейнера. Это удобно для разработки темы; - в production-режиме
startтемы кешируются — там правки требуют рестарта или явного отключения кеша (--spi-theme-...-cache-themes=false, что для прода не нужно). Это осознанно: в проде тема статична, и кеш ускоряет отдачу формы.
Проверка на стенде: после docker compose up -d и импорта realm запрос kcadm get realms/demo --fields realm,loginTheme возвращает "loginTheme":"khorost", а форма логина отдаётся с подключёнными css/login.css + css/khorost.css и логотипом image/svg+xml — тема применилась.
FreeMarker против keycloakify: честное сравнение
То, что мы сделали выше, — штатный путь: тема на FreeMarker-шаблонах Keycloak плюс CSS. У него есть современная альтернатива — keycloakify, — и выбор между ними стоит сделать осознанно, а не по инерции.
FreeMarker + CSS (штатный путь). Тема — это каталог, theme.properties, CSS и (при необходимости) копии .ftl. Плюсы: нулевые зависимости и никакого шага сборки, всё «из коробки» Keycloak, минимальная тема — буквально пара файлов. Для брендинга (цвета, логотип, шрифт, тексты) этого достаточно, что мы и показали. Минусы: FreeMarker — старый шаблонизатор; сложную кастомную логику или интерактив на нём писать неудобно, а переопределение экрана требует держать копию .ftl — с риском разойтись с апстримом при обновлении мажора Keycloak, где шаблоны меняются.
keycloakify (React-темы). Вы пишете экраны логина на React/TypeScript, а тулинг собирает их в стандартную Keycloak-тему (тот же theme/<name>/login, но с готовым JS/CSS-бандлом и FTL-обёрткой). Плюсы: React/TS-стек, компонентная модель, hot-reload в dev, Storybook всех экранов, типобезопасность. Минусы: нужен Node-тулчейн и шаг сборки в CI, появляется зависимость от совместимости keycloakify с версией Keycloak, а бандл тяжелее «пары CSS-файлов».
Вывод без универсального ответа: для чистого брендинга (наш случай) FreeMarker + CSS проще и надёжнее — меньше движущихся частей, нечему ломаться при апгрейде. Для продуктового login-UX с логикой, кастомными полями и дизайн-системой на React keycloakify оправдан — он даёт нормальный фронтенд-стек вместо борьбы с FreeMarker. Граница ровно там же, где в разделе про брендинг: «покрасить» — CSS; «переписать поведение» — React-тулинг. На стенде keycloakify мы не собирали — только назвали как альтернативу.
Identity brokering: Keycloak как брокер
Переходим ко второй задаче — входу через внешние учётки. Механизм называется identity brokering, и роль Keycloak в нём — брокер: он стоит между вашим приложением и внешним провайдером.
Идея в одном абзаце. Пользователь на форме логина жмёт «Войти через Яндекс». Keycloak перенаправляет его на Яндекс, тот аутентифицирует пользователя и возвращает результат обратно в Keycloak. Keycloak при первом таком входе заводит у себя локальную учётку и связывает её с внешним аккаунтом (first-broker-login flow), заполняя атрибуты из claims внешнего IdP через mappers. При последующих входах он узнаёт этот внешний аккаунт и логинит уже в существующую локальную учётку. А вашему приложению Keycloak выдаёт свой стандартный токен — с iss вашего realm. Приложение не знает и не должно знать, что за пользователем стоит Яндекс: оно работает с одним IdP.
Ключевое следствие: федерация не меняет контракт бэкенда. Всё, что мы разбирали в третьей статье — локальная JWKS-валидация, проверка iss/aud/exp, извлечение realm_access.roles, — работает ровно так же. Внешний провайдер — это деталь того, как пользователь попал в realm, а не того, как бэкенд проверяет токен.
Поток целиком:
sequenceDiagram
participant U as Пользователь (браузер)
participant KC as Keycloak (брокер)
participant IdP as Внешний IdP (Яндекс/VK/mock)
U->>KC: 1. На форме логина «Войти через ...»
KC-->>U: 2. redirect на authorization endpoint IdP
U->>IdP: 3. Аутентификация на стороне IdP
IdP-->>U: 4. redirect с authorization code на /broker/alias/endpoint
U->>KC: 5. Передаёт code брокеру
KC->>IdP: 6. Обмен code на токен (серверно, token endpoint)
IdP-->>KC: 7. id_token + userinfo (claims)
Note over KC: 8. first-broker-login — завести и связать учётку, mappers кладут claims в атрибуты и роль
KC-->>U: 9. redirect на клиента с кодом Keycloak
U->>KC: 10. Обмен кода на access_token Keycloak (iss = ваш realm)
Два места, которые настраиваются, — это сам identity provider (endpoints внешнего IdP, client_id/secret, scopes) и набор mappers (какой claim внешнего IdP кладётся в какой атрибут/роль локального пользователя). Дальше — как это выглядит на стенде.
Стенд: brokered-логин на mock-OIDC
Демонстрировать brokering на реальном Яндексе в публичном стенде нельзя — нужен зарегистрированный client_secret, которого не место в репозитории. Поэтому в качестве «внешнего IdP» на стенде поднят лёгкий mock-OIDC-провайдер (ghcr.io/navikt/mock-oauth2-server:2.1.10, порт 8083): он отдаёт полный OIDC discovery и JWKS из коробки и настроен так, чтобы неинтерактивно выдавать фиксированный профиль внешнего пользователя. Это позволяет прогнать весь поток brokering запускаемо, без реальных секретов. Для Яндекса конфиг отличается от mock только адресами endpoints, scopes и mappers (generic OAuth v2); а вот VK ID устроен сложнее обычного generic-провайдера — о его дополнительном контракте честно ниже.
Identity provider mock в realm demo — обычный generic OIDC-провайдер (providerId: oidc). Конфиг (секрет — demo-only, в реальном провайдере он идёт из секрет-менеджера):
{
"alias": "mock",
"displayName": "Mock OIDC (external IdP)",
"providerId": "oidc",
"enabled": true,
"trustEmail": true,
"firstBrokerLoginFlowAlias": "first broker login",
"config": {
"clientId": "keycloak-broker",
"clientSecret": "broker-demo-secret",
"clientAuthMethod": "client_secret_post",
"syncMode": "IMPORT",
"defaultScope": "openid profile email",
"authorizationUrl": "http://localhost:8083/default/authorize",
"tokenUrl": "http://mock-oidc:8083/default/token",
"jwksUrl": "http://mock-oidc:8083/default/jwks",
"userInfoUrl": "http://mock-oidc:8083/default/userinfo",
"issuer": "http://mock-oidc:8083/default",
"validateSignature": "true",
"useJwksUrl": "true"
}
}И пять mappers — они заполняют локального brokered-пользователя из claims внешнего IdP. Тип «Attribute Importer» (oidc-user-attribute-idp-mapper) переносит claim в атрибут, отдельные мапперы — username из шаблона и hardcoded-роль:
[
{ "name": "username", "identityProviderMapper": "oidc-username-idp-mapper",
"config": { "template": "${CLAIM.preferred_username}" } },
{ "name": "email", "identityProviderMapper": "oidc-user-attribute-idp-mapper",
"config": { "claim": "email", "user.attribute": "email" } },
{ "name": "firstName", "identityProviderMapper": "oidc-user-attribute-idp-mapper",
"config": { "claim": "given_name", "user.attribute": "firstName" } },
{ "name": "lastName", "identityProviderMapper": "oidc-user-attribute-idp-mapper",
"config": { "claim": "family_name", "user.attribute": "lastName" } },
{ "name": "grant-user","identityProviderMapper": "oidc-hardcoded-role-idp-mapper",
"config": { "role": "user" } }
]То есть: preferred_username внешнего IdP становится username локальной учётки, email/given_name/family_name — соответствующими атрибутами, а grant-user жёстко назначает brokered-пользователю realm-роль user. После этого его токен Keycloak несёт realm_access.roles: [..., "user"] — и бэкенд из третьей статьи разграничивает доступ по этой роли, не подозревая о внешнем происхождении пользователя.
Небольшая техническая заметка про создание атрибутных mappers через kcadm: их нельзя завести флагом -s 'config.user.attribute=email' — из-за точки в имени ключа nested-парсер kcadm ломается. На стенде мапперы создавались JSON-телом (create ... -f - со stdin) — это надёжный обходной приём.
Тонкость issuer: разные хосты для браузера и сервера
Здесь всплывает нюанс, родственный тому, что был с KC_HOSTNAME в третьей статье, но с новым поворотом: в brokering участвуют два актора с разных сторон сети. Браузер идёт на authorization endpoint внешнего IdP с хоста, а Keycloak обменивает код на токен и ходит за JWKS/userinfo серверно, из docker-сети. Один и тот же mock-провайдер эти адреса видит по-разному.
Решается это не через discovery (который вывел бы один хост для всех endpoints), а явным заданием URL с правильным хостом для каждого:
| endpoint | URL в конфиге IdP | кто вызывает |
|---|---|---|
authorizationUrl |
http://localhost:8083/... |
браузер (с хоста) |
tokenUrl |
http://mock-oidc:8083/... |
Keycloak (docker-сеть) |
jwksUrl |
http://mock-oidc:8083/... |
Keycloak |
userInfoUrl |
http://mock-oidc:8083/... |
Keycloak |
issuer |
http://mock-oidc:8083/default |
= iss серверного токена |
Поскольку Keycloak получает токен серверно (token endpoint на mock-oidc:8083), iss в id_token равен http://mock-oidc:8083/default — и совпадает с issuer в конфиге провайдера, иначе Keycloak отклонил бы токен. В production этой раздвоенности нет: внешний провайдер (Яндекс, VK) доступен по одному публичному домену и с хоста браузера, и с сервера Keycloak, а KC_HOSTNAME совпадает с реальным доменом — специальных ухищрений не требуется. Раздвоение хостов — артефакт именно локального docker-стенда, и полезно понимать его природу, чтобы не переносить костыли в прод.
Реальный прогон brokered-логина
На стенде весь поток прогоняется неинтерактивным скриптом (broker-demo.sh): он инициирует authorization code + PKCE-логин через IdP mock, вручную проходит цепочку редиректов брокера и в конце обменивает код Keycloak на access-token. Реальный вывод (на свежем стенде после docker compose down -v && up, воспроизводимость проверена):
== PKCE + инициируем brokered-логин через IdP 'mock' ==
hop 1: 303 (keycloak:8080/realms/demo/protocol/openid-connect/auth)
hop 2: 303 (keycloak:8080/realms/demo/broker/mock/login)
hop 3: 302 (localhost:8083/default/authorize) # внешний IdP
hop 4: 302 (keycloak:8080/realms/demo/broker/mock/endpoint)
hop 5: 302 (keycloak:8080/realms/demo/login-actions/first-broker-login)
hop 6: 302 (keycloak:8080/realms/demo/broker/after-first-broker-login)
финальный redirect на клиента — authorization code Keycloak получен
== Обмен кода Keycloak на access_token (PKCE) ==
Keycloak access_token payload (фрагмент):
"iss":"http://keycloak:8080/realms/demo"
"sub":"cd095524-1804-4e99-aaa9-e872aa2e16b3"
"realm_access":{"roles":["offline_access", ... "user", ...]}
"preferred_username":"ext-yandex-demo"
"email":"ext.user@yandex.mock"Шесть hop’ов — это и есть поток с диаграммы вживую: инициатор auth (hop 1) → редирект на брокер broker/mock/login (hop 2) → внешний IdP localhost:8083/authorize (hop 3) → возврат кода в broker/mock/endpoint (hop 4) → first-broker-login (hop 5, завести/связать учётку) → after-first-broker-login (hop 6) → код Keycloak клиенту. First-broker-login прошёл без интерактивной страницы Review Profile, потому что mock отдал полный профиль (email + given_name + family_name) — Keycloak нечего было «доспрашивать».
Что осталось в realm после логина (проверено через kcadm):
GET users/{id}/federated-identity ->
[ { "identityProvider":"mock", "userId":"ext-user-01", "userName":"ext-yandex-demo" } ]
GET users/{id}/role-mappings/realm ->
[ { "name":"user" }, { "name":"default-roles-demo" } ]Итог прогона предметный: внешний sub=ext-user-01 связан с локальным пользователем ext-yandex-demo, атрибуты (email, имя) подтянуты из claims через мапперы, назначена realm-роль user (hardcoded-mapper), и Keycloak выдал свой access-token с iss=http://keycloak:8080/realms/demo. Это ровно то, ради чего нужен брокер: один внешний вход — один локальный пользователь realm — один стандартный токен для бэкенда.
Яндекс и VK: подключение реальных РФ-провайдеров
Реальные российские провайдеры подключаются тем же механизмом brokering, что и mock. Отличаются только адреса endpoints, scopes, мапперы под конкретные имена claims — и тип провайдера в Keycloak. Тип выбирается не по вкусу, а по тому, отдаёт ли провайдер id_token: если да — oidc (OpenID Connect v1.0), если это чистый OAuth2 с профилем через userinfo — oauth2 (OAuth v2, отдельный тип в списке Add provider). Взять oidc для провайдера без id_token — типовая ошибка: OIDC-провайдер ждёт id_token в token-ответе и на его отсутствии спотыкается. clientSecret не в репозитории: на стенде — плейсхолдер/env, в проде — из секрет-менеджера. И один обязательный шаг на стороне провайдера: в кабинете приложения прописать redirectUri вида https://<keycloak-домен>/realms/<realm>/broker/<alias>/endpoint.
Яндекс (Yandex ID / Yandex OAuth). Приложение регистрируется на oauth.yandex.ru, включается доступ к email и логину. Discovery у Яндекса неполный — endpoints задают явно; id_token Яндекс не выдаёт, профиль отдаётся через userinfo (login.yandex.ru/info). Это чистый OAuth2 с userinfo, а не OIDC, поэтому в Keycloak его добавляют типом OAuth v2 (providerId: "oauth2"), а не «OpenID Connect v1.0» (Add provider → OAuth v2):
{
"alias": "yandex",
"displayName": "Яндекс",
"providerId": "oauth2",
"config": {
"clientId": "${YANDEX_CLIENT_ID}",
"clientSecret": "${YANDEX_CLIENT_SECRET}",
"authorizationUrl": "https://oauth.yandex.ru/authorize",
"tokenUrl": "https://oauth.yandex.ru/token",
"userInfoUrl": "https://login.yandex.ru/info?format=json",
"defaultScope": "login:email login:info"
}
}Мапперы (тип Attribute Importer, как на стенде): default_email → email, login/id → username, first_name/last_name → firstName/lastName. Профиль извлекается из userinfo, не из id_token.
VK (VK ID). С VK оговорка честнее и сложнее, чем «поменяй endpoints». Актуальный VK ID — это не обычный OIDC/OAuth2-провайдер: он требует PKCE, возвращает в callback дополнительный параметр device_id и требует передавать его при обмене authorization code на токены и при refresh. Актуальные endpoints живут на домене id.vk.ru (не только id.vk.com из старых примеров): /authorize, /oauth2/auth, /oauth2/user_info. Этот дополнительный контракт — device_id, который нужно протащить из ответа авторизации в token-запрос, — выходит за рамки того, что стоковый generic OAuth v2 / OIDC identity provider Keycloak умеет из коробки: пробросить device_id между callback и обменом кода стоковому провайдеру негде.
Поэтому честно: интеграцию VK ID со стоковым провайдером Keycloak я в стенде не проверял — и утверждать «работает как обычный generic OIDC» не стану. Реалистично один из двух путей: (1) кастомный identity-provider SPI / адаптер под VK ID (обрабатывающий PKCE + device_id), либо (2) прокси-слой, приводящий VK ID к каноничному OIDC-контракту. Плюс отдельная тонкость маппинга: email VK ID возвращает в ответе /oauth2/user_info при запрошенном scope. Практический вывод: VK ID — не «две строки конфига», а самостоятельная интеграционная задача; пиновать под текущую версию VK ID и проверять на живом контуре.
Общая рекомендация для РФ-аудитории: российские провайдеры (Яндекс, VK) — как основной вариант входа, зарубежные (Google, GitHub) — альтернативой. И trustEmail включать осознанно: доверять email внешнего IdP стоит только если провайдер отдаёт его как verified.
ЕСИА: почему только концептуально
Логичный следующий вопрос для РФ-продукта — «а Госуслуги (ЕСИА) так же подключаются?». Честный ответ: архитектурно — да, тем же brokering, но в публичном учебном стенде это не воспроизводимо, и имитировать «живое» подключение здесь я не буду. Причины конкретны, а не «сложно вообще».
- Аккредитация Минцифры. ЕСИА — государственный IdP. Доступ к нему как информационной системы требует регистрации ИС/РИС и согласования; нельзя просто «зарегистрировать OAuth-приложение», как у Яндекса или VK. Это административный барьер, а не технический.
- ГОСТ-TLS и подпись. Взаимодействие с ЕСИА идёт по каналам с ГОСТ-шифрованием (сертификаты аккредитованных УЦ, СКЗИ вроде КриптоПро) и с подписанием запросов по ГОСТ. Стандартный Keycloak с RS256 и обычным TLS «из коробки» это не закрывает — нужен ГОСТ-терминирующий прокси (например, stunnel или nginx с ГОСТ-движком) перед Keycloak плюс кастомная работа с подписью запросов.
- Специфика профиля. ЕСИА исторически ближе к собственному профилю OAuth2 (и SAML-схемам), набор scope и claims регламентирован (СНИЛС, подтверждённая учётная запись, уровни УЗ — упрощённая / стандартная / подтверждённая), плюс требования к журналированию и защите ПДн.
Как это ложится на Keycloak: ЕСИА подключается через generic OIDC/OAuth2-провайдер (или собственный SPI) плюс ГОСТ-TLS-прослойку, а атрибуты (СНИЛС, уровень УЗ) переносятся теми же мапперами, что и у Яндекса. Но развернуть это в открытом стенде без аккредитации и СКЗИ невозможно — поэтому демонстрацию brokering мы даём на mock-OIDC и конфиге Яндекс/VK, а ЕСИА оставляем как архитектурно возможный, но регуляторно и криптографически особый случай. Фабриковать «прогон ЕСИА» было бы нечестно.
Важная оговорка про актуальность: изложенное выше — это общая архитектурная форма, а не пошаговый регламент. Конкретика ЕСИА (версия протокольного профиля, точный набор scope и claims, уровни учётной записи, требования к сертификатам и журналированию, endpoints тестового и продуктивного контуров) задаётся официальными методическими рекомендациями и регламентом подключения ИС к ЕСИА, которые версионируются и периодически меняются. Перед реальной интеграцией сверяйтесь с текущей редакцией этих документов на официальных ресурсах (Минцифры / технический портал ЕСИА) и фиксируйте дату актуальности — детали здесь намеренно даны на уровне подхода, а не как замена действующему регламенту.
Social login как простой контраст
На фоне ЕСИА видно, насколько проще устроены соц-провайдеры. Google и GitHub в Keycloak — это встроенные типы identity provider (не generic OIDC, а именно Google/GitHub в списке): достаточно вставить client_id/client_secret из консоли провайдера и прописать redirect URI. Endpoints, scopes и базовые мапперы Keycloak уже знает — заводить их руками, как для generic-провайдера, не нужно.
Это удобный ориентир сложности подключения: встроенные соц-провайдеры (Google/GitHub) — два поля; generic-провайдер (Яндекс, OAuth v2) — endpoints + scopes + мапперы руками; VK ID — ступенью выше (PKCE + device_id, стоковый провайдер может не подойти — вероятен custom SPI/адаптер); ЕСИА — плюс аккредитация и ГОСТ-TLS. Один и тот же механизм brokering, но цена подключения растёт по мере того, как провайдер дальше от «стандартного OIDC из коробки».
Мост к «своему» входу и РФ-углу
Стоит соотнести эту статью с двумя точками серии.
С «сделай сам»: в цикле auth есть отдельная статья про вход и регистрацию, где OAuth-вход, привязку внешних аккаунтов и заведение локальной учётки нужно писать руками — обрабатывать редиректы, обменивать код, доставать профиль, связывать с существующим пользователем, думать про верификацию email. Здесь всё это — конфиг identity provider плюс мапперы. Ровно тот обмен, о котором первая статья серии: вы меняете разработку auth-пласта на эксплуатацию готового IdP. Федерация — один из самых наглядных примеров этой экономии: каждый внешний провайдер «сделай сам» — отдельная интеграция, в Keycloak — запись в realm.
С РФ-углом первой статьи: self-hosted Keycloak on-prem закрывает вход через Яндекс стандартным механизмом (VK ID из-за PKCE + device_id требует адаптера — см. выше), а ЕСИА показывает границу — где заканчивается «настроил провайдера» и начинается «нужна аккредитация и СКЗИ». Это не минус Keycloak, а свойство государственного IdP; понимать эту границу полезно ещё на этапе выбора.
Механику самих OAuth2/OIDC-флоу, которые здесь прячутся за словами «authorization code» и «обмен кода на токен», подробно разбирает статья про OAuth2 и OIDC — Keycloak в brokering выступает и клиентом внешнего IdP, и сервером для вашего приложения одновременно.
Итог
- Тема логина — тонкий слой поверх штатной. Каталог
themes/<name>/login,theme.propertiesсparent=keycloak, свой CSS и SVG-лого; ни одного скопированного.ftl— нечему расходиться при апгрейде. Тонкость: свойствоstylesне аддитивное, значение ребёнка заменяет родительское — перечисляйте и базовыйlogin.css, и свой файл, свой последним. - Брендинг — это CSS, не код. Цвета, шрифты, логотип (фоном на шапке), тексты — без правки шаблонов. Кастомные поля и логику одним CSS не сделать: там нужен
.ftlили keycloakify. - Локализация — через messages. Тема несёт свои переводы (
messages_<locale>.properties); заголовок формы менять сообщением, а не шаблоном. Русская локаль видна, только если в realm включена интернационализация. - FreeMarker против keycloakify. Для чистого брендинга — FreeMarker + CSS проще и надёжнее; для продуктового login-UX с логикой на React — keycloakify оправдан ценой Node-тулчейна и шага сборки.
- Identity brokering — Keycloak как брокер. Внешний вход → first-broker-login заводит и связывает локальную учётку → мапперы кладут claims в атрибуты и роли → приложению уходит стандартный токен с
issвашего realm. Контракт бэкенда из третьей статьи не меняется. - На стенде — запускаемо на mock-OIDC. Реальный прогон: шесть hop’ов брокера, brokered-пользователь
ext-yandex-demoсвязан с внешнимext-user-01, рольuserназначена, токен Keycloak выдан. Тонкость локального стенда — разные хосты endpoints для браузера и сервера; в проде её нет. - Яндекс — тип OAuth v2 (id_token не выдаёт, профиль через userinfo), endpoints + scopes + мапперы руками, секреты не в репо. VK ID — сложнее generic: PKCE +
device_id, стоковый провайдер Keycloak может не подойти (вероятен custom SPI/адаптер) — в стенде не проверялось. ЕСИА — только концептуально: аккредитация Минцифры и ГОСТ-TLS вне учебного стенда. Google/GitHub — встроенные провайдеры, два поля.
Осталась одна тема серии — самая «взрослая»: как эксплуатировать Keycloak в production. HA и кластеризация, внешняя БД как единственный источник состояния, бэкапы и восстановление, болезненные обновления мажоров. Об этом — пятая, заключительная статья серииготовится, с 16 сентября.
Источники
- Keycloak — Server Developer Guide (темы,
theme.properties, наследование): https://www.keycloak.org/docs/latest/server_development/#_themes - Keycloak — Server Administration Guide (identity brokering, mappers, first-broker-login): https://www.keycloak.org/docs/latest/server_admin/#_identity_broker
- keycloakify — сборка Keycloak-тем на React: https://keycloakify.dev
- Yandex ID — OAuth и получение данных пользователя: https://yandex.ru/dev/id/doc/
- VK ID — документация для разработчиков: https://id.vk.com/about/business/go/docs
- ЕСИА — методические рекомендации по использованию (профиль, scopes, требования): https://digital.gov.ru/ (раздел ЕСИА)
- OpenID Connect Core 1.0: https://openid.net/specs/openid-connect-core-1_0.html
Комментарии