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

БЕЗОПАСНОСТЬ И АВТОРИЗАЦИЯ Модель Keycloak

Обязательно

Realm, client, роли и пользователи

Keycloak · OIDC

Как realm изолирует пространство, client описывает приложение, а роли через мапперы попадают в claims токена.

1. Realm изолирует пользователей и ключи

1 / 5

Пользователи, клиенты, роли, ключи и политики realm не смешиваются с другим пространством.

  • Production и test разделены.
  • У realm свой issuer.
  • Admin каждого realm управляет своей областью.
Realm produsers · clients · keysRealm testusers · clients · keysРазные issuer и политика

2. Client описывает одно приложение

2 / 5

Client ID, redirect URI, тип доступа и scopes определяют, как приложение проходит OAuth flow.

  • Public client использует PKCE.
  • Confidential client хранит secret на сервере.
  • Redirect URI совпадает точно.
Client: backend-webpublic + PKCE S256redirect: https://app/callbackscope: openid profileNo wildcard redirect

3. Realm- и client-роли имеют разную область

3 / 5

Realm-роль общая для пространства, client-роль относится к конкретному приложению.

  • realm_access.roles содержит realm-роли.
  • resource_access[client].roles содержит client-роли.
  • Приложение выбирает ожидаемый claim.
Realm rolesadmin · supportClient rolesorders.readerОдин пользователь - разные области прав

4. Группы назначают роли пакетно

4 / 5

Пользователь входит в группу и наследует её роли. Мапперы добавляют нужные claims в токен.

  • Группа support выдаёт support.
  • Client scope ограничивает содержимое.
  • Удаление группы меняет будущие токены.
User groupRole mapperClient scopeJWT roles

5. Приложение переводит claims в свои права

5 / 5

Проверенный JWT даёт Principal с userID и ролями. Keycloak не знает владельца заказа и доменные переходы.

  • Middleware проверяет роль.
  • ABAC проверяет ресурс.
  • Неизвестная роль не расширяет доступ.
JWT claimsPrincipalDomain policyKeycloak выдаёт факты, приложение решает доступ

Вы поставили Keycloak, открыли админку — и сразу налетела куча незнакомых слов: realm, client, client scope, mapper, группы, роли. Непонятно, что главнее, что во что вложено и с чего начинать. А пока модель в голове не уложилась, легко настроить так, что вход то работает, то нет, роли «не доезжают» до приложения, а почему — неясно. Давайте разберём модель Keycloak по кусочкам, не торопясь, с аналогиями из жизни, и в конце соберём минимальный рабочий набор для типичной связки «серверный бэкенд + браузерный фронтенд».

Главная развилка этой модели видна сразу: право, назначенное в админке, работает только тогда, когда оно попало внутрь токена.

роль назначена в админке — приложение читает её только из токена админка Keycloak access token realm marketplace пользователь ivan realm-роль: moderator client-роль backoffice: card:approve маппер realm-ролей маппер client-ролей realm_access.roles: ["moderator"] resource_access:backoffice.roles:["card:approve"] resource_access: {}client-ролей нет маппер включён — обе роли в токенебэкенд пускает к модерации по card:approve маппер client-ролей выключенв админке роль на месте, в токене её нет — 403 приложение не спрашивает Keycloak про роли на каждый запросчинить надо маппер или client scope, а не назначение роли

Назначение в админке и содержимое токена — не одно и то же: между ними стоит протокол-маппер. Выключен маппер client-ролей — resource_access приедет пустым, и card:approve для приложения просто не существует.

Зачем вообще отдельный сервер для входа

Вводная статья уже разобрала, зачем выносить вход из приложений в Keycloak. Здесь важно другое: Keycloak выдаёт приложению подписанный токен, и оно читает права из него, а не ходит за ними в Keycloak на каждый запрос.

Пользователь работает с приложением, приложение отправляет его на вход в Keycloak, получает токены и ходит с ними в ваш бэкенд заголовком Authorization: Bearer access_token

Теперь разберём, из чего Keycloak состоит внутри.

Realm — изолированное пространство

Самое верхнее понятие в Keycloak — это realm. С него начинается всё остальное, поэтому с него и начнём.

