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, предназначен для локальной разработки и тестирования.

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

Теория

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

can_do_it = F(user, data, action)

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

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

https://www.youtube.com/watch?v=uq2I9z_ZB6Q

image.png

Прослойка, объединяющая технологии авторизации.

Функциональность:

Блоки 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:

OAuth 2.0 и OpenID Connect (OIDC)

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

Роли:

OAuth:

OpenID Connect (OIDC)

Типы токенов

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

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

Realms

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

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

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

Создание realm

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

image.png

 Имя не должно содержать пробелов и иных символов, плохо воспринимаемых в url адресе.

Настройка realm (realm settings)

В меню Manage realms выбирается реалм, который мы будем редактировать. Затем в левом меню, раздел Configure содержит 4 блока.

OpenID Endpoint configuration - здесь можно увидеть все endpoint настроенные для данного realm. 

изображение.png

Настройка процесса аутентификации. Раздел 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).

SAML

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

Clients

Клиенты - сущности, которые могут отправлять запрос в Keycloak на аутентификацию пользователя. Чтобы приложение могло использовать ресурсы keycloak, оно должно быть зарегистрировано. Клиенты делятся по идентификаторам. 

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

image.png

Раздел 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

image.png

Определяет данные и разрешения, получаемые клиентом в access token / через ID token при аутентификации пользователя. Взаимосвязь между атрибутами и scopes: 

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

Типы scope:

Стандартные 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

image.png

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

image.png

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

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

Группы (groups)

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

image.png

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

image.png

Child groups Дочерние группы. Дерево подчинения.
Members

Пользователи в группе. По умолчанию не показывает пользователей из дочерних групп. Для отображения пользователей дочерних групп нужно нажать кнопку Include sub-group users.

image.png

Attributes

Дополнительные атрибуты группы. 

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

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

image.png

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

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

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

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

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

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

 

Практика

Практика

Общая задача

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

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

Практика

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

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

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

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

pip install python-keycloak

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

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

192.168.1.195 keycloak server

Настройка keycloak.

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

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

image.png

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

image.png

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

image.png

image.png

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

image.png

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

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. Но это не так. Пример настройки для получения ролей:

 

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