Keycloak
Установка
Docker:
Для 26 версии:
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
Для 22 версии:
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
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, происходит перенаправление
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 система размещена снаружи. Предположительная схема взаимодействия:
Приложение перенаправляет пользователя в Keycloak. Тот запрашивает логин и пароль, возвращает обратно код. Приложение обменивает код на токен и начинает с ним работать. С этого момента пользователь считается авторизованным — можно проверять его права, роли и доступы.
https://www.youtube.com/watch?v=uq2I9z_ZB6Q
Прослойка, объединяющая технологии авторизации.
Функциональность:
- Single-sign on, Single-sign out для браузерных приложений
- Поддержка OpenID/OAuth 2.0/SAML
- Identity Brokering - аутентификация с помощью внешних провайдеров, Social login
- User federation - синхронизация с LDAP / Active directory / Kerberos
- Консоль администратора
Блоки keycloak
Realm
Деление на блоки через realm. Каждый realm изолирован, пользователь принадлежит только одному realm. Содержит конфигурацию, набор приложений и пользователей. Есть административный и остальные realm.
Есть консоль управления реалм и консоль управления аккаунтом.
Клиенты (clients)
Клиенты - сущности, которые могут отправлять запрос в Keycloak на аутентификацию пользователя. Есть встроенные клиенты. Эти встроенные клиенты создаются автоматически для каждого realm.
Области видимости клиентов (client scopes)
Определяет данные и разрешения, получаемые клиентом в access token / ID token при аутентификации пользователя.
Роли и группы
Роли - атомарное право или набор прав, который назначается пользователю. Группы - иерархическая структура. Отличия между ролями и группами:
| Роли | Группы | |
| Назначение | права доступа | организация пользователей |
| Уровень | низкий (atomic) | высокий (aggregation) |
| Используются в коде | Да | Косвенно |
| Наследование | Нет | Да |
| Иерархия | Нет | Да |
Т е группы служат скорее для удобства управления пользователями в keycloak, фактическое разрешение действий происходит за счет ролей.
Самое важное: keycloak просто отдает сопоставление пользователь - роль. Группа - дополнительное удобство.
Ограничения на стороне приложения.
Могут быть настроены на уровне realm и client.
{
"realm_access": {
"roles": ["admin"]
},
"resource_access": {
"my-client": {
"roles": ["editor"]
}
}
}
Здесь admin - роль уровня realm, editor - роль уровня client. Роли используются для проверки доступа в коде. Но есть возможность перенести проверку разрешения выполнения endpoint на keycloak.
Консоль управления сервером
Доступ: ip_keycloak:8080/admin
Группы (организационная принадлежность) и роли (набор разрешений). Когда пользователь добавляется в группу, он наследует все роли. Единственная задача keycloak: Пользователь - Разрешение. Внутри роли сопоставляется разрешение. Группа упрощает управление пользователями.
Протоколы и технологии
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 с защищенными данными.
- Пользователь: владелец данных.
- Провайдер: сервер, выдающий токены и подтверждающий личность.
OAuth:
-
Протокол авторизации. Не занимается аутентификацией пользователя напрямую.
-
Главная цель: разрешить приложению доступ к ресурсам пользователя (например, к его файлам, фото) без передачи пароля.
-
Работает через выдачу токенов доступа (Access Token).
-
Пример: приложение запрашивает у пользователя разрешение читать его документы в Google Docs.
OpenID Connect (OIDC)
-
Надстройка над 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 Технологии, посредством которых клиент получает токен.
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.
Есть консоль управления реалм.
Создание realm
Настройка realm в момент создания - только имя
Имя не должно содержать пробелов и иных символов, плохо воспринимаемых в url адресе.
Настройка realm (realm settings)
В меню Manage realms выбирается реалм, который мы будем редактировать. Затем в левом меню, раздел Configure содержит 4 блока.
OpenID Endpoint configuration - здесь можно увидеть все endpoint настроенные для данного realm.
Настройка процесса аутентификации. Раздел Authentication используется для настройки и управления потоками аутентификации, обязательными действиями, и политиками входа.
Authentication flow (поток аутентификации) - это набор шагов, через которые проходит пользователь или клиент при входе, регистрации, сбросе пароля и т.д. Каждый поток состоит из действий (executions): например, проверка пароля, OTP, условные переходы, выбор IdP и др.
Потоки создаются и настраиваются.
Grant Type - способ, получения Access Token от сервера авторизации в OAuth 2.0 / OpenID Connect. Тип разрешения (grant) определяет, как и при каких условиях клиент получает токены. Keycloak, как реализация Identity Provider и Authorization Server, поддерживает разные grant types (, ).
| Название | Описание | Тип приложения |
| Authorization Code |
Стандартный безопасный flow для веб-приложений, где сервер клиента может хранить секреты. Клиент формирует POST-запрос к token-эндпоинту авторизационного сервера с обязательными параметрами:
безопасно для серверов с хранением Client Secret. Уязвимость на этапе редиректа. Подходит для серверных приложений |
Веб-приложения (backend + frontend). |
| Authorization Code + PKCE |
Модификация Authorization Code Flow для публичных клиентов (например, мобильных и SPA), чтобы предотвратить атаку перехвата кода. Клиент перед началом авторизации:
безопасно для мобильных и браузерных клиентов без секретов. Нужен только Code Verifier Защита на этапе редиректа Подходит для мобильных и SPA |
Мобильные приложения, SPA (Single Page Applications). |
| Client Credentials | Классический machine-to-machine сценарий, когда приложение запрашивает токен от собственного имени, без пользователя. | Сервисные взаимодействия (backend to backend), микросервисы. |
| Device Code | Для устройств без полноценной клавиатуры (например, Smart TV). Пользователь вводит код на другом устройстве. | Устройства с ограниченным вводом - SmartTV, консоли, IoT. |
| Refresh Token | После получения Access Token можно получить новый токен без повторной аутентификации пользователя. | Любые клиенты с долгоживущими сессиями. |
Authentication
Настройка потоков аутентификации, обязательных действий и политик входа.
Authentication flow (поток аутентификации) - это набор шагов, через которые проходит пользователь или клиент при входе, регистрации, сбросе пароля и т.д.
Каждый поток состоит из действий (executions): например, проверка пароля, OTP, условные переходы, выбор IdP и др.
| Название | Описание |
| browser | Встроенный поток для авторизации на основе браузера. Включает формы логина, OTP (двухфакторную аутентификацию), CAPTCHA и другие шаги. |
| clients | Встроенный поток аутентификации для клиентов (используется в client credentials grant). Работает без участия пользователя. |
| direct grant | Встроенный поток для ресурсного владельца (Resource Owner Password Credentials Grant). Используется, когда пользователь напрямую вводит логин и пароль (например, через curl или мобильное приложение). |
| docker auth | Поток аутентификации Docker-клиентов при доступе к приватному Docker Registry через Keycloak. |
| First broker login | Поток, срабатывающий при первом входе пользователя через внешний Identity Provider (например, Google, GitHub). Позволяет привязать внешний аккаунт к локальному пользователю Keycloak. |
| Registration | Поток регистрации новых пользователей. Включает форму ввода данных, подтверждение по email и проверки по политике безопасности (например, сила пароля). |
| Reset credentials | Поток для восстановления доступа, если пользователь забыл пароль. Может включать подтверждение по email, OTP и другие шаги для подтверждения личности. |
У каждого realm свой открытый ключ. С его помощью можно проверять подлинность полученных токенов. После авторизации под любым пользователем выполнить один раз
KEYCLOAK_PUBLIC_KEY = f"""
-----BEGIN PUBLIC KEY-----
{keycloak_openid.public_key()}
-----END PUBLIC KEY-----
"""
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 на аутентификацию пользователя. Чтобы приложение могло использовать ресурсы keycloak, оно должно быть зарегистрировано. Клиенты делятся по идентификаторам.
Настройка через консоль управления реалмом, раздел клиенты. Желательно настраивать на каждый сервер / сервис отдельный клиент.
Раздел Clients
Client list
Client list - список клиентов. Параметры настройки клиента:
| Параметр | Описание |
| Client type | Алгоритм аутентификации клиента. OpenID или SAML. |
| Client ID | Идентификатор клиента. Он будет использоваться в запросах, поэтому желательно с маленькой буквы, без пробелов и прочего. |
| Client authentication |
Сможет ли приложение хранить секрет и использовать его для аутентификации на Keycloak. ON — Конфиденциальный клиент Приложение обязано хранить секрет. При обмене авторизационного кода на токены клиент предъявляет код и Client Secret. Используется для бэкенда. Пример: FastAPI приложение на сервере, обращающееся к Keycloak из защищенной среды. OFF — Публичный клиент У клиента нет секрета, при обмене кода на токены секрет не используется. Используется для одностраничных приложений, мобильных приложений. |
| Authorization |
Активация 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). Это требует:
|
| Root URL | Базовый адрес вашего приложения. Используется как префикс для других URL, если они указаны относительно. Может быть пустым, но лучше указать. Некоторые функции Keycloak (например, ссылка "Back to application" на странице логина) используют этот URL. |
| Home URL | Куда пользователь попадет после нажатия на логотип Keycloak или ссылку "Back to application".
Отличие от Root URL: Root URL — это база/префикс. Home URL — конкретная страница, которая считается "домашней" для приложения. Если не указан, используется Root URL. |
| Valid Redirect URIs |
Адреса, на которые Keycloak разрешит отправлять авторизационный код или токены после успешной аутентификации.
Для безопасности критично. Как минимум указывать относительный. Просто * убивает всю авторизацию.
|
| Valid Post Logout Redirect URIs | Адреса, на которые Keycloak перенаправит пользователя после успешного выхода (logout).
Отличие от Valid Redirect URIs: Обычные редиректы — после входа (авторизации) Post Logout — после выхода Зачем нужно: Совет: Обычно указывают страницу вроде /goodbye, /logged-out или сам Root URL. |
| Web Origins |
Какие домены имеют право выполнять CORS-запросы к вашему Keycloak из браузера (для SPA-приложений). Используется для: CORS (Cross-Origin Resource Sharing) на эндпоинтах Keycloak (/token, /logout, /userinfo и т.д.). Пример для SPA на React: Если Web Origins указан правильно, браузер позволит SPA из myapp.com делать fetch-запросы к Keycloak. + Плюс означает использовать Valid Redirect URIs как список разрешенных источников * Любой источник (все домены) |
Клиенты по умолчанию в Master realm:
| Название | Описание | Адрес |
| account | Клиент пользовательского личного кабинета. Управление профилем, просмотр сеансов входа и разрешения. | /realms/{realm}/account |
| account-console | Фронтенд клиента account, вызывающий API account. |
|
| admin-cli | Клиент взаимодействия с Keycloak через CLI или REST API. Например получение токена через kcadm.sh или curl для скриптов/CI/CD. Поддерживает client_credentials и password гранты. |
|
| broker |
Внутренний клиент для федерации идентичности (SSO между провайдерами). Пример: Keycloak как Identity Broker между Google, GitHub и другим Keycloak сервером. |
|
| master-realm | Формальный клиент, представляющий сам master realm. Часто используется для определённых внутренних механизмов и ссылок в Admin Console. |
|
| security-admin-console | Клиент для административной консоли Keycloak. Используется при входе в админ-панель, управлении реалмами, пользователями и клиентами. | /admin/{realm}/console |
Пример работы с admin-cli:
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()
Ответ сервера:
{
'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 <access_token> то будет доступ. Полный пример запроса:
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
Определяет данные и разрешения, получаемые клиентом в access token / через ID token при аутентификации пользователя. Взаимосвязь между атрибутами и scopes:
Маппер (Mapper) внутри Client Scope — это инструкция для Keycloak: «Возьми данные ОТСЮДА (атрибут, роль, контекст) и положи ИХ В ТОКЕН ПОД ТАКИМ ИМЕНЕМ».
Типы scope:
- Default: всегда включается в токен по умолчанию, даже если клиент не запрашивает его явно.
- Optional: будет включен в токен в случае явного запроса клиентом в параметре scope=... при авторизации.
- (Unassigned): не применяется клиенту вообще, пока его не назначат вручную или не укажут явно в токене.
Стандартные scopes:
| Название | Тип | Протокол | Назначение |
| acr | Default | OpenID Connect | добавляет в токен acr (Authentication Context Class Reference) - уровень аутентификации (например, MFA, пароль и т. д.). |
| address | Optional | OpenID Connect | добавляет адрес пользователя (address claim) в токен (если заполнено в профиле). |
| basic | Default | OpenID Connect | включает базовые claims - sub, name, preferred_username, given_name, family_name |
| Default | OpenID Connect |
добавляет email и email_verified в ID и Access токены |
|
| microprofile-jwt | Optional | OpenID Connect |
обеспечивает совместимость с Eclipse MicroProfile JWT - популярным стандартом для Java microservices |
| offline_access | Optional | OpenID Connect |
разрешает выдачу offline refresh token (живёт долго и используется без пользовательского взаимодействия). |
| role_list | Default | SAML |
включает список ролей (roles) пользователя в SAML assertions |
| roles | Default | OpenID Connect |
добавляет роли пользователя (realm roles и client roles) в access token (в realm_access.roles и resource_access) |
Также есть phone, profile
Добавление маппера:
Типы мапперов:
| Имя | Описание | |||||||||||||||||||||
| Allowed Web Origins |
Scope с данным mapper добавляет все разрешенные веб-источники к свойству "allowed-origins" в токене. Иначе это механизм копирования списка доверенных источников браузера из конфигурации Keycloak Client внутрь Access Token, чтобы бэкенд мог программно принять решение о разрешении CORS. При генерации токена он читает список разрешенных веб-источников, настроенный у Client (родителя этого Client Scope), и копирует этот список в указанный claim токена. Поля конфигурации маппера:
Пример: SPA (на http://localhost:3000) стучится на бэкенд (http://localhost:8080). Бэкенд получает токен, но как ему узнать, что JavaScript вашего фронта имеет право вызывать его API? Решение через маппер Allowed Web Origins:
Результат: Внутри Access Token появляется поле:
Теперь бэкенд:
Используется редко.
|
|||||||||||||||||||||
| Audience, Hardcoded claim, Audience Resolve | Решает вопрос "для кого этот токен" (Resource Server). Это поле aud (audience) в JWT токене. Оно указывает, какой ресурс (API/сервис) должен принять этот токен.
Зачем это нужно? Например, есть 3 микросервиса:
Если токен не имеет audience, любой сервис может принять токен, выданный для другого. Злоумышленник получает токен для payment-api, но пытается вызвать order-api. Без проверки aud — успешно. Решение: Keycloak добавляет в токен aud = "payment-api". order-api проверяет этот claim и отвергает токен (403 Forbidden).
Типы мапперов для Audience Audience (простой) Создаете маппер в Client Scope, указываете жестко имя аудитории. Конфигурация:
Результат: В access token появится "aud": ["payment-api"] Вручную claim aud с любым значением (массивом или строкой). Конфигурация:
Audience resolve (продвинутый) Автоматически собирает всех клиентов, которым был выдан токен, и добавляет их в aud. Например, если клиент spa-app запрашивает токен с scope, включающим audience-resolve маппер, в aud попадут все клиенты, на которые у пользователя есть доступ через service accounts. Реальный сценарий использования Сценарий: SPA + два микросервиса Настройка в Keycloak:
Процесс: // В токене: // SPA вызывает payment-api ✅ разрешено Сценарий: Токен для нескольких API Иногда нужно, чтобы один токен работал с несколькими сервисами: { Как сделать: Создаете маппер Hardcoded claim с массивом значений или используете несколько мапперов Audience. |
|||||||||||||||||||||
| Authentication Context Class Reference (ACR) |
Механизм указывающий способ (уровень или метод) аутентификации пользователя, а не просто факт. Claim acr.
Конфигурация маппера:
Алгоритм:
Приложение может запросить конкретный acr, и keycloak можно настроить на изменение способа авторизации. В токен попадает итоговый реальный acr. Реальные сценарии Сценарий 1: Разные уровни доверия для разных операций
Сценарий 2: Соответствие стандартам (eIDAS, NIST) Правительственные системы требуют определенные уровни аутентификации:
Сценарий 3: Корпоративная безопасность
Отличие ACR от AMR AMR (Authentication Methods Reference) — что использовали: пароль, OTP, WebAuthn. |
|||||||||||||||||||||
| Authentication Method Reference (AMR) |
Claim в JWT токене, который содержит массив строк, каждая из которых обозначает конкретный метод аутентификации, использованный пользователем во время входа.
RFC 8176 определяет стандартные значения AMR :
Keycloak позволяет использовать любые кастомные значения, не ограничиваясь стандартным списком.
Настройка в консоли администратора Настройка аутентификаторов:
Добавление маппера AMR:
Маппер автоматически соберет все выполненные аутентификаторы и добавит их в поле amr
|
|||||||||||||||||||||
| Claims parameter Token, Claims parameter with value ID Token |
Это НЕ маппер для добавления произвольных данных в токен из запроса. Это маппер для чтения OIDC Claims Parameter из запроса авторизации и добавления их в токен. встроенный в Keycloak маппер, который читает этот самый параметр claims из запроса авторизации и преобразует его в реальные claims в токене
Из остальных точек (headers, ...) данный элемент не считывается. Сценарий применения нишевый. Пример: необходимо, чтобы в ID token обязательно были email и phone_number, но только для этого конкретного запроса, а не для всех. Без маппера: пришлось бы создавать отдельный client scope и настраивать мапперы. С маппером: можно просто отправить правильный claims параметр.
В случае Claims parameter with value ID Token, добавляются именно в токен ID.
Параметр claims НЕ фильтрует claims, которые приходят из скоупов. Он добавляет дополнительные claims сверх того, что уже включено через scope. |
|||||||||||||||||||||
| Group Membership |
Берет группы, в которые входит пользователь в Keycloak, и записывает их названия в токен.
Настройка Создание маппера происходит в разделе Client Scopes или непосредственно в настройках Client: Clients → Client Scopes → выберите нужный scope (или создайте новый) Вкладка Mappers → Add mapper → By configuration Выберите Mapper Type → Group Membership
Best Practice:
|
|||||||||||||||||||||
| Map user group membership | Жестко запрограммированное утверждение | |||||||||||||||||||||
| Hardcoded Role | Жестко запрограммируйте роль в токене доступа. | |||||||||||||||||||||
| Nonce backwards compatible | Добавляет утверждение nonce в токен Access, Refresh и ID | |||||||||||||||||||||
| Organization Membership | Сопоставьте членство пользователя в организации | |||||||||||||||||||||
| Map user Organization membership | Попарный идентификатор субъекта | |||||||||||||||||||||
| Pairwise subject identifier | Вычисляет парный идентификатор субъекта, используя хэш-код sha-256, и добавляет его к заявке "sub". Смотрите спецификацию OpenID Connect для получения дополнительной информации о парных идентификаторах субъектов. | |||||||||||||||||||||
| Role Name Mapper | Сопоставьте назначенную роль с новым именем или позицией в токене. | |||||||||||||||||||||
| Session State | Добавьте запрос о состоянии сеанса (session_state) | |||||||||||||||||||||
| Subject (sub) | Добавьте запрос о предмете (под) | |||||||||||||||||||||
| User Address | Сопоставляет атрибуты адреса пользователя (улица, населенный пункт, регион, почтовый индекс и страна) с утверждением OpenID Connect "адрес". | |||||||||||||||||||||
| User Attribute | Сопоставляет пользовательский атрибут пользователя с утверждением токена. | |||||||||||||||||||||
| User Client Role |
Сопоставьте роль пользователя-клиента с заявкой на токен.
Когда Multivalued выключен, возвращаются только назначенные роли; когда включен — все существующие. |
|||||||||||||||||||||
| User Property | Сопоставьте встроенное свойство пользователя (адрес электронной почты, имя, фамилию) с заявкой на токен. | |||||||||||||||||||||
| User Realm Role |
Сопоставьте роль области пользователя с заявкой на токен.
Когда Multivalued выключен, возвращаются только назначенные роли; когда включен — все существующие. |
|||||||||||||||||||||
| User Session Note | Сопоставьте пользовательскую заметку о сеансе пользователя с заявкой на токен. | |||||||||||||||||||||
| User's full name | Сопоставляет имя и фамилию пользователя с заявкой OpenID Connect "имя". Формат <first> + ' ' + <last> |
Users
Пользователи привязываются к определенной системе реалма.
Roles (роли)
Модели авторизации
| Модель | Процесс | + / - |
| Backend-driven | JWT → roles → проверка в FastAPI | + простой + быстрый - логика размазана по коду |
| Token-driven | JWT уже содержит всё → backend просто читает | + без внешних запросов - сложнее управлять динамикой |
| Keycloak Authorization Services | Backend → Keycloak → decision | + централизованная политика + ABAC / RBAC / rules - сложность - задержка |
Каждая роль может быть переключена в композитный режим (объединение других ролей). Но использовать аккуратно, можно запутаться.
Роли по умолчанию
| Название | Композитная | Только в master realm | Назначение |
| admin | Да | Да | Административные права: управление пользователями, клиентами, ролями и конфигурацией реалма |
| create-realm | Нет | Да | Создание новых realms через Admin Console или REST API. |
| default-roles-master | Да | Нет | набор ролей, назначаемых по умолчанию всем новым пользователям master realm. Включает offline_access, uma_authorization и другие (можно посмотреть внутри composite). |
| offline-access | Нет | Нет | дает возможность получать offline refresh tokens - живут дольше, не требуют активной сессии пользователя. |
| uma_authorization | Нет | Нет | позволяет использовать User-Managed Access (UMA) - механизм, при котором пользователь может делегировать доступ к своим ресурсам другим пользователям (используется при ресурсно-ориентированном доступе). |
Область видимости ролей
В текущей версии с настройками ролей есть неявная особенность. Один из вариантов добавления scope через dedicated scopes. Перейдем в Clients -> <client name> -> Client scopes
Scope clientforsimpletest-dedicated называется так, поскольку клиент называется clientforsimpletest. Перейдем в этот scope, вкладку Scope.
И там есть переключатель Full scope allowed. Он по умолчанию включен, но если его отключить - появляется возможность настройки видимых ролей в рамках клиента. Это дополнительный фильтр. То есть, возможно одному клиенту отправить один набор ролей пользователя, другому клиенту - другой набор. Пример:
Это именно фильтр для конкретного клиента.
Группы (groups)
Настройка групп в реалме.
Для каждой группы возможны следующие настройки:
| Child groups | Дочерние группы. Дерево подчинения. |
| Members |
Пользователи в группе. По умолчанию не показывает пользователей из дочерних групп. Для отображения пользователей дочерних групп нужно нажать кнопку Include sub-group users. |
| Attributes |
Дополнительные атрибуты группы. Атрибуты наследуются и объединяются, но если имя атрибута совпадает — приоритет у самого глубокого (дочернего) элемента в иерархии. Если же у дочерней группы атрибут имеет пустое значение, это НЕ удаляет родительский атрибут (в текущих версиях Keycloak пустое значение просто игнорируется). Если пользователь в нескольких дочерних группах, то значения будут объединены в список. Значение атрибута в пользователе затрет все объединения. Работает если в маппере атрибута (Client Scope → Mappers) включена опция "Multivalued" и "Aggregate attribute values". Если эти опции не включены, поведение может отличаться: Без "Multivalued": будет взято только одно значение (какое именно — недетерминировано) Без "Aggregate": групповые атрибуты могут вообще не добавляться в токен |
| Role mapping | Роли, добавляемые данной группой. |
| Admin Events | Административные события. Отображается список для группы, редактирование в разделе Realm settings - Events. |
Практика
Общая задача
Задача: Необходимо организовать защиту приложения со следующими требованиями:
- Авторизация по логину и паролю
- Разделение пользователей на группы
- Администратор (все права)
- Руководитель (просмотр статистики по всем направлениям)
- Руководитель направления (права в пределах направления, нет возможности просмотра других направлений)
- Руководитель предприятия (права в пределах предприятия, нет возможности просмотра других объектов)
- Разделение пользователей по ролям в соответствии с группами
- На backend есть API endpoint и HTML endpoint
- Неавторизованный пользователь имеет доступ к некоторым страницам без авторизации
Реализуем это при помощи декораторов из отдельного модуля. Желательно не хардкодить роли, должно быть внешнее хранилище.
Базовая авторизация
Реализуем следующий процесс авторизации:
Делаем страницу с проверкой, авторизован или нет пользователь. Если пользователь не авторизован, показываем кнопку Авторизация. Если авторизован - кнопку Выход. Кнопки отличаются ссылками и текстом.
Будем использовать модуль python-keycloak
pip install python-keycloak
Адресация серверов
192.168.1.3 web server и ПК, с которого я тестирую работу
192.168.1.195 keycloak server
Настройка keycloak.
Создаем realm для данного эксперимента. Назовем его pythonsimpletest.
В разделе Manage realms - Create
Теперь создаем клиента. Назовем его clientforsimpletest. Clients - Create client
Поскольку этот клиент доверенный и расположен на сервере, то Client authentication включаем,
И тут проявилась первая ошибка. Localhost виден с моего ПК. Поэтому, когда я прописал на сервере keycloak в разделе Root URL localhost - ничего не заработало. Похоже, что необходимо указывать имена/ip доступные с сервера keycloak а не только с браузера на ПК пользователя. Результирующие настройки клиента:
Создаем пользователя и задаем ему пароль.
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'<!DOCTYPE html><html><body><a href="{login_url}"><button>Авторизация</button></a></body></html>'
else:
html_content = '<!DOCTYPE html><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')
Добавление ролей
Теперь добавим scopes в токен. Даже просто добавление списка ролей оказалось той еще задачей. Во многих инструкциях будет сказано, что настройка маппера в Keycloak Admin Console -> ваш realm → Clients → ваш клиент -> Вкладка 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).
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'<!DOCTYPE html><html><body><a href="{login_url}"><button>Авторизация</button></a></body></html>'
else:
roles = await get_current_user_roles(request)
html_content = f'''<!DOCTYPE html><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)
Добавление декораторов
Почему-то для fastapi предпочтительнее использовать механизм Depends. Однако мне проще декораторы.
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'''<!DOCTYPE html><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'''<!DOCTYPE html><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'''<!DOCTYPE html><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')