Представьте многоквартирный дом. Один установленный Keycloak — это весь дом. А realm — это отдельная квартира со своим замком. Жильцы одной квартиры физически не могут попасть в другую: у каждой квартиры свои ключи, свои жильцы, свои правила. Квартиры стоят в одном доме, но друг про друга ничего не знают.

Realm — это полностью изолированное пространство. Внутри него живут: свой набор пользователей, свои приложения (client'ы), свои роли, группы и настройки входа (например, через какие способы можно логиниться). Главное свойство — изоляция: пользователь, заведённый в одном realm, в другом realm просто не существует. Роль из одного realm в другом не действует. Это не «разные папки в одной системе», а действительно отдельные миры.

Зачем такая жёсткая изоляция? Затем, что на одном сервере Keycloak часто нужно держать совершенно разные аудитории, которые не должны пересекаться. Типичные примеры:

  • отдельно realm для сотрудников компании и отдельно realm для внешних клиентов — у них разные пользователи и разные правила;
  • отдельный realm под каждый независимый проект, чтобы они не мешали друг другу.

На схеме видно, что один Keycloak держит несколько realm, и они не связаны: пользователи и роли realm A никак не видны в realm B.

Один сервер Keycloak держит три независимых realm - master, myapp и partners, причём пользователи и роли realm myapp не видны в realm partners

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

Ещё одна вещь, которую полезно запомнить сразу: все адреса Keycloak привязаны к realm. Базовый адрес, по которому приложение находит ключи и настройки конкретного realm, выглядит так:

https://auth.example.com/realms/myapp

Этот адрес называют issuer (издатель токенов) — именно он стоит в каждом выданном токене как «кто меня выпустил». Запомните его: он понадобится при настройке приложения, и он у каждого realm свой.

Client — это приложение, которое обращается к Keycloak

Внутри realm живут client'ы. Это второе по важности понятие, и оно часто путает, потому что слово «client» здесь означает не пользователя, а приложение.

Откуда вообще берётся такое понятие? Keycloak должен как-то различать, какое именно приложение к нему пришло. Браузерный фронтенд, мобильное приложение и фоновый серверный сервис ведут себя по-разному, и доверять им можно по-разному. Чтобы их различать, для каждого приложения в realm заводят запись.

Client — это и есть запись о приложении внутри realm. У каждого приложения, которое пользуется этим Keycloak, есть свой client со своим идентификатором (client id) — например, myapp-frontend или myapp-backend. Когда приложение начинает процесс входа, оно представляется этим идентификатором: «здравствуй, Keycloak, я client myapp-frontend».

Ключевая характеристика, по которой client'ы делятся на два типа, — может ли приложение надёжно хранить секрет (то есть пароль самого приложения). От этого зависит, как именно приложение доказывает Keycloak, что оно — это оно.

public код в браузере секрет прочтут секрета нет спасает PKCE confidential код на сервере секрет скрыт секрет есть доказывает себя

Два ряда рядом: смотрите на первую клетку каждого, где лежит код приложения, из этого следует всё остальное.

Public client

Public («публичный») — это приложение, которое в принципе не может надёжно спрятать секрет. Почему не может? Потому что весь его код доступен пользователю:

  • код одностраничного сайта (React, Angular, Vue) целиком загружается в браузер — открой инструменты разработчика и читай;
  • мобильное приложение можно скачать и распаковать.

Любой «секретный пароль», зашитый в такой код, на самом деле виден всем желающим, а значит, он бесполезен как секрет. Поэтому у public client секрета нет вообще — Keycloak его и не требует.

Так настраивают: одностраничные сайты (React, Angular, Vue) и мобильные приложения.

Возникает резонный вопрос: если у приложения нет секрета, как тогда защитить вход от подмены? Для этого у public client обязательно используют PKCE. Идея простая: в самом начале входа приложение генерирует одноразовую секретную строку, держит её у себя и в конце предъявляет, доказывая, что именно оно — то самое приложение, которое начинало вход. Это закрывает дыру, через которую злоумышленник мог бы перехватить код авторизации и обменять его на токены вместо вас. Важная деталь: сам сервер по умолчанию не требует PKCE — он лишь поддерживает его, если приложение решит использовать. Так в Keycloak было раньше, так осталось и в 26-й версии. Библиотеки на фронтенде обычно используют, но полагаться на это не стоит. В настройках клиента в Keycloak есть поле для метода проверки (S256) — заполните его, и тогда вход без PKCE сервер просто отклонит.

Confidential client

Confidential («конфиденциальный») — это приложение, которое работает на сервере и может надёжно хранить секрет, потому что его код пользователю не виден: он крутится на вашем бэкенде, а не в браузере.

У такого client есть client secret — по сути, пароль самого приложения. Им бэкенд доказывает Keycloak: «я действительно client myapp-backend, вот мой секрет». Так настраивают серверные приложения: бэкенд на Spring Boot, сервис на Node.js и т. п.

Разница между двумя типами укладывается в одну табличку. На ней — кто чем доказывает свою подлинность и для каких приложений подходит.

Тип clientХранит секретЧем защищён обмен кода на токеныДля каких приложений
publicнетприложение себя не удостоверяет вообще; PKCE лишь связывает старт входа с обменомфронтенд (SPA), мобильные
confidentialдаclient secret — им приложение доказывает, что оно это оно (PKCE сверху не лишний)серверный бэкенд

Простое правило, которое стоит запомнить: код виден пользователю — public; код только на сервере — confidential.

Два поля, на которых спотыкаются все: redirect URI и Web origins

У записи о приложении есть два поля, из-за которых первое подключение обычно не работает. Оба про адреса, и ошибки в них дают разные, но одинаково загадочные симптомы.

Valid redirect URIs — список адресов, на которые Keycloak согласен вернуть пользователя после входа. Проверка нужна не для порядка: без неё злоумышленник подставил бы в запрос свой адрес и получил код входа вместо вас. Поэтому адрес из запроса сверяется со списком, и если он не подошёл, Keycloak не редиректит никуда, а показывает страницу с текстом Invalid parameter: redirect_uri. Это ошибка номер один при первом подключении, и ищут её не в коде: сверяют, что в списке стоит ровно тот адрес, который приложение отправляет, вместе со схемой, портом и путём. Один лишний или пропущенный слеш на конце — уже несовпадение.

Web origins — список источников, которым Keycloak разрешает обращаться к себе из браузера. Это про механизм проверки источника запроса (CORS), и касается он только фронтенда: одностраничное приложение само ходит на /token за токенами, а браузер перед этим спрашивает у Keycloak разрешения. Если источника нет в списке, браузер запрос не выпустит, и в консоли появится жалоба на CORS — а разработчик прочитает её как «Keycloak не отвечает» или «токен не выдаётся». Значение + в этом поле удобно: оно разрешает источники, выведенные из списка redirect URIs.

Проверить оба поля дешевле всего с самого начала: пройти вход руками в браузере и посмотреть, доходит ли дело до возврата и до запроса на /token. Что именно в этих полях допустимо ставить, а что открывает дыру, разбирает чек-лист ниже.

Service account — учётная запись самого приложения

Отдельный случай confidential client: приложение, которое ходит в другое приложение без человека. Ночная выгрузка, сервис заказов, вызывающий сервис оплаты, обработчик очереди. Пользователя здесь нет, а токен нужен.

Для этого у client включают Service accounts roles (вкладка «Settings», требует включённой «Client authentication»). Keycloak создаёт для такого клиента отдельную учётную запись с именем вида service-account-order-service — она видна в списке пользователей и ведёт себя как пользователь, только без пароля и без возможности войти в браузере. Роли вешают именно на неё: открыть client, вкладка «Service accounts roles», назначить нужные.

Дальше приложение получает токен по паре client_id и client_secret:

curl -s -X POST https://auth.example.com/realms/myapp/protocol/openid-connect/token \
  -d grant_type=client_credentials \
  -d client_id=order-service -d client_secret=$CLIENT_SECRET

В полученном токене sub указывает на эту служебную запись, а не на человека, поле preferred_username равно service-account-order-service, и по полю azp видно, какой клиент его получил. Принимающая сторона по этим признакам и отличает сервисный вызов от пользовательского.

Две вещи, которые тут делают неправильно. Роли назначают самому клиенту «по привычке» через realm-роли для всех — тогда любой сервис получает права любого; право дают узкое и именно этой служебной записи. И второе: у такого клиента выключают всё остальное — Standard flow, Direct access grants, — потому что человек через него входить не должен. Как этим пользуются на стороне вызывающего сервиса и что проверяет принимающий, разбирает статья про межсервисные вызовы.

Пользователи

Мы разобрали, где живут приложения. Теперь — про людей.

Пользователь (user) — это учётная запись человека внутри realm. У неё есть логин, пароль, email, имя, а также произвольные дополнительные поля, которые в Keycloak называют атрибутами (например, отдел или номер телефона). Именно пользователь проходит вход: вводит логин и пароль, а Keycloak их проверяет.

Откуда берутся пользователи? Способов несколько:

  • их создают вручную в админке Keycloak;
  • они регистрируются сами, если регистрация включена;
  • их подтягивают из внешнего каталога компании (например, LDAP или Active Directory);
  • они приходят из входа через Google и подобные внешние сервисы.

Важный момент: сам по себе пользователь никаких прав не несёт. «Иван существует» — это ещё не «Ивану что-то можно». Права пользователю дают роли — к ним и переходим.

Роли: realm-роли против client-ролей

Вот центральная развилка модели, которую важно понять как следует.

Проблема: запись «пользователь Иван» не отвечает на вопрос «что Ивану можно». Нам нужен способ сказать «Иван — администратор» или «Иван может оформлять заказы». Именно для этого существуют роли.

Роль — это метка с правом, которую вешают на пользователя. admin, manager, customer — типичные роли. Дальше приложение смотрит на роли в токене и решает, пускать ли пользователя в ту или иную часть. Например, в раздел администрирования пускаем только тех, у кого есть роль admin.

В Keycloak роли бывают двух видов, и разница между ними — частый источник путаницы, поэтому разберём её подробно.

Realm-роль — это роль, общая для всего realm. Она не привязана ни к какому конкретному приложению и имеет смысл во всём проекте сразу. Хорошие кандидаты в realm-роли — это общие, сквозные понятия: admin, user, manager. Если у вас одно приложение или несколько приложений, но право означает одно и то же везде, — это realm-роль.

Client-роль — это роль, которая принадлежит конкретному client (приложению). Она существует только в контексте этого приложения. Зачем такое нужно? Затем, что одно и то же слово в разных приложениях может значить разное. Роль manager в приложении myapp-backend и роль manager в приложении reports-app — это две независимые роли, они не пересекаются. У «менеджера» в основном приложении и «менеджера» в системе отчётов могут быть совершенно разные права, и client-роли позволяют их не смешивать.

Аналогия, которая хорошо ложится: realm-роль — это пропуск во всё здание, а client-роль — ключ от одной конкретной комнаты внутри этого здания.

Когда какие выбирать? Для маленького проекта обычно хватает realm-ролей — они проще: завёл admin и user, раздал пользователям, и всё. Client-роли берут, когда приложений несколько и права в них реально различаются настолько, что общие realm-роли начинают мешать.

Отдельно полезно знать про составные роли (composite). Это роль, которая включает в себя другие роли. Назначаешь пользователю одну такую роль — и вместе с ней он автоматически получает все вложенные. Классический пример: роль admin включает в себя роль user. Тогда любому администратору не нужно отдельно выдавать user — она приедет вместе с admin. Удобно, чтобы не дублировать назначения. Удобство это не бесплатное, и цену стоит знать заранее. Составная роль раскрывается в токен целиком: если admin включает пять ролей, а каждая из них ещё по три, в realm_access.roles приедут все пятнадцать, а не одна. Отсюда два последствия. Токен растёт, а он едет в заголовке каждого запроса и упирается в ограничения прокси — об этом статья про токены. И главное, становится непонятно, откуда у человека право: в токене видно payouts-approve, а по какой из вложенных ролей она приехала и кто её там выдал, по токену не сказать вовсе.

Второй слой той же проблемы — группы вместе с составными ролями. Роли приезжают из группы, группа наследует роли родительской группы, а роль внутри составная: в итоге у человека набор прав, который никто не может объяснить, не разложив вручную три уровня вложенности. Поэтому дисциплина такая: вкладывать роли не глубже одного уровня, не смешивать «роли через группы» и «составные роли» для одного и того же права, и держать в realm короткий список — разбор в статье про роли и доступ.

Группы — чтобы не раздавать роли по одной

Роли мы умеем раздавать пользователям поштучно. Но представьте: у вас 200 пользователей, и каждому надо назначить один и тот же набор из пяти ролей. Делать это руками, по одному человеку, — мучительно и легко где-нибудь ошибиться (кому-то роль забыли, кому-то лишнюю дали).

Эту боль решают группы.

Группа (group) — это набор пользователей с общими ролями и атрибутами. Логика такая: вы назначаете роли группе один раз, а каждый пользователь, который попадает в эту группу, автоматически получает все её роли. Добавил человека в группу — он сразу получил нужные права. Перевёл в другую группу — набор прав поменялся. Не надо трогать роли у каждого вручную.

Группы можно вкладывать друг в друга: подгруппа наследует роли своей родительской группы. Например, группа «Сотрудники» даёт базовые права, а вложенная в неё группа «Бухгалтерия» добавляет ещё свои — и член бухгалтерии получает и то, и другое.

Чтобы не путать роли и группы, запомните разницу одной фразой: роль — это само право; группа — это удобный способ раздать пачку прав сразу многим пользователям.

Теперь соберём всю модель в одну картину. На схеме видно дерево: realm — корень, внутри него лежат client'ы, пользователи, группы и realm-роли; а client-роли «висят» уже под конкретным client'ом, не на уровне realm.

Дерево realm myapp: внутри лежат clients, users, groups и realm roles, client roles висят под конкретным client, а группы включают пользователей и раздают им роли

Пунктирные стрелки на схеме читаются так: группы раздают своим участникам роли и содержат пользователей. Сплошные стрелки — это «что во что вложено».

Как роли попадают в токен

А вот частая точка, где всё ломается на практике. Вы назначили пользователю роль в админке, всё выглядит правильно — а приложение её «не видит» и не пускает. Причина почти всегда одна и та же: роль не попала в токен.

Вспомним мысль из начала статьи: токен — это не запрос в базу Keycloak в реальном времени. Это готовая, уже подписанная «справка», и приложение читает права прямо из неё, не ходя в Keycloak. А значит, нужные данные должны оказаться внутри токена в тот момент, когда Keycloak его выдаёт. Если роли в токене нет — приложению неоткуда о ней узнать, сколько бы ролей ни висело на пользователе в админке.

За то, что и в каком виде попадает в токен, отвечают два механизма.

Протокол-маппер (protocol mapper) — это правило вида «возьми вот это (роль, атрибут, email) и положи в токен под таким-то именем». Хорошая новость: мапперы для realm-ролей и для client-ролей в Keycloak есть из коробки и обычно включены по умолчанию. То есть в типовой настройке роли попадают в токен сами, без ручной работы. Но если кто-то их выключил или вы настраивали client вручную «с нуля» — вот тут роли и могут потеряться.

Client scope — это переиспользуемый набор мапперов. Вместо того чтобы вешать одни и те же мапперы на каждый client руками, их собирают в client scope и подключают к нужным client'ам разом. Это просто способ не повторять одну и ту же настройку много раз.

Теперь — куда именно роли ложатся в токене. Эти поля стоит знать наизусть, потому что именно на них смотрит приложение:

{
  "preferred_username": "ivan",
  "realm_access": {
    "roles": ["admin", "user"]
  },
  "resource_access": {
    "myapp-backend": {
      "roles": ["manager"]
    }
  }
}

Читается так:

  • realm-роли лежат в поле realm_access.roles — это плоский список общих ролей;
  • client-роли лежат в resource_access.<client-id>.roles — то есть сгруппированы по приложению, к которому относятся.

С client-ролями есть вторая причина пропажи, и она коварнее первой, потому что маппер при ней совершенно исправен. Keycloak кладёт в resource_access роли не всех подряд приложений, а только тех, которые попали в область действия этого токена. Пока у клиента включена настройка «Full scope allowed» — попадают все, и вопрос не встаёт. А вот как только её выключили (а выключают её как раз из соображений безопасности, чтобы токен не носил лишнего), в токен приедут роли только тех приложений, которые явным образом подключены к клиенту через client scope. Не подключили — роли в токене нет, хотя на пользователе она висит и маппер включён.

Практический вывод: если роль есть у пользователя, но в токене её нет — не ищите проблему в назначении роли (там всё в порядке). Смотрите в две стороны: выключенный или неподключённый маппер и — для client-ролей — область действия токена.

Как это читает приложение

Соберём концы: токен выдан, роли в нём есть — что делает с ним серверное приложение?

Серверное приложение выступает в роли resource server — буквально «сервер ресурсов». Оно само не занимается входом (логином, паролями, перенаправлениями) — этим занят фронтенд вместе с Keycloak. Бэкенду приходит уже готовый токен в заголовке Authorization: Bearer <access_token>, и его задача — только проверить этот токен и пустить или не пустить.

Минимум настройки для Spring Boot — указать issuer вашего realm:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com/realms/myapp

Что происходит дальше автоматически. По этому адресу Spring сам находит служебный файл .well-known/openid-configuration (его публикует Keycloak), оттуда узнаёт адрес с публичными ключами realm (JWKS), скачивает ключи и кэширует их у себя. После этого каждый входящий токен Spring проверяет локально: сходится ли подпись (теми самыми ключами) и тот ли издатель (issuer). Никакого обращения к Keycloak на каждый запрос не происходит — поэтому это быстро.

Одна типичная доработка, без которой роли не заработают. Spring по умолчанию не знает, что роли лежат именно в realm_access.roles — это формат, специфичный для Keycloak, а не общий стандарт. Поэтому к нему добавляют небольшой конвертер ролей: он достаёт роли из realm_access.roles и превращает их в привычные Spring права вида ROLE_admin. После этого работают обычные проверки доступа на эндпоинтах. Подробно этот конвертер разбирается в отдельной статье про защиту API — ссылка ниже.

Чтобы связка была рабочей здесь и сейчас, вот он целиком — это действительно несколько строк:

@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter roles = new JwtGrantedAuthoritiesConverter();
    roles.setAuthoritiesClaimName("realm_access.roles");
    roles.setAuthorityPrefix("ROLE_");

    JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
    converter.setJwtGrantedAuthoritiesConverter(roles);
    return converter;
}

