# Keycloak

# Установка

<span style="color: rgb(0, 0, 0);">[Официальный сайт](https://www.keycloak.org/getting-started/getting-started-docker)</span>

**<span style="color: rgb(0, 0, 0);">Docker:</span>**

<span style="color: rgb(0, 0, 0);">Для 26 версии:</span>

```
docker run 
    -p 8080:8080 
    -e KC_BOOTSTRAP_ADMIN_USERNAME=admin 
    -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin 
    quay.io/keycloak/keycloak:26.2.5 start-dev
```

<span style="color: rgb(0, 0, 0);">Для 22 версии:</span>

```
 docker run 
    -e KEYCLOAK_ADMIN=admin 
    -e KEYCLOAK_ADMIN_PASSWORD=admin 
    -p 8080:8080 quay.io/keycloak/keycloak:22.0.0 
    start-dev
```

Можно еще напрямую через JDK.

Упрощенный compose

```yaml
services:
  keycloak:
    image: quay.io/keycloak/keycloak:26.3.2
    container_name: keycloak
    environment:
      KC_BOOTSTRAP_ADMIN_USERNAME: admin
      KC_BOOTSTRAP_ADMIN_PASSWORD: SecretPassword123
    ports:
      - '0.0.0.0:8180:8080'
    command: start-dev
    volumes:
      - ~/kk/data:/opt/keycloak/data

```

Для хранения данных либо встроенная база (H2), либо нужно отдельно поднять postgresql:

Обязательно изменить константу KC\_HOSTNAME, происходит перенаправление

```yaml
services:
  postgres:
    image: postgres:15
    container_name: keycloak_postgres
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: keycloak
    ports:
      - "5433:5432"
    volumes:
      - ./postgres_data:/var/lib/postgresql/data
    networks:
      - keycloak_net

  keycloak:
    image: quay.io/keycloak/keycloak:26.2
    container_name: keycloak
    command: start-dev
    environment:
      KC_DB: postgres
      KC_DB_URL_HOST: postgres
      KC_DB_URL_DATABASE: keycloak
      KC_DB_USERNAME: keycloak
      KC_DB_PASSWORD: keycloak
      KC_HOSTNAME: localhost
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
    ports:
      - "9090:8080"
    depends_on:
      - postgres
    networks:
      - keycloak_net

networks:
  keycloak_net:
```

**Dev / prod режимы**

Параметр start-dev запускает в режиме dev, предназначен для локальной разработки и тестирования.

- Упрощенный запуск: автоматически применяются настройки, не требующие сложной конфигурации.
- Включён HTTP: можно запускать без HTTPS.
- Отключена проверка сертификатов: удобно для работы с самоподписанными сертификатами.
- Включены developer-friendly endpoints: например, доступ к /admin API может быть менее защищен.
- Более подробные логи: включая stack trace.
- Слабые требования к паролям и политике безопасности.
- Нет ограничений на CORS и Content-Security-Policy, если не заданы явно.

prod - предназначен для развертывания в боевой среде.

- Требуется HTTPS: нельзя запустить без конфигурации TLS.
- Политики безопасности применяются строго (например, CSP, CORS, защита от CSRF).
- Проверка конфигурации при старте: ошибки в настройке вызовут отказ запуска.
- Пароли и другие секреты не должны быть слабыми.
- Отключены "удобные" фичи, потенциально опасные в проде (dev endpoints и т.д.).
- Подразумевается использование внешней базы данных (а не H2).
- Повышенные требования к производительности и отказоустойчивости.

# Теория

По логике система управления доступом должна разрешить/запретить пользователю выполнить действие с данными. Т е

can\_do\_it = F(user, data, action)

Причем система может быть как размещена снаружи и возвращать да или нет, так быть встроенной в систему и непосредственно блокировать действия. В случае keycloak система размещена снаружи. Предположительная схема взаимодействия:

<div drawio-diagram="270"><img src="http://bobrobotirk.ru/uploads/images/drawio/2026-04/drawing-1-1775998252.png" alt=""/></div>

Приложение перенаправляет пользователя в Keycloak. Тот запрашивает логин и пароль, возвращает обратно код. Приложение обменивает код на токен и начинает с ним работать. С этого момента пользователь считается авторизованным — можно проверять его права, роли и доступы.

[https://www.youtube.com/watch?v=uq2I9z\_ZB6Q](https://www.youtube.com/watch?v=uq2I9z_ZB6Q)

[![image.png](http://bobrobotirk.ru/uploads/images/gallery/2025-11/scaled-1680-/qCgimage.png)](http://bobrobotirk.ru/uploads/images/gallery/2025-11/qCgimage.png)

<span style="color: rgb(0, 0, 0);">Прослойка, объединяющая технологии авторизации.</span>

<span style="color: rgb(0, 0, 0);">Функциональность:</span>

- <span style="color: rgb(0, 0, 0);">Single-sign on, Single-sign out для браузерных приложений</span>
- <span style="color: rgb(0, 0, 0);">Поддержка OpenID/OAuth 2.0/SAML</span>
- <span style="color: rgb(0, 0, 0);">Identity Brokering - аутентификация с помощью внешних провайдеров, </span><span style="color: rgb(0, 0, 0);">Social login</span>
- <span style="color: rgb(0, 0, 0);">User federation - синхронизация с LDAP / Active directory / Kerberos</span>
- <span style="color: rgb(0, 0, 0);">Консоль администратора</span>

**<span style="color: rgb(0, 0, 0);">Блоки keycloak</span>**

**Realm**

<span style="color: rgb(0, 0, 0);">Деление на блоки через realm. Каждый realm изолирован, пользователь принадлежит только одному realm. Содержит конфигурацию, набор приложений и пользователей. Есть административный и остальные realm.</span>

<span style="color: rgb(0, 0, 0);">Есть консоль управления реалм и консоль управления аккаунтом.</span>

**<span style="color: rgb(0, 0, 0);">Клиенты (clients)</span>**

<span style="color: rgb(0, 0, 0);">Клиенты - сущности, которые могут отправлять запрос в Keycloak на аутентификацию пользователя. Есть встроенные клиенты. Эти встроенные клиенты создаются автоматически для каждого realm.</span>

**<span style="color: rgb(0, 0, 0);">Области видимости клиентов (client scopes)</span>**

<span style="color: rgb(0, 0, 0);">Определяет данные и разрешения, получаемые клиентом в access token / ID token при аутентификации пользователя.</span>

**<span style="color: rgb(0, 0, 0);">Роли и группы</span>**

<span style="color: rgb(0, 0, 0);">Роли - атомарное право или набор прав, который назначается пользователю. Группы - иерархическая структура. Отличия между ролями и группами:</span>

<table border="1" id="bkmrk-%D0%A0%D0%BE%D0%BB%D0%B8-%D0%93%D1%80%D1%83%D0%BF%D0%BF%D1%8B-%D0%9D%D0%B0%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD" style="border-collapse: collapse; width: 100%; height: 178.781px;"><colgroup><col style="width: 21.3349%;"></col><col style="width: 36.8396%;"></col><col style="width: 41.8254%;"></col></colgroup><thead><tr style="height: 29.7969px;"><td class="align-center" style="height: 29.7969px;">  
</td><td class="align-center" style="height: 29.7969px;">Роли</td><td class="align-center" style="height: 29.7969px;">Группы</td></tr></thead><tbody><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Назначение</td><td class="align-center" style="height: 29.7969px;">права доступа</td><td class="align-center" style="height: 29.7969px;">организация пользователей</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Уровень</td><td class="align-center" style="height: 29.7969px;">низкий (atomic)</td><td class="align-center" style="height: 29.7969px;">высокий (aggregation)</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Используются в коде</td><td class="align-center" style="height: 29.7969px;">Да</td><td class="align-center" style="height: 29.7969px;">Косвенно</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Наследование</td><td class="align-center" style="height: 29.7969px;">Нет</td><td class="align-center" style="height: 29.7969px;">Да</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Иерархия</td><td class="align-center" style="height: 29.7969px;">Нет</td><td class="align-center" style="height: 29.7969px;">Да</td></tr></tbody></table>

<span style="color: rgb(0, 0, 0);">Т е группы служат скорее для удобства управления пользователями в keycloak, фактическое разрешение действий происходит за счет ролей. </span>

<span style="color: rgb(224, 62, 45);">Самое важное: keycloak просто отдает сопоставление пользователь - роль. Группа - дополнительное удобство. </span>

<span style="color: rgb(224, 62, 45);">Ограничения на стороне приложения.</span>

<span style="color: rgb(0, 0, 0);">Могут быть настроены на уровне realm и client. </span>

```json
{
  "realm_access": {
    "roles": ["admin"]
  },
  "resource_access": {
    "my-client": {
      "roles": ["editor"]
    }
  }
}
```

Здесь admin - роль уровня realm, editor - роль уровня client. Роли используются для проверки доступа в коде. Но есть возможность перенести проверку разрешения выполнения endpoint на keycloak.

**<span style="color: rgb(0, 0, 0);">Консоль управления сервером</span>**

<span style="color: rgb(0, 0, 0);">**Доступ:** ip\_keycloak:8080/admin</span>

<span style="color: rgb(0, 0, 0);">Группы (организационная принадлежность) и роли (набор разрешений). Когда пользователь добавляется в группу, он наследует все роли. Единственная задача keycloak: Пользователь - Разрешение. Внутри роли сопоставляется разрешение. Группа упрощает управление пользователями.</span>

**Протоколы и технологии**

**LDAP**

Lightweight Directory Access Protocol - протокол доступа к каталогу пользователей, используемый для хранения и получения информации о пользователях, группах, ролях и другой организационной информации.  
Не занимается безопасной аутентификацией сам по себе - только хранит данные.

LDAP = база пользователей и их свойств

**Kerberos**

Протокол аутентификации, основанный на криптографии и "билетах".  
Позволяет пользователю один раз войти в систему (SSO) и получать доступ к другим сервисам без повторного ввода пароля.  
Часто используется в доменных Windows-сетях. Основан на централизованном сервере (KDC - Key Distribution Center).  
Kerberos = безопасный вход в систему + SSO

Сервер KDC:

- Центральный сервер в протоколе Kerberos, отвечает за аутентификацию пользователей и выдачу билетов (tickets) для доступа к другим сервисам. KDC состоит из двух основных компонентов:
- Authentication Server (AS) Проверяет логин и пароль пользователя и выдает TGT - ticket-granting ticket (билет доступа к другим билетам).
- Ticket Granting Server (TGS) Получает TGT от пользователя и выдает сервисные билеты (service tickets) - они используются для доступа к конкретным приложениям, например, файловому серверу или веб-приложению.

**OAuth 2.0 и OpenID Connect (OIDC)**

Открытые протоколы безопасной аутентификации и авторизации пользователей в приложениях и API.

Роли:

- Клиент: приложение, запрашивающее доступ.
- Ресурсный сервер: API с защищенными данными.
- Пользователь: владелец данных.
- Провайдер: сервер, выдающий токены и подтверждающий личность.

<span id="bkmrk-oauth%3A-1">OAuth:</span>

- Протокол авторизации. Не занимается аутентификацией пользователя напрямую.
- Главная цель: разрешить приложению доступ к ресурсам пользователя (например, к его файлам, фото) без передачи пароля.
- Работает через выдачу токенов доступа (Access Token).
- Пример: приложение запрашивает у пользователя разрешение читать его документы в Google Docs.

<span id="bkmrk-openid-connect-%28oidc-1">OpenID Connect (OIDC)</span>

- Надстройка над OAuth 2.0, которая добавляет аутентификацию пользователя.
- OpenID Connect расширяет OAuth, чтобы приложение могло не только получить доступ к ресурсам, но и узнать, кто такой пользователь.
- Добавляет понятие ID Token - это отдельный токен (в формате JWT), который содержит информацию о пользователе (имя, email и т.д.).

**Типы токенов**

- Access Token - доступ к API 
    - Opaque. Непрозрачный токен (например, 8fa3b2e1-cb4e-4fd7-b517-84fc4c2f3a77), просто идентификатор. Для получения деталей ресурсный сервер обращеается к серверу авторизации. Плюсы: безопаснее (минимальные утечки данных через токен), легче отозвать токен централизованно. Минусы: нужен дополнительный сетевой вызов для валидации токена, больше нагрузка на сервер авторизации.
    - JWT самостоятельный токен, в себе несёт всю информацию (пользователь, права, срок жизни и т.д.). Токен подписан, ресурсный сервер проверяет токен без запроса к серверу авторизации. Плюсы: нет лишних сетевых вызовов на проверку и быстрее работает на стороне ресурсных серверов. Минусы: выданный токен нельзя "убить" мгновенно (если только через сложные механизмы типа revocation lists) и токен может содержать чувствительную информацию - важно правильно шифровать или минимизировать payload. Есть сайт jwt.io для просмотра данных из токена.
- Refresh Token - продление действия access token. Отправляется на сервер и сессия продлевается.
- ID Token - (только в OIDC) информация о пользователе

**Grant types** Технологии, посредством которых клиент получает токен.

Authorization code - получение кода и обмен его на токен доступа

# Realms

Деление на блоки через realm. Каждый realm изолирован, пользователь принадлежит только одному realm. Содержит конфигурацию, набор приложений и пользователей. Есть административный и остальные realm.

Master: административный realm, задачи:

- Управления другими realms (создание, удаление и настройка realms, управление пользователями-администраторами)
- Создание глобальных админов. Роли realm-admin, admin, create-realm и т. д. позволяют входить в Keycloak Admin Console (/admin) и управлять сервером.
- Использование Keycloak'ом для внутренних задач. Системные клиенты и сервисные аккаунты находятся в master realm.  
    Примеры: admin-cli, account, broker.

Есть консоль управления реалм.

**<span style="color: rgb(0, 0, 0);">Создание realm</span>**

Настройка realm в момент создания - только имя

[![image.png](http://bobrobotirk.ru/uploads/images/gallery/2026-04/scaled-1680-/YbFimage.png)](http://bobrobotirk.ru/uploads/images/gallery/2026-04/YbFimage.png)

 <span style="color: rgb(0, 0, 0);">Имя не должно содержать пробелов и иных символов, плохо воспринимаемых в url адресе.</span>

**<span style="color: rgb(0, 0, 0);">Настройка realm (realm settings)</span>**

<span style="color: rgb(0, 0, 0);">В меню Manage realms выбирается реалм, который мы будем редактировать. Затем в левом меню, раздел Configure содержит 4 блока.</span>

<span style="color: rgb(0, 0, 0);">OpenID Endpoint configuration - здесь можно увидеть все endpoint настроенные для данного realm. </span>

[![изображение.png](http://bobrobotirk.ru/uploads/images/gallery/2025-11/scaled-1680-/Mfcizobrazenie.png)](http://bobrobotirk.ru/uploads/images/gallery/2025-11/Mfcizobrazenie.png)

**Настройка процесса аутентификации.** Раздел Authentication используется для настройки и управления потоками аутентификации, обязательными действиями, и политиками входа.  
Authentication flow (поток аутентификации) - это набор шагов, через которые проходит пользователь или клиент при входе, регистрации, сбросе пароля и т.д. Каждый поток состоит из действий (executions): например, проверка пароля, OTP, условные переходы, выбор IdP и др.

Потоки создаются и настраиваются.

Grant Type - способ, получения Access Token от сервера авторизации в OAuth 2.0 / OpenID Connect. Тип разрешения (grant) определяет, как и при каких условиях клиент получает токены. Keycloak, как реализация Identity Provider и Authorization Server, поддерживает разные grant types (, ).

<table border="1" id="bkmrk-%D0%9D%D0%B0%D0%B7%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D0%9E%D0%BF%D0%B8%D1%81%D0%B0%D0%BD%D0%B8%D0%B5-%D0%A2%D0%B8" style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 17.5209%;"></col><col style="width: 64.3552%;"></col><col style="width: 18.1239%;"></col></colgroup><thead><tr><td>Название</td><td>Описание</td><td>Тип приложения</td></tr></thead><tbody><tr><td>Authorization Code</td><td>Стандартный безопасный flow для веб-приложений, где сервер клиента может хранить секреты. Клиент формирует POST-запрос к token-эндпоинту авторизационного сервера с обязательными параметрами:

- grant\_type -&gt; Authorization Code
- authorization\_code -&gt; code, полученный на шаге 6
- redirect\_uri -&gt; тот же URI, что использовался при авторизации
- client\_id -&gt; Идентификатор клиента
- client\_secret -&gt; Секрет клиента

безопасно для серверов с хранением Client Secret. Уязвимость на этапе редиректа. Подходит для серверных приложений

</td><td>Веб-приложения (backend + frontend).</td></tr><tr><td>Authorization Code + PKCE</td><td>Модификация Authorization Code Flow для публичных клиентов (например, мобильных и SPA), чтобы предотвратить атаку перехвата кода. Клиент перед началом авторизации:

- Генерирует случайный код (code\_verifier)
- Строит на его основе code\_challenge (обычно SHA-256)
- При первом запросе отправляется code\_challenge
- При обмене Authorization Code на Access Token отправляется code\_verifier.
- Authorization Server сверяет code\_challenge и code\_verifier:  
    Если всё сходится, только тогда выдает Access Token.  
    Это защищает от атаки перехвата кода, потому что даже если злоумышленник украдет Authorization Code, без правильного code\_verifier он не сможет обменять его на Access Token.

безопасно для мобильных и браузерных клиентов без секретов. Нужен только Code Verifier Защита на этапе редиректа Подходит для мобильных и SPA

</td><td>Мобильные приложения, SPA (Single Page Applications).</td></tr><tr><td>Client Credentials </td><td>Классический machine-to-machine сценарий, когда приложение запрашивает токен от собственного имени, без пользователя.</td><td>Сервисные взаимодействия (backend to backend), микросервисы.</td></tr><tr><td>Device Code</td><td>Для устройств без полноценной клавиатуры (например, Smart TV). Пользователь вводит код на другом устройстве.</td><td>Устройства с ограниченным вводом - SmartTV, консоли, IoT.</td></tr><tr><td>Refresh Token</td><td>После получения Access Token можно получить новый токен без повторной аутентификации пользователя.</td><td>Любые клиенты с долгоживущими сессиями.</td></tr></tbody></table>

**Authentication**

Настройка потоков аутентификации, обязательных действий и политик входа.  
Authentication flow (поток аутентификации) - это набор шагов, через которые проходит пользователь или клиент при входе, регистрации, сбросе пароля и т.д.  
Каждый поток состоит из действий (executions): например, проверка пароля, OTP, условные переходы, выбор IdP и др.

<table border="1" id="bkmrk-%D0%9D%D0%B0%D0%B7%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D0%9E%D0%BF%D0%B8%D1%81%D0%B0%D0%BD%D0%B8%D0%B5-br" style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 21.4541%;"></col><col style="width: 78.5459%;"></col></colgroup><thead><tr><td>Название</td><td>Описание</td></tr></thead><tbody><tr><td>browser</td><td>Встроенный поток для авторизации на основе браузера. <span id="bkmrk-%D0%92%D0%BA%D0%BB%D1%8E%D1%87%D0%B0%D0%B5%D1%82-%D1%84%D0%BE%D1%80%D0%BC%D1%8B-%D0%BB%D0%BE%D0%B3%D0%B8%D0%BD">Включает формы логина, OTP (двухфакторную аутентификацию), CAPTCHA и другие шаги.</span></td></tr><tr><td>clients</td><td><span id="bkmrk-%D0%92%D1%81%D1%82%D1%80%D0%BE%D0%B5%D0%BD%D0%BD%D1%8B%D0%B9-%D0%BF%D0%BE%D1%82%D0%BE%D0%BA-%D0%B0%D1%83%D1%82">Встроенный поток аутентификации для клиентов (используется в client credentials grant). Работает без участия пользователя.</span></td></tr><tr><td>direct grant</td><td><span id="bkmrk-%D0%92%D1%81%D1%82%D1%80%D0%BE%D0%B5%D0%BD%D0%BD%D1%8B%D0%B9-%D0%BF%D0%BE%D1%82%D0%BE%D0%BA-%D0%B4%D0%BB%D1%8F">Встроенный поток для ресурсного владельца (Resource Owner Password Credentials Grant). Используется, когда пользователь напрямую вводит логин и пароль (например, через curl или мобильное приложение).</span></td></tr><tr><td>docker auth</td><td><span id="bkmrk-%D0%9F%D0%BE%D1%82%D0%BE%D0%BA-%D0%B0%D1%83%D1%82%D0%B5%D0%BD%D1%82%D0%B8%D1%84%D0%B8%D0%BA%D0%B0%D1%86%D0%B8%D0%B8">Поток аутентификации Docker-клиентов при доступе к приватному Docker Registry через Keycloak.</span></td></tr><tr><td><span id="bkmrk-first-broker-login">First broker login</span></td><td><span id="bkmrk-%D0%9F%D0%BE%D1%82%D0%BE%D0%BA%2C-%D1%81%D1%80%D0%B0%D0%B1%D0%B0%D1%82%D1%8B%D0%B2%D0%B0%D1%8E%D1%89%D0%B8%D0%B9">Поток, срабатывающий при первом входе пользователя через внешний Identity Provider (например, Google, GitHub). Позволяет привязать внешний аккаунт к локальному пользователю Keycloak.</span></td></tr><tr><td><span id="bkmrk-registration"><span id="bkmrk-registration-1">Registration</span></span></td><td><span id="bkmrk-%D0%9F%D0%BE%D1%82%D0%BE%D0%BA-%D1%80%D0%B5%D0%B3%D0%B8%D1%81%D1%82%D1%80%D0%B0%D1%86%D0%B8%D0%B8-%D0%BD%D0%BE"><span id="bkmrk-%D0%9F%D0%BE%D1%82%D0%BE%D0%BA-%D1%80%D0%B5%D0%B3%D0%B8%D1%81%D1%82%D1%80%D0%B0%D1%86%D0%B8%D0%B8-%D0%BD%D0%BE-1">Поток регистрации новых пользователей. Включает форму ввода данных, подтверждение по email и проверки по политике безопасности (например, сила пароля).</span></span></td></tr><tr><td><span id="bkmrk-reset-credentials"><span id="bkmrk-reset-credentials-1">Reset credentials</span></span></td><td><span id="bkmrk-%D0%9F%D0%BE%D1%82%D0%BE%D0%BA-%D0%B4%D0%BB%D1%8F-%D0%B2%D0%BE%D1%81%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BB"><span id="bkmrk-%D0%9F%D0%BE%D1%82%D0%BE%D0%BA-%D0%B4%D0%BB%D1%8F-%D0%B2%D0%BE%D1%81%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BB-1">Поток для восстановления доступа, если пользователь забыл пароль. Может включать подтверждение по email, OTP и другие шаги для подтверждения личности.</span></span></td></tr></tbody></table>

У каждого realm свой открытый ключ. С его помощью можно проверять подлинность полученных токенов. После авторизации под любым пользователем выполнить один раз

```python
KEYCLOAK_PUBLIC_KEY = f"""
-----BEGIN PUBLIC KEY-----
{keycloak_openid.public_key()}
-----END PUBLIC KEY-----
"""
```

<div id="bkmrk-%D0%98-%D0%B2%D1%8B%D0%B2%D0%B5%D1%81%D1%82%D0%B8-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5.-"><div></div><div>И вывести значение. Это будет открытым ключом. </div></div>**User Federation**

Подключение внешних источников пользователей (LDAP, Active Directory или Kerberos).

- пользователи остаются в LDAP/AD/Kerberos;
- Keycloak может их аутентифицировать;
- можно синхронизировать или кэшировать данные (имя, email, роли и т.д.);
- поддерживается одноразовый вход (SSO), если внешняя система это позволяет.

**SAML**

Security Assertion Markup Language - открытый стандарт SSO между разными доменами. Позволяет аутентифицироваться в одной системе и получить доступ к другой, предоставив подтверждение своей аутентификации. Используется с 2005 года и остаётся популярным для федерации идентичности в B2B и B2E-сценариях, особенно в государственном и корпоративном секторе. SAML оперирует:

- XML - для описания данных пользователя
- HTTP - как транспортный протокол

# Clients

Клиенты - сущности, которые могут отправлять запрос в Keycloak на аутентификацию пользователя. <span style="color: rgb(0, 0, 0);">Чтобы приложение могло использовать ресурсы keycloak, оно должно быть зарегистрировано. Клиенты делятся по идентификаторам. </span>

Настройка через консоль управления реалмом, раздел клиенты. Желательно настраивать на каждый сервер / сервис отдельный клиент.

[![image.png](http://bobrobotirk.ru/uploads/images/gallery/2026-04/scaled-1680-/k3fimage.png)](http://bobrobotirk.ru/uploads/images/gallery/2026-04/k3fimage.png)

**Раздел Clients**

**Client list**

Client list - список клиентов. Параметры настройки клиента:

<table border="1" id="bkmrk-%D0%9F%D0%B0%D1%80%D0%B0%D0%BC%D0%B5%D1%82%D1%80-%D0%9E%D0%BF%D0%B8%D1%81%D0%B0%D0%BD%D0%B8%D0%B5-cl" style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 21.3321%;"></col><col style="width: 78.6679%;"></col></colgroup><thead><tr><td class="align-center">Параметр</td><td class="align-center">Описание</td></tr></thead><tbody><tr><td>Client type</td><td>Алгоритм аутентификации клиента. OpenID или SAML.</td></tr><tr><td>Client ID</td><td>Идентификатор клиента. Он будет использоваться в запросах, поэтому желательно с маленькой буквы, без пробелов и прочего.</td></tr><tr><td>Client authentication</td><td>Сможет ли приложение хранить секрет и использовать его для аутентификации на Keycloak.

**ON — Конфиденциальный клиент** Приложение обязано хранить секрет. При обмене авторизационного кода на токены клиент предъявляет код и Client Secret. Используется для бэкенда. Пример: FastAPI приложение на сервере, обращающееся к Keycloak из защищенной среды.

**OFF — Публичный клиент** У клиента нет секрета, при обмене кода на токены секрет не используется. Используется для одностраничных приложений, мобильных приложений.

</td></tr><tr><td>Authorization</td><td>Активация RBAC, доступ к конкретным строчкам в БД.

**OFF (выключено — поведение по умолчанию)**

 Пользователю даются роли (например, user, admin), и клиент сам решает на своей стороне, что с этими ролями делать.

 Решение принимает бэкенд клиента.

**ON (включено — тонкая (fine-grained) авторизация)**

 Как работает: Вы описываете ресурсы (например: Аккаунт №123, Документ "Отчет.xlsx"), области действий (read, write, delete) и политики (кто, при каких условиях может что делать). Keycloak сам принимает решение: разрешено или запрещено.

 Кто принимает решение: Keycloak (сервер авторизации).

 Ваше приложение: Спрашивает у Keycloak: «Может ли пользователь Вася выполнить read над ресурсом Аккаунт №123?» Keycloak отвечает: Permit или Deny.

После включения, в меню клиента появляются новые вкладки:

 Resources — что вы защищаете (например, Документы, Счета, Данные профиля).

 Authorization Scopes — действия (например, view, edit, delete, download).

 Policies — кто и при каких условиях может что-то делать (например, Только владелец ресурса, Только пользователи из отдела продаж, Только между 9:00 и 18:00).

 Permissions — связывание всего вместе: «Разрешить edit для ресурса Документ, если выполнена политика Владелец документа».

Включение Authorization усложняет архитектуру. Keycloak становится точкой принятия решений (PEP — Policy Enforcement Point на клиенте, PDP — Policy Decision Point на Keycloak). Это требует:

- Дополнительных запросов от вашего приложения к Keycloak (проверка каждого доступа).
- Глубокого понимания политик.
- Аккуратного проектирования ресурсов.

</td></tr><tr><td>Root URL</td><td>Базовый адрес вашего приложения. Используется как префикс для других URL, если они указаны относительно. Может быть пустым, но лучше указать. Некоторые функции Keycloak (например, ссылка "Back to application" на странице логина) используют этот URL.</td></tr><tr><td>Home URL</td><td>Куда пользователь попадет после нажатия на логотип Keycloak или ссылку "Back to application". Отличие от Root URL:

 Root URL — это база/префикс.

 Home URL — конкретная страница, которая считается "домашней" для приложения.

Если не указан, используется Root URL.

</td></tr><tr><td>Valid Redirect URIs</td><td>Адреса, на которые Keycloak разрешит отправлять авторизационный код или токены после успешной аутентификации.

```
https://myapp.example.com/callback
https://myapp.example.com/oauth/callback
http://localhost:3000/callback  # для разработки
/*                             # любой путь в рамках корневого URL
```

Для безопасности критично. Как минимум указывать относительный. Просто \* убивает всю авторизацию.

</td></tr><tr><td>Valid Post Logout Redirect URIs</td><td>Адреса, на которые Keycloak перенаправит пользователя после успешного выхода (logout). Отличие от Valid Redirect URIs:

 Обычные редиректы — после входа (авторизации)

 Post Logout — после выхода

Зачем нужно:  
Чтобы пользователь после выхода не попал на поддельную страницу, например: вышли из банка → а вас кинули на https://fake-bank.com/again-login.

Совет: Обычно указывают страницу вроде /goodbye, /logged-out или сам Root URL.

</td></tr><tr><td>Web Origins</td><td>Какие домены имеют право выполнять CORS-запросы к вашему Keycloak из браузера (для SPA-приложений).

Используется для: CORS (Cross-Origin Resource Sharing) на эндпоинтах Keycloak (/token, /logout, /userinfo и т.д.).

Пример для SPA на React:  
Ваше SPA работает на https://myapp.com, а Keycloak на https://keycloak.example.com.

Если Web Origins указан правильно, браузер позволит SPA из myapp.com делать fetch-запросы к Keycloak.

\+ Плюс означает использовать Valid Redirect URIs как список разрешенных источников

\* Любой источник (все домены)

</td></tr></tbody></table>

Клиенты по умолчанию в Master realm:

<table border="1" id="bkmrk-%D0%9D%D0%B0%D0%B7%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D0%9E%D0%BF%D0%B8%D1%81%D0%B0%D0%BD%D0%B8%D0%B5-%D0%90%D0%B4" style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 20.8512%;"></col><col style="width: 45.7757%;"></col><col style="width: 33.3731%;"></col></colgroup><thead><tr><td class="align-center">Название</td><td class="align-center">Описание</td><td class="align-center">Адрес</td></tr></thead><tbody><tr><td>account</td><td>Клиент пользовательского личного кабинета. Управление профилем, просмотр сеансов входа и разрешения.</td><td>/realms/{realm}/account</td></tr><tr><td>account-console</td><td>Фронтенд клиента account, вызывающий API account.  
</td><td>  
</td></tr><tr><td>admin-cli</td><td>Клиент взаимодействия с Keycloak через CLI или REST API.  
Например получение токена через kcadm.sh или curl для скриптов/CI/CD. Поддерживает client\_credentials и password гранты.  
</td><td>  
</td></tr><tr><td>broker</td><td>Внутренний клиент для федерации идентичности (SSO между провайдерами). Пример: Keycloak как Identity Broker между Google, GitHub и другим Keycloak сервером.

</td><td>  
</td></tr><tr><td>master-realm</td><td>Формальный клиент, представляющий сам master realm.  
Часто используется для определённых внутренних механизмов и ссылок в Admin Console.</td><td>  
</td></tr><tr><td>security-admin-console</td><td>Клиент для административной консоли Keycloak. Используется при входе в админ-панель, управлении реалмами, пользователями и клиентами.</td><td>/admin/{realm}/console</td></tr></tbody></table>

Пример работы с admin-cli:

```python
import requests

def get_keycloak_token():
    url = "http://192.168.1.195:9090/realms/master/protocol/openid-connect/token"
    payload = {
        "grant_type": "password",
        "client_id": "admin-cli",
        "username": "admin",
        "password": "admin",
    }
    headers = {
        "Content-Type": "application/x-www-form-urlencoded"
    }
    response = requests.post(url, data=payload, headers=headers)
    # Проверка на ошибки HTTP
    response.raise_for_status()
    print(response.json())

get_keycloak_token()

```

Ответ сервера:

```json
{
  'access_token': hbG...L7A', 
  'expires_in': 60, 
  'refresh_expires_in': 1800, 
  'refresh_token': 'hbG...gcQ', 
  'token_type': 'Bearer', 
  'not-before-policy': 0, 
  'session_state': 'f...c', 
  'scope': 'profile email'
}
```

Если затем сформировать например get запрос к endpoint /admin/realms с заголовком Authorization Brearer &lt;access\_token&gt; то будет доступ. Полный пример запроса:

```python
import requests

BASE_URL = "http://192.168.1.195:9090"

def get_keycloak_token():
    url = f"{BASE_URL}/realms/master/protocol/openid-connect/token"
    payload = {
        "grant_type": "password",
        "client_id": "admin-cli",
        "username": "admin",
        "password": "admin",
    }
    headers = {
        "Content-Type": "application/x-www-form-urlencoded"
    }
    response = requests.post(url, data=payload, headers=headers)
    response.raise_for_status()
    return response.json()["access_token"]

def get_realms(access_token: str):
    url = f"{BASE_URL}/admin/realms"
    headers = {
        "Authorization": f"Bearer {access_token}"
    }
    response = requests.get(url, headers=headers)
    response.raise_for_status()
    return response.json()

if __name__ == "__main__":
    try:
        token = get_keycloak_token()
        realms = get_realms(token)
        print("Realms:")
        for realm in realms:
            print("-", realm.get("realm"))
    except requests.HTTPError as e:
        print("HTTP error:", e.response.text)
    except Exception as e:
        print("Unexpected error:", str(e))
```

**Client registration**

Параметры, которые будут применяться к пользователям, зарегистрированным в клиентах.

**Раздел client scopes**

[![image.png](http://bobrobotirk.ru/uploads/images/gallery/2026-04/scaled-1680-/7UIimage.png)](http://bobrobotirk.ru/uploads/images/gallery/2026-04/7UIimage.png)

<span style="color: rgb(0, 0, 0);">Определяет данные и разрешения, получаемые клиентом в access token / через ID token при аутентификации пользователя. Взаимосвязь между атрибутами и scopes: </span>

<div drawio-diagram="276"><img src="http://bobrobotirk.ru/uploads/images/drawio/2026-04/drawing-1-1777567456.png" alt=""/></div>

<span style="color: rgb(0, 0, 0);">Маппер (Mapper) внутри Client Scope — это инструкция для Keycloak: «Возьми данные ОТСЮДА (атрибут, роль, контекст) и положи ИХ В ТОКЕН ПОД ТАКИМ ИМЕНЕМ».</span>

<span style="color: rgb(0, 0, 0);">Типы scope:</span>

- <span style="color: rgb(0, 0, 0);">Default: всегда включается в токен по умолчанию, даже если клиент не запрашивает его явно.</span>
- <span style="color: rgb(0, 0, 0);">Optional: будет включен в токен в случае явного запроса клиентом в параметре scope=... при авторизации.</span>
- <span style="color: rgb(0, 0, 0);">(Unassigned): не применяется клиенту вообще, пока его не назначат вручную или не укажут явно в токене.</span>

<span style="color: rgb(0, 0, 0);">Стандартные scopes: </span>

<table border="1" id="bkmrk-%D0%9D%D0%B0%D0%B7%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D0%A2%D0%B8%D0%BF-%D0%9F%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE" style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 14.5411%;"></col><col style="width: 10.0119%;"></col><col style="width: 11.5614%;"></col><col style="width: 63.8856%;"></col></colgroup><thead><tr><td>Название</td><td>Тип</td><td>Протокол</td><td>Назначение</td></tr></thead><tbody><tr><td>acr</td><td>Default</td><td>OpenID Connect</td><td>добавляет в токен acr (Authentication Context Class Reference) - уровень аутентификации (например, MFA, пароль и т. д.).</td></tr><tr><td>address</td><td>Optional</td><td>OpenID Connect</td><td>добавляет адрес пользователя (address claim) в токен (если заполнено в профиле).</td></tr><tr><td>basic</td><td>Default</td><td>OpenID Connect</td><td>включает базовые claims - sub, name, preferred\_username, given\_name, family\_name</td></tr><tr><td>e-mail</td><td>Default</td><td>OpenID Connect</td><td>добавляет email и email\_verified в ID и Access токены

</td></tr><tr><td>microprofile-jwt</td><td>Optional</td><td>OpenID Connect</td><td>обеспечивает совместимость с Eclipse MicroProfile JWT - популярным стандартом для Java microservices

</td></tr><tr><td>offline\_access</td><td>Optional</td><td>OpenID Connect</td><td>разрешает выдачу offline refresh token (живёт долго и используется без пользовательского взаимодействия).

</td></tr><tr><td>role\_list</td><td>Default</td><td>SAML</td><td>включает список ролей (roles) пользователя в SAML assertions

</td></tr><tr><td>roles</td><td>Default</td><td>OpenID Connect</td><td>добавляет роли пользователя (realm roles и client roles) в access token (в realm\_access.roles и resource\_access)

</td></tr></tbody></table>

<span style="color: rgb(0, 0, 0);">Также есть phone, profile</span>

<span style="color: rgb(0, 0, 0);">Добавление маппера:</span>

Типы мапперов:

<table border="1" id="bkmrk-%D0%98%D0%BC%D1%8F-%D0%9E%D0%BF%D0%B8%D1%81%D0%B0%D0%BD%D0%B8%D0%B5-allowed" style="border-collapse: collapse; width: 100%; height: 1667.89px;"><colgroup><col style="width: 28.359%;"></col><col style="width: 71.641%;"></col></colgroup><thead><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Имя</td><td style="height: 29.7969px;">Описание</td></tr></thead><tbody><tr style="height: 738.203px;"><td style="height: 738.203px;">Allowed Web Origins</td><td style="height: 738.203px;">Scope с данным mapper добавляет все разрешенные веб-источники к свойству "allowed-origins" в токене. Иначе это механизм копирования списка доверенных источников браузера из конфигурации Keycloak Client внутрь Access Token, чтобы бэкенд мог программно принять решение о разрешении CORS. При генерации токена он читает список разрешенных веб-источников, настроенный у Client (родителя этого Client Scope), и копирует этот список в указанный claim токена.

Поля конфигурации маппера:

- Name: Allowed Web Origins (любое имя)
- Mapper Type: Allowed Web Origins
- Claim name: Обычно allowed-web-origins (но можно кастомизировать)
- Claim JSON Type: String или String Array
- Add to ID token: Вкл/Выкл (обычно выкл, чтобы не раздувать ID токен)
- Add to access token: Вкл (обычно да)

**Пример**: SPA (на http://localhost:3000) стучится на бэкенд (http://localhost:8080). Бэкенд получает токен, но как ему узнать, что JavaScript вашего фронта имеет право вызывать его API?

Решение через маппер Allowed Web Origins:

- Вы идете в Client Scope (например, microservice-scope).
- Добавляете маппер типа Allowed Web Origins.
- Протоколируете этот scope к вашему клиенту (SPA).

Результат: Внутри Access Token появляется поле:

```json
{
  "allowed-web-origins": ["http://localhost:3000", "https://myapp.com"],
  ...
}
```

Теперь бэкенд:

- Декодирует токен.
- Видит значение allowed-web-origins.
- В middleware CORS сверяет Origin заголовок входящего запроса со списком.
- Если совпадает — отдает данные. Нет — блокирует.

Используется редко.

- Настроить Web Origins в клиенте, а бэкенд будет брать CORS-политику из конфига вне Keycloak.
- Использовать маппер, только если ваш бэкенд динамически решает, кому доверять, на основе данных из токена.

</td></tr><tr style="height: 46.5938px;"><td style="height: 46.5938px;">Audience, Hardcoded claim, Audience Resolve</td><td style="height: 46.5938px;">Решает вопрос "для кого этот токен" (Resource Server). Это поле aud (audience) в JWT токене. Оно указывает, какой ресурс (API/сервис) должен принять этот токен. ```json
{
  "iss": "https://keycloak.local/realms/myrealm",
  "sub": "user-123",
  "aud": ["account", "https://api.mycompany.com/v1"],
  ...
}
```

Зачем это нужно? Например, есть 3 микросервиса:

- payment-api (оплата)
- order-api (заказы)
- analytics-api (аналитика)

Если токен не имеет audience, любой сервис может принять токен, выданный для другого. Злоумышленник получает токен для payment-api, но пытается вызвать order-api. Без проверки aud — успешно.

Решение: Keycloak добавляет в токен aud = "payment-api". order-api проверяет этот claim и отвергает токен (403 Forbidden).

**Типы мапперов для Audience**

**Audience (простой)**

Создаете маппер в Client Scope, указываете жестко имя аудитории.

Конфигурация:

- Included Client Audience: выбираете клиента (например, payment-api)
- Add to ID token: обычно false
- Add to access token: true

Результат: В access token появится "aud": \["payment-api"\]  
**Hardcoded claim**

Вручную claim aud с любым значением (массивом или строкой).

Конфигурация:

- Claim name: aud
- Claim value: \["service-a", "service-b"\]
- Claim JSON Type: String Array

**Audience resolve (продвинутый)**

Автоматически собирает всех клиентов, которым был выдан токен, и добавляет их в aud.

Например, если клиент spa-app запрашивает токен с scope, включающим audience-resolve маппер, в aud попадут все клиенты, на которые у пользователя есть доступ через service accounts.

**Реальный сценарий использования**

**Сценарий: SPA + два микросервиса**

Настройка в Keycloak:

- Создаете Client для SPA: my-spa
- Создаете Client для каждого API: payment-api, order-api
- Создаете Client Scope с именем audience-payment
- Добавляете маппер типа Audience:
- Included Client Audience: payment-api

Процесс:  
// SPA запрашивает токен с scope = 'audience-payment'  
const token = await keycloak.getToken({ scope: 'audience-payment' });

// В токене:  
{  
 "aud": \["payment-api"\], // Только для payment-api  
 "scope": "audience-payment"  
}

// SPA вызывает payment-api ✅ разрешено  
// SPA вызывает order-api ❌ токен не содержит aud=order-api

**Сценарий: Токен для нескольких API**

Иногда нужно, чтобы один токен работал с несколькими сервисами:  
json

{  
 "aud": \["payment-api", "order-api", "analytics-api"\]  
}

Как сделать: Создаете маппер Hardcoded claim с массивом значений или используете несколько мапперов Audience.

</td></tr><tr style="height: 46.5938px;"><td style="height: 46.5938px;">Authentication Context Class Reference (ACR)</td><td style="height: 46.5938px;">Механизм указывающий способ (уровень или метод) аутентификации пользователя, а не просто факт. Claim acr.

```json
{
  "iss": "https://keycloak.local/realms/myrealm",
  "sub": "user-123",
  "acr": "1",
  "auth_time": 1699123456,
  ...
}
```

Конфигурация маппера:

- Тип маппера: Authentication Context Class Reference (ACR)
- Имя: acr-mapper (любое)
- Включить в ID token: true (рекомендуется)
- Включить в access token: true (если бэкенд проверяет acr)

Алгоритм:

- Копирует значение acr из аутентификационного контекста в токен
- Если в сессии нет явного acr — используется значение по умолчанию

Приложение может запросить конкретный acr, и keycloak можно настроить на изменение способа авторизации. В токен попадает итоговый реальный acr.

**Реальные сценарии**

Сценарий 1: Разные уровни доверия для разных операций

- Просмотр баланса → достаточно acr = "1" (пароль)
- Перевод до 1000$ → нужно acr = "2" (пароль + SMS)
- Перевод от 1000$ → нужно acr = "3" (пароль + OTP + биометрия)

Сценарий 2: Соответствие стандартам (eIDAS, NIST)

Правительственные системы требуют определенные уровни аутентификации:

- NIST AAL1 (Low): пароль
- NIST AAL2 (Moderate): пароль + OTP
- NIST AAL3 (High): аппаратный токен + биометрия

Сценарий 3: Корпоративная безопасность

- Доступ к обычным документам → acr = "password"
- Доступ к финансовым отчетам → acr = "password+mfa"
- Доступ к HR данным → acr = "hardware-key"

**Отличие ACR от AMR**

AMR (Authentication Methods Reference) — что использовали: пароль, OTP, WebAuthn.  
ACR (Authentication Context Class) — насколько надежно, агрегированный уровень.

</td></tr><tr style="height: 46.5938px;"><td style="height: 46.5938px;">Authentication Method Reference (AMR)</td><td style="height: 46.5938px;"><span class="">Claim в JWT токене, который содержит </span>**<span class="">массив строк</span>**<span class="">, каждая из которых обозначает конкретный метод аутентификации, использованный пользователем во время входа. </span>

RFC 8176 определяет стандартные значения AMR :

- pwd Пароль (Password)
- mfa Многофакторная аутентификация (Multi-Factor)
- otp Одноразовый пароль (One-Time Password)
- sms SMS-код
- tel Телефонный звонок
- geo Геолокация
- fpt Отпечаток пальца (Fingerprint)
- eye Сканирование сетчатки глаза
- face Распознавание лица
- hwk Аппаратный ключ (Hardware Key)
- pin PIN-код
- user Аутентификация пользователем (обычно через интерфейс ОС)
- kba Knowledge-based authentication
- mca Multi-channel authentication
- sc Смарт-карта (Smart Card)

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

**Настройка в консоли администратора**

Настройка аутентификаторов:

- Перейдите в Authentication → Flows
- Выберите ваш flow (например, Browser)
- Для каждого execution (например, Username/Password Form) нажмите Actions → Config
- В поле Authenticator Reference укажите значение (например, pwd)

Добавление маппера AMR:

- Создайте новый маппер в Client Scope
- Mapper Type: Authentication Method Reference (AMR)
- Name: amr-mapper (любое имя)
- Add to ID token: ON (рекомендуется)
- Add to access token: ON (если бэкенд проверяет AMR)

Маппер автоматически соберет все выполненные аутентификаторы и добавит их в поле amr

</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Claims parameter Token, Claims parameter with value ID Token</td><td style="height: 29.7969px;">Это НЕ маппер для добавления произвольных данных в токен из запроса. Это маппер для чтения OIDC Claims Parameter из запроса авторизации и добавления их в токен. встроенный в Keycloak маппер, который читает этот самый параметр claims из запроса авторизации и преобразует его в реальные claims в токене

```
https://keycloak.local/auth/realms/myrealm/protocol/openid-connect/auth?
    client_id=myapp&
    redirect_uri=https://myapp.com/callback&
    response_type=code&
    scope=openid&
    claims={"userinfo":{"given_name":{"essential":true},"email":null}}
```

Из остальных точек (headers, ...) данный элемент не считывается.

Сценарий применения нишевый. Пример: необходимо, чтобы в ID token обязательно были email и phone\_number, но только для этого конкретного запроса, а не для всех.

Без маппера: пришлось бы создавать отдельный client scope и настраивать мапперы.

С маппером: можно просто отправить правильный claims параметр.

В случае Claims parameter with value ID Token, добавляются именно в токен ID.

Параметр claims НЕ фильтрует claims, которые приходят из скоупов. Он добавляет дополнительные claims сверх того, что уже включено через scope.

</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Group Membership</td><td style="height: 29.7969px;">Берет группы, в которые входит пользователь в Keycloak, и записывает их названия в токен.

```json
{
  "groups": ["Engineering", "Managers"]
}
```

**Настройка**

Создание маппера происходит в разделе Client Scopes или непосредственно в настройках Client:  
Вариант 1: Через UI консоли администратора

 Clients → Client Scopes → выберите нужный scope (или создайте новый)

 Вкладка Mappers → Add mapper → By configuration

 Выберите Mapper Type → Group Membership

<table border="1" style="border-collapse: collapse; width: 100.158%;"><colgroup><col style="width: 22.7986%;"></col><col style="width: 55.75%;"></col><col style="width: 21.438%;"></col></colgroup><thead><tr><td>Параметр</td><td>Описание</td><td>Рекомендация</td></tr></thead><tbody><tr><td>Name</td><td>Имя маппера (для внутреннего использования)</td><td>groups-mapper</td></tr><tr><td>Token Claim Name</td><td>Как будет называться поле в токене</td><td>groups</td></tr><tr><td>Full group path</td><td>Включать ли полный путь группы (с родителями)</td><td>OFF для простых названий, ON если есть вложенные группы</td></tr><tr><td>Add to ID token</td><td>Добавлять ли claim в ID Token</td><td>ON (если фронтенду нужны группы)</td></tr><tr><td>Add to access token</td><td>Добавлять ли claim в Access Token</td><td>ON (если API проверяет группы)</td></tr><tr><td>Add to userinfo</td><td>Добавлять ли в ответ UserInfo endpoint</td><td>По желанию</td></tr></tbody></table>

**Best Practice:**

- Используйте группы для структурной организации (отдел, команда, локация)
- Используйте роли для прав доступа (can\_read, can\_write)
- Включайте groups в Access Token, если API проверяет принадлежность к отделам
- При большом количестве групп на пользователя рассмотрите фильтрацию или вынос в UserInfo

</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Map user group membership</td><td style="height: 29.7969px;">Жестко запрограммированное утверждение</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Hardcoded Role</td><td style="height: 29.7969px;">Жестко запрограммируйте роль в токене доступа.</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Nonce backwards compatible</td><td style="height: 29.7969px;">Добавляет утверждение nonce в токен Access, Refresh и ID</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Organization Membership</td><td style="height: 29.7969px;">Сопоставьте членство пользователя в организации</td></tr><tr style="height: 46.5938px;"><td style="height: 46.5938px;">Map user Organization membership</td><td style="height: 46.5938px;">Попарный идентификатор субъекта</td></tr><tr style="height: 63.3906px;"><td style="height: 63.3906px;">Pairwise subject identifier</td><td style="height: 63.3906px;">Вычисляет парный идентификатор субъекта, используя хэш-код sha-256, и добавляет его к заявке "sub". Смотрите спецификацию OpenID Connect для получения дополнительной информации о парных идентификаторах субъектов.</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Role Name Mapper</td><td style="height: 29.7969px;">Сопоставьте назначенную роль с новым именем или позицией в токене.</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Session State</td><td style="height: 29.7969px;">Добавьте запрос о состоянии сеанса (session\_state)</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Subject (sub)</td><td style="height: 29.7969px;">Добавьте запрос о предмете (под)</td></tr><tr style="height: 46.5938px;"><td style="height: 46.5938px;">User Address</td><td style="height: 46.5938px;">Сопоставляет атрибуты адреса пользователя (улица, населенный пункт, регион, почтовый индекс и страна) с утверждением OpenID Connect "адрес".</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">User Attribute</td><td style="height: 29.7969px;">Сопоставляет пользовательский атрибут пользователя с утверждением токена.</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">User Client Role</td><td style="height: 29.7969px;">Сопоставьте роль пользователя-клиента с заявкой на токен.

<span class="">Когда Multivalued выключен, возвращаются только назначенные роли; когда включен — все существующие.</span>

</td></tr><tr style="height: 46.5938px;"><td style="height: 46.5938px;">User Property</td><td style="height: 46.5938px;">Сопоставьте встроенное свойство пользователя (адрес электронной почты, имя, фамилию) с заявкой на токен.</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">User Realm Role</td><td style="height: 29.7969px;">Сопоставьте роль области пользователя с заявкой на токен.

<span class="">Когда Multivalued выключен, возвращаются только назначенные роли; когда включен — все существующие.</span>

</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">User Session Note</td><td style="height: 29.7969px;">Сопоставьте пользовательскую заметку о сеансе пользователя с заявкой на токен.</td></tr><tr style="height: 46.5938px;"><td style="height: 46.5938px;">User's full name</td><td style="height: 46.5938px;">Сопоставляет имя и фамилию пользователя с заявкой OpenID Connect "имя". Формат &lt;first&gt; + ' ' + &lt;last&gt;</td></tr></tbody></table>

# Users

Пользователи привязываются к определенной системе реалма.

# Roles (роли)

**Модели авторизации**

<table border="1" id="bkmrk-%D0%9C%D0%BE%D0%B4%D0%B5%D0%BB%D1%8C-%D0%9F%D1%80%D0%BE%D1%86%D0%B5%D1%81%D1%81-%2B-%2F--" style="border-collapse: collapse; width: 100%; height: 119.188px;"><colgroup><col style="width: 27.0635%;"></col><col style="width: 39.6826%;"></col><col style="width: 33.3731%;"></col></colgroup><thead><tr style="height: 29.7969px;"><td class="align-center" style="height: 29.7969px;">Модель</td><td class="align-center" style="height: 29.7969px;">Процесс</td><td class="align-center" style="height: 29.7969px;">+ / -</td></tr></thead><tbody><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Backend-driven</td><td style="height: 29.7969px;">JWT → roles → проверка в FastAPI</td><td style="height: 29.7969px;">+ простой  
+ быстрый  
- логика размазана по коду</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Token-driven</td><td style="height: 29.7969px;">JWT уже содержит всё → backend просто читает</td><td style="height: 29.7969px;">+ без внешних запросов  
- сложнее управлять динамикой</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Keycloak Authorization Services</td><td style="height: 29.7969px;">Backend → Keycloak → decision</td><td style="height: 29.7969px;">+ централизованная политика  
+ ABAC / RBAC / rules  
- сложность  
- задержка</td></tr></tbody></table>

Каждая роль может быть переключена в композитный режим (объединение других ролей). Но использовать аккуратно, можно запутаться.

Роли по умолчанию

<table border="1" id="bkmrk-%D0%9D%D0%B0%D0%B7%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D0%9A%D0%BE%D0%BC%D0%BF%D0%BE%D0%B7%D0%B8%D1%82%D0%BD%D0%B0%D1%8F" style="width: 100%;"><colgroup><col style="width: 10.9654%;"></col><col style="width: 12.9917%;"></col><col style="width: 13.704%;"></col><col style="width: 62.339%;"></col></colgroup><thead><tr><td>Название</td><td>Композитная</td><td>Только в master realm</td><td>Назначение</td></tr></thead><tbody><tr><td>admin</td><td>Да</td><td>Да</td><td>Административные права: управление пользователями, клиентами, ролями и конфигурацией реалма</td></tr><tr><td>create-realm</td><td>Нет</td><td>Да</td><td>Создание новых realms через Admin Console или REST API.</td></tr><tr><td>default-roles-master</td><td>Да</td><td>Нет</td><td>набор ролей, назначаемых по умолчанию всем новым пользователям master realm.   
Включает offline\_access, uma\_authorization и другие (можно посмотреть внутри composite).</td></tr><tr><td>offline-access</td><td>Нет</td><td>Нет</td><td>дает возможность получать offline refresh tokens - живут дольше, не требуют активной сессии пользователя.</td></tr><tr><td>uma\_authorization</td><td>Нет</td><td>Нет</td><td>позволяет использовать User-Managed Access (UMA) - механизм, при котором пользователь может делегировать доступ к своим ресурсам другим пользователям (используется при ресурсно-ориентированном доступе).</td></tr></tbody></table>

**Область видимости ролей**

В текущей версии с настройками ролей есть неявная особенность. Один из вариантов добавления scope через dedicated scopes. Перейдем в Clients -&gt; &lt;client name&gt; -&gt; Client scopes

[![image.png](http://bobrobotirk.ru/uploads/images/gallery/2026-05/scaled-1680-/Zjximage.png)](http://bobrobotirk.ru/uploads/images/gallery/2026-05/Zjximage.png)

Scope clientforsimpletest-dedicated называется так, поскольку клиент называется clientforsimpletest. Перейдем в этот scope, вкладку Scope.

[![image.png](http://bobrobotirk.ru/uploads/images/gallery/2026-05/scaled-1680-/nWBimage.png)](http://bobrobotirk.ru/uploads/images/gallery/2026-05/nWBimage.png)

И там есть переключатель Full scope allowed. Он по умолчанию включен, но если его отключить - появляется возможность настройки видимых ролей в рамках клиента. Это дополнительный фильтр. То есть, возможно одному клиенту отправить один набор ролей пользователя, другому клиенту - другой набор. Пример:

<div drawio-diagram="287"><img src="http://bobrobotirk.ru/uploads/images/drawio/2026-05/drawing-1-1779023372.png" alt=""/></div>

Это именно фильтр для конкретного клиента.

# Группы (groups)

Настройка групп в реалме.

[![image.png](http://bobrobotirk.ru/uploads/images/gallery/2026-04/scaled-1680-/NBPimage.png)](http://bobrobotirk.ru/uploads/images/gallery/2026-04/NBPimage.png)

Для каждой группы возможны следующие настройки:

[![image.png](http://bobrobotirk.ru/uploads/images/gallery/2026-04/scaled-1680-/naLimage.png)](http://bobrobotirk.ru/uploads/images/gallery/2026-04/naLimage.png)

<table border="1" id="bkmrk-child-groups-%D0%94%D0%BE%D1%87%D0%B5%D1%80%D0%BD%D0%B8" style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 20.6548%;"></col><col style="width: 79.4644%;"></col></colgroup><tbody><tr><td>Child groups</td><td>Дочерние группы. Дерево подчинения.</td></tr><tr><td>Members</td><td>Пользователи в группе. По умолчанию не показывает пользователей из дочерних групп. Для отображения пользователей дочерних групп нужно нажать кнопку Include sub-group users.

[![image.png](http://bobrobotirk.ru/uploads/images/gallery/2026-04/scaled-1680-/JrGimage.png)](http://bobrobotirk.ru/uploads/images/gallery/2026-04/JrGimage.png)

</td></tr><tr><td>Attributes</td><td>Дополнительные атрибуты группы.

Атрибуты наследуются и объединяются, но если имя атрибута совпадает — приоритет у самого глубокого (дочернего) элемента в иерархии.

Если же у дочерней группы атрибут имеет пустое значение, это НЕ удаляет родительский атрибут (в текущих версиях Keycloak пустое значение просто игнорируется).

[![image.png](http://bobrobotirk.ru/uploads/images/gallery/2026-04/scaled-1680-/NcVimage.png)](http://bobrobotirk.ru/uploads/images/gallery/2026-04/NcVimage.png)

Если пользователь в нескольких дочерних группах, то значения будут объединены в список. Значение атрибута в пользователе затрет все объединения.

Работает если в маппере атрибута (Client Scope → Mappers) включена опция "Multivalued" и "Aggregate attribute values".

Если эти опции не включены, поведение может отличаться:

 Без "Multivalued": будет взято только одно значение (какое именно — недетерминировано)

 Без "Aggregate": групповые атрибуты могут вообще не добавляться в токен

</td></tr><tr><td>Role mapping</td><td>Роли, добавляемые данной группой.</td></tr><tr><td>Admin Events</td><td>Административные события. Отображается список для группы, редактирование в разделе Realm settings - Events. </td></tr></tbody></table>

# Практика

# Общая задача

Задача: Необходимо организовать защиту приложения со следующими требованиями:

- Авторизация по логину и паролю
- Разделение пользователей на группы 
    - Администратор (все права)
    - Руководитель (просмотр статистики по всем направлениям)
    - Руководитель направления (права в пределах направления, нет возможности просмотра других направлений)
    - Руководитель предприятия (права в пределах предприятия, нет возможности просмотра других объектов)
- Разделение пользователей по ролям в соответствии с группами
- На backend есть API endpoint и HTML endpoint
- Неавторизованный пользователь имеет доступ к некоторым страницам без авторизации

Реализуем это при помощи декораторов из отдельного модуля. Желательно не хардкодить роли, должно быть внешнее хранилище.

# Базовая авторизация

Реализуем следующий процесс авторизации:

<div drawio-diagram="278"><img src="http://bobrobotirk.ru/uploads/images/drawio/2026-05/drawing-1-1778861417.png" alt=""/></div>

Делаем страницу с проверкой, авторизован или нет пользователь. Если пользователь не авторизован, показываем кнопку Авторизация. Если авторизован - кнопку Выход. Кнопки отличаются ссылками и текстом.

Будем использовать модуль python-keycloak

```bash
pip install python-keycloak
```

**Адресация серверов**

192.168.1.3 web server и ПК, с которого я тестирую работу

192.168.1.195 keycloak server

**Настройка keycloak.**

Создаем realm для данного эксперимента. Назовем его pythonsimpletest.

В разделе Manage realms - Create

[![image.png](http://bobrobotirk.ru/uploads/images/gallery/2026-05/scaled-1680-/image.png)](http://bobrobotirk.ru/uploads/images/gallery/2026-05/image.png)

Теперь создаем клиента. Назовем его clientforsimpletest. Clients - Create client

[![image.png](http://bobrobotirk.ru/uploads/images/gallery/2026-05/scaled-1680-/zaPimage.png)](http://bobrobotirk.ru/uploads/images/gallery/2026-05/zaPimage.png)

 Поскольку этот клиент доверенный и расположен на сервере, то Client authentication включаем,

[![image.png](http://bobrobotirk.ru/uploads/images/gallery/2026-05/scaled-1680-/w5kimage.png)](http://bobrobotirk.ru/uploads/images/gallery/2026-05/w5kimage.png)

[![image.png](http://bobrobotirk.ru/uploads/images/gallery/2026-05/scaled-1680-/4aNimage.png)](http://bobrobotirk.ru/uploads/images/gallery/2026-05/4aNimage.png)

И тут проявилась первая ошибка. Localhost виден с моего ПК. Поэтому, когда я прописал на сервере keycloak в разделе Root URL localhost - ничего не заработало. Похоже, что необходимо указывать имена/ip доступные с сервера keycloak а не только с браузера на ПК пользователя. Результирующие настройки клиента:

[![image.png](http://bobrobotirk.ru/uploads/images/gallery/2026-05/scaled-1680-/Gamimage.png)](http://bobrobotirk.ru/uploads/images/gallery/2026-05/Gamimage.png)

Создаем пользователя и задаем ему пароль.

**Python клиент**

```python
from fastapi import FastAPI, Depends, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from uvicorn import run
from typing import Optional

from keycloak import KeycloakOpenID

app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)

KEYCLOAK_URL = "http://192.168.1.195:9090/"
REALM_NAME = "pythonsimpletest"
CLIENT_ID = "clientforsimpletest"
CLIENT_SECRET = "Vix6txRyHwt81KyZIpl7O06CxpWMFtib"
REDIRECT_URI = "http://192.168.1.3:8100/auth/callback"

# Инициализация Keycloak клиента
keycloak_openid = KeycloakOpenID(
    server_url=KEYCLOAK_URL,
    client_id=CLIENT_ID,
    realm_name=REALM_NAME,
    client_secret_key=CLIENT_SECRET,
)

   
async def get_current_user_from_cookie(request: Request) -> Optional[dict]:
    """Извлечение и валидация пользователя из cookie"""
    
    # Получаем токен из cookies
    access_token = request.cookies.get("access_token")
    
    if not access_token:
        return None
    
    try:
        # Проверяем токен через Keycloak
        userinfo = keycloak_openid.userinfo(access_token)
        return userinfo
    except Exception as e:
        # Если токен просрочен или невалиден, удаляем его
        # Но здесь мы не можем удалить cookie, это нужно делать в ответе
        return None

def get_login_url():
    """Генерация URL для входа через Keycloak"""
    auth_url = keycloak_openid.auth_url(
        redirect_uri=REDIRECT_URI,
        scope="openid email profile",
        state="random_state_string"  # В реальном приложении используйте генерацию случайной строки
    )
    return auth_url

@app.get("/", response_class=HTMLResponse)
async def simpleauth(request: Request):
    user = await get_current_user_from_cookie(request)
    if not user:
        login_url = get_login_url()
        html_content = f'<html><body><a href="{login_url}"><button>Авторизация</button></a></body></html>'
    else:
        html_content = '<html><body><a href="/logout"><button>Выход</button></a></body></html>'
    return HTMLResponse(content=html_content, status_code=200)

@app.get("/auth/callback")
async def auth_callback(code: str):
    """Callback URL для обработки ответа от Keycloak после логина"""
    try:
        # Обмен кода на токены
        token_response = keycloak_openid.token(
            grant_type="authorization_code",
            code=code,
            redirect_uri=REDIRECT_URI
        )
        
        access_token = token_response.get('access_token')
        
        # Создаем редирект с токеном в заголовке (через установку cookie)
        response = RedirectResponse(url="/")
        
        # Устанавливаем токен в cookie (альтернатива Authorization header для браузера)
        response.set_cookie(
            key="access_token",
            value=access_token,
            httponly=True,  # Защита от XSS
            secure=False,   # True для HTTPS
            samesite="lax"
        )
        refresh_token = token_response.get('refresh_token')
        response.set_cookie(
            key="refresh_token",
            value=refresh_token,
            httponly=True,  # Защита от XSS
            secure=False,   # True для HTTPS
            samesite="lax"
        )
        return response
        
    except Exception as e:
        return HTMLResponse(content=f"<h1>Ошибка авторизации: {str(e)}</h1>", status_code=400)

@app.get("/logout")
async def logout(request: Request):
    """Выход из системы"""
    refresh_token = request.cookies.get("refresh_token")
    
    # Логаут через python-keycloak
    if refresh_token:
        try:
            keycloak_openid.logout(refresh_token)
        except Exception as e:
            print(f"Keycloak logout error: {e}")
    response = RedirectResponse(url="/")
    response.delete_cookie("access_token")
    response.delete_cookie("refresh_token")
    return response

if __name__ == '__main__':
    run(app="simple:app", host='0.0.0.0', port=8100, workers=4, log_level='warning')
```

</body></html>

# Добавление ролей

Теперь добавим scopes в токен. Даже просто добавление списка ролей оказалось той еще задачей. Во многих инструкциях будет сказано, что настройка маппера в Keycloak Admin Console -&gt; ваш realm → Clients → ваш клиент -&gt; Вкладка Mappers. Но это не так. Пример настройки для получения ролей:

- Перейди в Client Scopes: Твой realm → Clients → выбери своего клиента → вкладка Client scopes → кликни на дедикейтед скоп \*-dedicated (например, my-client-dedicated).
- Открой вкладку Mappers.
- Нажми Configure a new mapper и выбери By configuration.
- Выбери тип маппера: В выпадающем списке выбери User Realm Role.
- Вот это ключевой момент: использование предустановленного типа, а не создание своего, гарантирует корректную внутреннюю структуру.
- Проверь настройки (они должны заполниться автоматически):
- Name: Оставь realm roles (или любое другое).
- Token Claim Name: Убедись, что здесь realm\_access . Это единственное правильное имя поля.
- Add to userinfo: Убедись, что переключатель установлен в ON.
- Add to access token / Add to ID token: Можно оставить OFF, если роли не нужны в самих JWT токенах.
- Сохрани маппер (кнопка Save).

```python
async def get_current_user_roles(request: Request) -> Optional[dict]:
    """Извлечение ролей"""
    access_token = request.cookies.get("access_token")
    if not access_token:
        return []
    try:
        claims = jwt.get_unverified_claims(access_token)
        roles = claims.get("realm_access", {}).get("roles", [])
        return roles
    except Exception:
        return []
...
@app.get("/", response_class=HTMLResponse)
async def simpleauth(request: Request):
    user = await get_current_user_from_cookie(request)
    if not user:
        login_url = get_login_url()
        html_content = f'<html><body><a href="{login_url}"><button>Авторизация</button></a></body></html>'
    else:
        roles = await get_current_user_roles(request)
        html_content = f'''<html><body><a href="/logout"><button>Выход</button></a>
        <p>Доступные ключи: {user.keys()}</p>
        <p>Доступные роли: {roles}</p>
        </body></html>'''
    return HTMLResponse(content=html_content, status_code=200)

```

</body></html>

# Добавление декораторов

Почему-то для fastapi предпочтительнее использовать механизм Depends. Однако мне проще декораторы.

```python
from functools import wraps

from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.concurrency import run_in_threadpool
from uvicorn import run
from typing import Optional

from keycloak import KeycloakOpenID
from jose import jwt
from jose.exceptions import JWTError, ExpiredSignatureError

app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)

KEYCLOAK_URL = "http://192.168.1.195:9090/"
REALM_NAME = "pythonsimpletest"
CLIENT_ID = "clientforsimpletest"
CLIENT_SECRET = "Vix6txRyHwt81KyZIpl7O06CxpWMFtib"
REDIRECT_URI = "http://192.168.1.3:8100/auth/callback"

KEYCLOAK_PUBLIC_KEY = """-----BEGIN PUBLIC KEY----- 
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtrnsNk3z21imvfNFDT8teaekNgFhnOSuKcZiPc1Iv+qknDg8X1zF7KJLtZMrCZSLRJM6T754F8KtNUSj0Ioa3ehYSPEdF4SX7tBdacpeDz0GlKWYFa845/e6H2349I+7EuO1bkHfWz/v6n2mm+jeZKU0ujqr2boBoPWwkMoKwXNMl/Sac0YMlv1fVHiISyREDG+FordAwYlbLVYCD36ckk0UnAKBc59Q36DMiKSx7JpdvR0vHIeWb4mQFlRbLdmWkK4ebUe+B/k1/cdd3LHdQ6vc1i45bMcJlrFYocDOGK99Mf8pT8OMPQvTJ8cTe9xSkCAx0l7YiSv+5hfK1C6DswIDAQAB 
-----END PUBLIC KEY-----"""
ALGORITHMS = ["RS256"]

# Инициализация Keycloak клиента
keycloak_openid = KeycloakOpenID(
    server_url=KEYCLOAK_URL,
    client_id=CLIENT_ID,
    realm_name=REALM_NAME,
    client_secret_key=CLIENT_SECRET,
)

# ==============================Декораторы авторизации=========================
def verify_token(token: str):
    return jwt.decode(
        token,
        KEYCLOAK_PUBLIC_KEY,
        algorithms=ALGORITHMS,
        audience="account",
        options={
            "verify_signature": True,
            "verify_aud": False,
            "verify_exp": True,
        }
    )

def get_login_url():
    """Генерация URL для входа через Keycloak"""
    auth_url = keycloak_openid.auth_url(
        redirect_uri=REDIRECT_URI,
        scope="openid email profile",
        state="random_state_string"  # В реальном приложении используйте генерацию случайной строки
    )
    return auth_url

def session_required(func):
    @wraps(func)
    async def wrapper(*args, **kwargs):
        request: Request = kwargs.get("request")
        if request is None:
            for arg in args:
                if isinstance(arg, Request):
                    request = arg
                    break
        if request is None:
            raise RuntimeError("Request object not found")

        token = request.cookies.get("access_token")

        if not token:
            return RedirectResponse(get_login_url())

        try:
            claims = verify_token(token)
            request.state.user = claims
        except ExpiredSignatureError:
            response = RedirectResponse(get_login_url())
            response.delete_cookie("access_token")
            response.delete_cookie("refresh_token")
            return response
        except JWTError:
            response = RedirectResponse(get_login_url())
            response.delete_cookie("access_token")
            response.delete_cookie("refresh_token")
            return response
        return await func(*args, **kwargs)
    return wrapper

def html_norole():
    html_content = f'''<html><body><a href="/"><button>На главную</button></a>
    <p><a href="/logout"><button>Выход</button></a></p>
    <p>У вас нет доступа к этой странице</p>
    </body></html>'''
    return HTMLResponse(content=html_content, status_code=200)

def role_secondrole_required(func):
    @wraps(func)
    async def wrapper(*args, **kwargs):
        request: Request = kwargs.get("request")
        if request is None:
            for arg in args:
                if isinstance(arg, Request):
                    request = arg
                    break
        if request is None:
            raise RuntimeError("Request object not found")
        userinfo = request.state.user
        roles = userinfo.get("realm_access", {}).get("roles", [])
        if 'secondrole' not in roles:
            return html_norole()
        return await func(*args, **kwargs)
    return wrapper
# =============================================================================

@app.get("/", response_class=HTMLResponse)
@session_required
async def simpleauth(request: Request):
    userinfo = request.state.user #await get_current_user_from_cookie(request)
    roles = userinfo.get("realm_access", {}).get("roles", [])
    
    html_content = f'''<html><body><a href="/logout"><button>Выход</button></a>
    <p>Доступные ключи: {userinfo}</p>
    <p>Доступные роли: {roles}</p>
    <p><a href="/secret"><button>Доступ к секрету</button></a></p>
    </body></html>'''
    return HTMLResponse(content=html_content, status_code=200)

@app.get("/secret", response_class=HTMLResponse)
@session_required
@role_secondrole_required
async def roleneeded(request: Request):
    html_content = f'''<html><body><a href="/logout"><button>Выход</button></a>
    <p><a href="/"><button>На домашнюю страницу</button></a></p>
    </body></html>'''
    return HTMLResponse(content=html_content, status_code=200)

@app.get("/auth/callback")
async def auth_callback(code: str):
    """Callback URL для обработки ответа от Keycloak после логина"""
    def append_cookie(response, key, value):
        response.set_cookie(
            key=key,
            value=value,
            httponly=True,  # Защита от XSS
            secure=False,   # True для HTTPS
            samesite="lax"
        )
        return response
    try:
        # Обмен кода на токены
        token_response = keycloak_openid.token(
            grant_type="authorization_code",
            code=code,
            redirect_uri=REDIRECT_URI
        )
        # Создаем редирект с токеном в заголовке (через установку cookie)
        response = RedirectResponse(url="/")
        access_token = token_response.get('access_token')
        # Устанавливаем токен в cookie (альтернатива Authorization header для браузера)
        response = append_cookie(response, "access_token", access_token)
        refresh_token = token_response.get('refresh_token')
        response = append_cookie(response, "refresh_token", refresh_token)
        return response
        
    except Exception as e:
        return HTMLResponse(content=f"<h1>Ошибка авторизации: {str(e)}</h1>", status_code=400)

@app.get("/logout")
async def logout(request: Request):
    """Выход из системы"""
    refresh_token = request.cookies.get("refresh_token")
    if refresh_token:
        try:
            keycloak_openid.logout(refresh_token)
        except Exception as e:
            print(f"Keycloak logout error: {e}")
    response = RedirectResponse(url="/")
    response.delete_cookie("access_token")
    response.delete_cookie("refresh_token")
    return response

if __name__ == '__main__':
    run(app="02_withroles:app", host='0.0.0.0', port=8100, workers=4, log_level='warning')
```

</body></html>