Client scope в Keycloak: что внутри, зачем нужен и как это работает поверх Active Directory
После прошлого поста про IdP у многих остались вопросы почему же один сервис одного юзера пускает, а другой не пускает и выдает ошибку. Так что ловите обещанное продолжение. Расскажу про одну из самых важных штук на примере Keycloak, так как сам работаю с ним. Начинаем начинать)
Scope чаще всего воспринимают как «галочку в настройках клиента». На деле это главный инструмент управления тем, что вообще попадает в токен. А когда под Keycloak стоит AD — почти все инциденты рождаются на стыке двух миров: федерации и scope. Разберём по порядку.
Что лежит внутри scope
Client scope — контейнер, который цепляется к клиенту и содержит:
- Protocol mappers — то, что реально формирует claims: audience, группы, роли, атрибуты пользователя
- Role scope mappings — какие роли (realm/client) попадут в токен при этом scope
- Тип: Default (применяется всегда) или Optional (только если клиент запросил через scope=)
Плюс встроенные: profile, email, roles, web-origins, address, phone, offline_access, acr, microprofile-jwt.
Важно: scope живёт на уровне realm и переиспользуется между клиентами. Это общий контракт, а не приватная настройка одного клиента.
Зачем он нужен
- Ограничить привелегии. Бухгалтерии нужен email, а не членство в 40 группах и не LDAP-пути.
- Переиспользование. Один scope «api-audience» на двадцать клиентов вместо двадцати одинаковых мапперов.
- Размер токена. Тяжёлые claims выносим в Optional — платим только там, где нужно.
- Явный контракт. Default = «клиент получает всегда», Optional = «можно запросить».
Главное, что нужно понять про AD
Scope НЕ знает про Active Directory. Он работает уже с загруженной моделью пользователя в Keycloak.
Данные проходят четыре звена:
- AD → атрибут (sAMAccountName, mail, department, memberOf)
- User Federation → LDAP mapper → попадает в модель пользователя Keycloak
- Protocol mapper ВНУТРИ client scope → кладёт значение в токен как claim
- Resource server читает claim
Отсюда правило, которое снимает половину вопросов: если атрибута нет в модели пользователя, можно поставить хоть десять protocol mappers — claim будет пустой. И наоборот: если атрибут в модели есть, но protocol mapper не привязан к нужному scope — в токене его не будет.
LDAP mapper — это «втянуть из AD в Keycloak». Protocol mapper в scope — это «отдать наружу в токене». Постоянно путают.
LDAP mappers, которые обычно настраивают
- Username / RDN mapper — username = sAMAccountName или userPrincipalName (выбор фиксируется при создании провайдера, потом не меняется)
- First name / Last name / Full name
- user-attribute-ldap-mapper — любой атрибут: department, title, employeeID, manager
- group-ldap-mapper — AD-группы → группы Keycloak
- role-ldap-mapper — AD-группы → роли Keycloak
- msad-user-account-control-mapper — учитывает userAccountControl (отключённые/заблокированные аккаунты)
- hardcoded-attribute-ldap-mapper — константа, если нужно
Имя атрибута в LDAP mapper и User Attribute в protocol mapper должны совпадать посимвольно. Разница в регистре — самая обидная причина пустого claim.
Edit Mode — рычаг против устаревших данных
- READ_ONLY — атрибут импортируется и живёт в БД Keycloak. Изменилось в AD → в Keycloak старое, пока не прошла синхронизация
- WRITABLE — изменения в Keycloak уезжают в AD (пароль — только если разрешает политика)
- UNSYNCED — атрибут не хранится в БД, читается из AD при каждом обращении. Всегда свежий, но дороже по latency
Именно здесь чаще всего теряется свежесть: настроили department как READ_ONLY, поменяли отдел в AD, а в токене он прежний — и никто не понимает почему.
Синхронизация и её подводные камни
- Import Users On/OFF — импортировать всех сразу или только при первом входе
- Periodic Full Sync / Periodic Changed Users Sync — расписание в Sync Settings
- «Sync changed users» ориентируется на uSNChanged в AD и заметно быстрее полной, но изменения членства в группах могут не подхватиться так, как ожидаешь
- После правок групп в AD: Provider → Synchronize all users (или Sync changed users), а пользователю — перелогиниться
И самое коварное: claims живут в токене. Изменил группу — старый access token продолжает действовать до exp или refresh. Это не баг Keycloak, это природа JWT, и закладывать это надо в дизайн.
Проблемы и как с ними бороться
1. Токен раздувается, 431 / 400
group-ldap-mapper с полными DN, сотни групп, плюс роли и атрибуты → токен 8–12 КБ → браузер упирается в лимит заголовков.
→ Группы и тяжёлые атрибуты — в Optional scope, отдавать точечно. Совсем тяжёлое — не в токен, а через UserInfo / Admin API.
→ На прокси проверить large_client_header_buffers, помнить про лимит cookie 4 КБ (актуально для больших ID-токенов).
2. Claim пустой, хотя в AD значение есть
Разбирать строго по цепочке, сверху вниз:
→ Пользователь → Attributes: есть ли атрибут в модели Keycloak? Нет — проблема в LDAP mapper, Edit Mode, правах сервисной учётки или синхронизации
→ Есть — Client scopes → Evaluate: привязан ли protocol mapper к scope этого клиента?
→ Привязан — декодировать реальный токен и посмотреть claim глазами, а не через настройки
3. В Authority/Audience нет нужной цели
Выключили Full Scope Allowed — и вместе с лишними ролями уехал нужный aud.
→ Явный audience mapper в scope клиента.
4. Роли «пропали» после правок
Роль есть в маппинге, но не в scope mapping нужного client scope; client role виден только при Full Scope Allowed или явном маппинге; composite-роли раскрываются не так, как ожидалось.
→ Роли сервиса — только через выделенный client scope с явным role scope mapping. Full Scope Allowed = true — почти всегда антипаттерн.
5. Вложенные группы AD не видны
Классика: memberOf отдаёт только прямые группы. Пользователь в Group A, Group A внутри Group B — в токене будет только A.
→ В фильтре группы использовать LDAP_MATCHING_RULE_IN_CHAIN (1.2.840.113556.1.4.1941), либо раскладывать иерархию AD в группы Keycloak, либо не строить авторизацию на глубокой вложенности вовсе.
6. Дубли пользователей после пересоздания провайдера
→ UUID хранится в objectGUID. Пересоздали провайдер или сменили UUID-атрибут — получили второго «того же» пользователя. Не плодить провайдеры без нужды.
7. Size Limit Exceeded при синхронизации
AD по умолчанию отдаёт не больше 1000 записей за раз.
→ Включить LDAPv3 paging в настройках провайдера. Для больших лесов это обязательный пункт, а не опция.
8. Связка падает целиком
Сервисная учётка истекла, LDAPS отвалился, сеть до контроллера — и логина нет ни у кого.
→ LDAPS/StartTLS, отдельная read-only учётка (не доменный админ), пароль через vault, connection pooling и таймауты, мониторинг на факт коннекта.
9. Несколько доменов и дубли username
Два провайдера, одинаковый sAMAccountName в разных доменах → неоднозначность.
→ Различать username (UPN/suffix), либо Global Catalog (3286/3268), либо отдельные провайдеры с явными правилами.
10. Путаница двух «scope»
В Keycloak есть второе значение слова — resource scope в Authorization Services (resource:read). Это не OIDC scope и живёт в другом месте. Половина вопросов «мой scope не работает» — ровно про это.
Чек-лист
- Default — только то, без чего клиент не работает; Optional — всё тяжёлое
- Full Scope Allowed — выключен, роли выдаём явно
- Audience — всегда явным маппером
- LDAP mapper не заменяет protocol mapper, и наоборот — проверяем оба звена
- Edit Mode осознанно: READ_ONLY при stale-данных делает токен вручную устаревшим
- Paging включён, LDAPS включён, учётка read-only
- Любая правка общего scope — через Evaluate на всех клиентах, которые его используют
- Токен в пределах 1–2 КБ, остальное — по запросу
- Помните: JWT живёт до exp, изменения в AD доходят не мгновенно
Scope — не бюрократия, а способ не отдавать сервису лишнего. А когда снизу AD, он ещё и единственное место, где решается, какие доменные атрибуты вообще увидят ваши сервисы.