Поддержка вложенного пути (realm_access.roles вместо просто roles) появилась в Spring Security 6.4; в более старых версиях тот же результат получают своим преобразователем, который сам читает Map из поля realm_access. И ROLE_ в префиксе не украшение: без него hasRole('admin') не найдёт роль, потому что этот метод сам дописывает ROLE_ к имени. Разбор этой путаницы и client-роли вместо realm-ролей — в статье про роли и доступ.

Минимум для Go — библиотека coreos/go-oidc: ей дают адрес realm (issuer), она сама читает .well-known/openid-configuration, скачивает JWKS, кэширует ключи и дальше проверяет подпись и издателя локально, не обращаясь к Keycloak на каждый запрос:

provider, err := oidc.NewProvider(ctx, "https://auth.example.com/realms/myapp")
verifier := provider.Verifier(&oidc.Config{ClientID: "myapp-backend"})

func authenticate(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		raw := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
		token, err := verifier.Verify(r.Context(), raw)
		if err != nil {
			w.Header().Set("WWW-Authenticate", `Bearer error="invalid_token"`)
			http.Error(w, "", http.StatusUnauthorized)
			return
		}
		var claims struct {
			RealmAccess struct {
				Roles []string `json:"roles"`
			} `json:"realm_access"`
		}
		if err := token.Claims(&claims); err != nil {
			http.Error(w, "", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r.WithContext(withRoles(r.Context(), claims.RealmAccess.Roles)))
	})
}

