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 Технологии, посредством которых клиент получает токен. 

 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. 

 

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

 Создание 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-эндпоинту авторизационного сервера с обязательными параметрами:   

 

 grant_type -> Authorization Code 

 authorization_code -> code, полученный на шаге 6 

 redirect_uri -> тот же URI, что использовался при авторизации 

 client_id -> Идентификатор клиента 

 client_secret -> Секрет клиента 

 

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

 

 Веб-приложения (backend + frontend). 

 

 

 Authorization Code + PKCE 

 

 Модификация 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 

 

 Мобильные приложения, 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). Это требует: 

 

 Дополнительных запросов от вашего приложения к 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 разрешит отправлять авторизационный код или токены после успешной аутентификации.  

 https://myapp.example.com/callback

https://myapp.example.com/oauth/callback

http://localhost:3000/callback # для разработки

/* # любой путь в рамках корневого URL 

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

   

   

 

 

 

 Valid Post Logout Redirect URIs 

 Адреса, на которые Keycloak перенаправит пользователя после успешного выхода (logout).

 Отличие от Valid Redirect URIs: 

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

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

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

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

 

 

 

 Web Origins 

 

 Какие домены имеют право выполнять 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 как список разрешенных источников 

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

 

 

 

 

 

 

 

 Клиенты по умолчанию в 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 

 

 

 e-mail 

 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 токена. 

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

 

 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 появляется поле:  

 {

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

 ...

} 

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

 

 Декодирует токен. 

 Видит значение allowed-web-origins. 

 В middleware CORS сверяет Origin заголовок входящего запроса со списком. 

 Если совпадает — отдает данные. Нет — блокирует. 

 

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

 

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

 Использовать маппер, только если ваш бэкенд динамически решает, кому доверять, на основе данных из токена. 

 

 

 

 

 Audience, Hardcoded claim, Audience Resolve 

 Решает вопрос "для кого этот токен" (Resource Server). Это поле aud (audience) в JWT токене. Оно указывает, какой ресурс (API/сервис) должен принять этот токен.

 {

 "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. 

 

 

 

 Authentication Context Class Reference (ACR) 

 

 Механизм указывающий способ (уровень или метод) аутентификации пользователя, а не просто факт. Claim acr.  

 {

 "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) — насколько надежно, агрегированный уровень. 

 

 

 

 Authentication Method Reference (AMR) 

 

 Claim в JWT токене, который содержит массив строк , каждая из которых обозначает конкретный метод аутентификации, использованный пользователем во время входа.  

   

 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  

   

 

 

 

 Claims parameter Token, Claims parameter with value ID Token 

 

 Это НЕ маппер для добавления произвольных данных в токен из запроса. Это маппер для чтения 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. 

 

 

 

 Group Membership 

 

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

 {

 "groups": ["Engineering", "Managers"]

} 

 Настройка 

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

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

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

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

   

 

 

 

 Параметр 

 Описание 

 Рекомендация 

 

 

 

 

 Name 

 Имя маппера (для внутреннего использования) 

 groups-mapper 

 

 

 Token Claim Name 

 Как будет называться поле в токене 

 groups 

 

 

 Full group path 

 Включать ли полный путь группы (с родителями) 

 OFF для простых названий, ON если есть вложенные группы 

 

 

 Add to ID token 

 Добавлять ли claim в ID Token 

 ON (если фронтенду нужны группы) 

 

 

 Add to access token 

 Добавлять ли claim в Access Token 

 ON (если API проверяет группы) 

 

 

 Add to userinfo 

 Добавлять ли в ответ UserInfo endpoint 

 По желанию 

 

 

 

       

 

 Best Practice: 

 

 Используйте группы для структурной организации (отдел, команда, локация) 

 Используйте роли для прав доступа (can_read, can_write) 

 Включайте groups в Access Token, если API проверяет принадлежность к отделам 

 При большом количестве групп на пользователя рассмотрите фильтрацию или вынос в UserInfo 

 

   

 

 

 

 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') 

  