Александр Есин / блог

Client scope в Keycloak: что внутри, зачем нужен и как это работает поверх Active Directory

24 октября 2026

После прошлого поста про 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.

Данные проходят четыре звена:

  1. AD → атрибут (sAMAccountName, mail, department, memberOf)
  2. User Federation → LDAP mapper → попадает в модель пользователя Keycloak
  3. Protocol mapper ВНУТРИ client scope → кладёт значение в токен как claim
  4. 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
  • Email
  • 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, он ещё и единственное место, где решается, какие доменные атрибуты вообще увидят ваши сервисы.