Роли лежат в realm_access.roles — это формат Keycloak, а не общий стандарт, поэтому их достают из claims руками и кладут в контекст запроса; проверка доступа дальше — обычное сравнение со списком. Client-роли вместо realm-ролей и путаница вокруг них разобраны в статье про роли и доступ.

Минимум для NestJS — стратегия passport-jwt с jwks-rsa: ключи она берёт по адресу JWKS realm, кэширует и проверяет подпись и издателя локально, не обращаясь к Keycloak на каждый запрос:

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      issuer: 'https://auth.example.com/realms/myapp',
      algorithms: ['RS256'],
      secretOrKeyProvider: passportJwtSecret({
        jwksUri: 'https://auth.example.com/realms/myapp/protocol/openid-connect/certs',
        cache: true,
      }),
    });
  }

  validate(payload: { sub: string; realm_access?: { roles: string[] } }) {
    return { id: payload.sub, roles: payload.realm_access?.roles ?? [] };
  }
}

Роли лежат в realm_access.roles — это формат Keycloak, а не общий стандарт, поэтому validate достаёт их сам и кладёт в request.user; дальше guard с @Roles('admin') сравнивает их со списком. Client-роли вместо realm-ролей и путаница вокруг них разобраны в статье про роли и доступ.

Минимум для FastAPI — PyJWT с PyJWKClient: он читает JWKS realm, кэширует ключи и проверяет подпись и издателя локально, не обращаясь к Keycloak на каждый запрос:

ISSUER = "https://auth.example.com/realms/myapp"
jwks = PyJWKClient(f"{ISSUER}/protocol/openid-connect/certs", cache_keys=True)
bearer = HTTPBearer()


def current_user(creds: HTTPAuthorizationCredentials = Depends(bearer)) -> User:
    try:
        key = jwks.get_signing_key_from_jwt(creds.credentials).key
        claims = jwt.decode(creds.credentials, key, algorithms=["RS256"], issuer=ISSUER, audience="myapp-backend")
    except jwt.PyJWTError:
        raise HTTPException(401, headers={"WWW-Authenticate": 'Bearer error="invalid_token"'})
    return User(id=claims["sub"], roles=claims.get("realm_access", {}).get("roles", []))

Роли лежат в realm_access.roles — это формат Keycloak, а не общий стандарт, поэтому зависимость достаёт их сама; дальше проверка доступа — обычное сравнение со списком в зависимости require_role("admin"). Client-роли вместо realm-ролей и путаница вокруг них разобраны в статье про роли и доступ.

Минимум для связки «бэкенд + фронт»

Теперь, когда модель понятна по частям, соберём её в практический чек-лист. Для типичного приложения с серверным бэкендом и браузерным фронтендом нужно:

  1. Один realm под проект — например myapp. Именно свой, не master: в master живут администраторы самого Keycloak, и учётка оттуда открывает весь сервер.
  2. Два client'а в этом realm:
    • myapp-frontend — public, с PKCE, для одностраничного фронтенда: секрет в браузере спрятать негде, он оказался бы в JS-бандле у каждого посетителя;
    • myapp-backend — confidential, с client secret, для серверного API.
  3. Роли — для начала realm-роли, например admin и user. Client-роли подключают позже, если приложений станет несколько и права начнут различаться.
  4. Группы — по желанию: удобно, когда пользователей много и наборы прав типовые.
  5. Пользователи с назначенными ролями — напрямую или через группы.
  6. Проверить, что realm-роли реально попадают в токен (поле realm_access.roles) — мапперы для этого обычно уже включены, но убедиться стоит: иначе бэкенд увидит вошедшего пользователя без единой роли и ответит 403 на всё.
  7. На бэкенде — resource server с адресом вашего realm и чтением ролей из realm_access.roles: без этого роли из токена до проверки доступа не доедут (в Spring это конвертер ролей, иначе hasRole не сработает ни разу).

Этого достаточно, чтобы фронтенд логинил пользователя через Keycloak, получал токен и слал его в API, а бэкенд проверял токен и пускал пользователя по ролям.

Как это выглядит в маркетплейсе

В маркетплейсе один realm marketplace: делить его по ролям нельзя, потому что продавец бывает и покупателем. Тестовый контур поднимают отдельным сервером Keycloak, а не realm в бою.

Клиенты заводят по приложениям: storefront-web, seller-cabinet и mobile-app — public с PKCE, серверные backoffice и order-service — confidential. Последний получает токен по client credentials для вызова Payment без человека.

Общие роли buyer, seller, moderator, dispute-operator и finance — realm-роли. Действия внутри backoffice — client-роли: card:approve, dispute:resolve, payout:approve. Сотрудник может быть модератором и при этом не иметь права на выплаты. Группы «Модерация» и «Споры» собирают типовые наборы прав для новых сотрудников.

В токене модератора это видно как realm_access.roles: [moderator] и resource_access.backoffice.roles: [card:approve]. Сервисы проверяют эти поля, а владение конкретной карточкой или заказом — отдельно, как в статье про роли и доступ.

Дополнительно: при первом чтении можно пропустить

Глубже: что выключить сразу после создания clientрасширенное

Свежесозданный client работает, и это плохой знак: несколько настроек по умолчанию или по привычке открывают дыры, которые в статьях упоминаются одной фразой. Чек-лист на пять минут после «Create client».

  • Direct access grants выключить. Это вход по паролю прямо через /token, без браузера и без второго фактора; нужен только для стенда и редких скриптов, а оставленный включённым он позволяет перебирать пароли мимо формы входа и обходить второй фактор.
  • Implicit flow выключить. Токен в адресной строке, в истории браузера и в логах прокси; поток объявлен устаревшим, о чём статья про authorization code flow.
  • Standard flow оставить, для серверных сервисов без пользователей включить только Service accounts и выключить всё остальное.
  • Valid redirect URIs точные, без * в конце домена и без http://localhost в боевом realm. Адрес с * позволяет отправить код входа на подконтрольный злоумышленнику путь; в новых версиях Keycloak предупреждает о *, но не запрещает.
  • Web origins явные, без *; значение + разрешает источники из списка redirect URIs, и это обычно то, что нужно для фронтенда.
  • Client authentication включить у серверных клиентов, и секрет хранить как секрет, а не в application.yml.
  • Full scope allowed выключить (вкладка «Client scopes», выделенный scope клиента, «Scope»). Пока включено, в токен попадают все роли пользователя во всех клиентах realm, и сервис оплаты видит роли из админки склада; выключенный режим кладёт только роли, явно назначенные этому клиенту, и заодно делает токен короче.
  • PKCE для public client обязателен: «Advanced», «Proof Key for Code Exchange Code Challenge Method» в S256.
  • Consent для своих приложений выключен, для сторонних включён: пользователь должен видеть, кому отдаёт доступ.
  • Срок жизни токенов у клиента не увеличивать сверх realm ради удобства: access в минуты, о причинах в статье про токены.

Проверить это можно не мышью, а экспортом клиента в JSON и сравнением с эталоном, о чём следующий раздел; в realm с десятком клиентов ошибка в одном находится только так.

Глубже: эксплуатация: realm как код, прокси, база и обновлениярасширенное

Realm экспортируют в файл и хранят в репозитории: kc.sh export --dir /tmp/export --realm shop --users realm_file. Импорт при старте создаёт realm, но не обновляет существующий, поэтому изменения применяют через keycloak-config-cli или провайдер Terraform. Секреты клиентов в файл не кладут. Прокси, резервные копии и обновления разобраны в разделе о цене владения вводной статьи, здесь не повторяем.

Коротко

  • Realm — изолированное пространство со своими пользователями, приложениями, ролями и настройками; master только для администрирования, под проект создают свой.
  • Client — запись о приложении. Public для браузера и мобильных не хранит секрет и требует PKCE, confidential для сервера имеет client secret.
  • Realm-роль общая для пространства, client-роль принадлежит одному приложению, группа раздаёт наборы ролей. Составные роли раскрываются в токен целиком и усложняют происхождение прав.
  • Мапперы кладут realm-роли в realm_access.roles, client-роли — в resource_access.<client-id>.roles. Роль есть, а в токене нет — проверяют маппер и область токена, а не назначение.
  • Resource server проверяет подпись локально по ключам realm и читает роли из claims. Минимум: realm, public и confidential client, realm-роли, пользователи и resource server.
  • После создания client выключают Direct access grants, Implicit flow и Full scope allowed, задают точные redirect URIs и Web origins. Service account получает роли отдельно, остальные потоки выключены. Настройки realm хранят как код.

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