Авторизация внешних пользователей и систем через Keycloak
Начиная с версии 2.20.0 TDG поддерживает авторизацию внешних пользователей и систем через внешние провайдеры аутентификации (auth providers) OpenID Connect (OIDC), в частности Keycloak. До версии 2.20.0 TDG проверял только те учётные данные, которыми владел сам: логин и пароль, собственные токены приложений и LDAP. В организации, где доступ управляется централизованно в Keycloak, учётные записи TDG приходилось создавать и отзывать отдельно. Начиная с версии 2.20.0 клиент может получить токен из Keycloak и передать его в TDG. TDG самостоятельно проверяет подпись токена и сопоставляет группы пользователя со своими ролями. Пользователь при этом не обязан существовать в TDG.
Аутентифицированный пользователь получает временную учётную запись,
которая существует только в течение времени обработки запроса. Эта
учётная запись не требует предварительного создания и не хранится в
списке пользователей TDG. Идентификатор учётной записи имеет вид
keycloak:<имя провайдера>:<claim sub>. Отображаемое имя содержит поле (claim)
preferred_username, email или sub, а источник — Keycloak.
Токен передаётся в стандартном заголовке:
Authorization: Bearer <JWT>
Внешние провайдеры опрашиваются первыми — до собственных токенов TDG и
cookie-сессий. Чтобы не нарушить существующие интеграции, провайдер
Keycloak реагирует только на то, что выглядит как JWT, — значение после
Bearer , которое состоит из трёх непустых частей, разделённых точками.
Всё остальное провайдер передаёт дальше по цепочке, поэтому токены
приложений TDG не затрагиваются. Токен, который выглядит как JWT, но не
проходит проверку, отклоняется с кодом 401 и телом
Invalid Keycloak token. В этом случае резервного пути нет. Отказы
сопровождаются заголовком ответа
WWW-Authenticate: Bearer error="invalid_token".
Встроенный верификатор поддерживает входящие JWT-токены, подписанные
асимметричным ключом. Его подключения для discovery и JWKS поддерживают
режимы proxy, proxy_port, proxy_user_pwd и no_proxy.
Интерактивный вход в браузере с перенаправлением в Keycloak (Authorization
Code Flow) не встроен. Расширение может реализовать интроспекцию по
RFC 7662, в том числе для непрозрачных токенов доступа, как описано ниже
в разделе Точки расширения.
HTTP-вывод может получить токен доступа OAuth 2.0 с помощью Client
Credentials и отправить его как Authorization: Bearer. Это не зависит
от входа через Keycloak и поддерживает непрозрачные токены доступа.
Ответ адреса обработки запросов токена обычно содержит положительное
число expires_in, которое TDG использует для кэширования и обновления
токена. Начиная с версии 2.20.0 для такого адреса обработки запросов
можно задать положительное значение параметра token_ttl в секции
auth как явное резервное время жизни. В подключении можно задать TLS-,
proxy- и транспортные опции встроенного верификатора, в том числе
proxy, proxy_port, proxy_user_pwd и no_proxy. Опции применяются к
запросу токена независимо от запроса вывода.
Параметр auth.type принимает только значение
oauth2_client_credentials. Для Keycloak адрес обработки запросов токена
выглядит как строка вида <issuer>/protocol/openid-connect/token,
например https://keycloak.example.com/realms/tdg/protocol/openid-connect/token
(в Keycloak до версии 17 перед /realms добавляется /auth). Оба
способа аутентификации клиента работают с конфиденциальным клиентом, у
которого включены сервисные аккаунты.
По умолчанию клиент аутентифицируется на адресе обработки запросов токена
по протоколу HTTP Basic (client_secret_basic). Если адрес обработки
запросов ожидает учётные данные в теле запроса, задайте для параметра
token_endpoint_auth_method значение client_secret_post. Такой запрос
не содержит заголовка Authorization и передаёт client_id и
client_secret как поля формы.
Токен кэшируется для каждого вывода и обновляется до истечения срока
действия, параллельные запросы совместно используют одно обновление.
Непредвиденная ошибка во время обновления токена завершает его как
недоступный адрес обработки запросов и никогда не оставляет ожидающих в
заблокированном состоянии. Применение конфигурации никогда не обращается
к адресу обработки запросов токена. Ответ 401 от сервера ресурсов
определяет недействительными только ту генерацию токена, которую
использовал этот запрос, принудительные обновления ограничены по частоте.
TDG никогда не повторяет запрос вывода автоматически.
connector:output:- name: external-apitype: httpurl: https://api.example.com/eventsformat: jsonauth:type: oauth2_client_credentialstoken_url: https://sso.example.com/tokenclient_id: tdg-outputclient_secret: secretscopes: [events.write]
Полный список параметров конфигурации приведен в справочнике по настройке output-коннектора HTTP.
Настроить авторизацию через Keycloak можно, указав
внешних провайдеров аутентификации в секции
auth_providers файла конфигурации config.yml.
В секции auth_providers можно задать несколько провайдеров keycloak —
по одному на каждый уникальный идентификатор издателя токена issuer.
У каждого провайдера задаются уникальное имя (name), тип провайдера
(type) и настройки провайдера (options).
Если провайдеров несколько, для каждого провайдера также должен быть
задан свой уникальный непустой issuer. TDG читает непроверенное поле iss только для
выбора провайдера. Доступ не предоставляется, пока выбранный провайдер не
проверит подпись и все настроенные поля, поэтому JWT для другой
настроенной области (realm) проходит мимо предыдущего провайдера. Токен
с совпадающим issuer, не прошедший проверку подписи, отклоняется и не
может быть принят последующим провайдером. Два провайдера с совпадающим
идентификатором токена отклоняются как неоднозначные. Унаследованный
пользовательский верификатор без идентификатора поддерживается, если он
единственный провайдер Keycloak.
auth_providers:- name: keycloak-internaltype: keycloakoptions:issuer: https://keycloak.example.com/realms/internalaudience: [tdg-api]roles: [{role: admin, groups: [/tdg/admin]}]- name: keycloak-partnerstype: keycloakoptions:issuer: https://keycloak.example.com/realms/partnersaudience: [tdg-partner-api]roles: [{role: user, groups: [/tdg/users]}]
Имена TLS и транспортных опций совпадают с теми, что уже использует output-коннектор HTTP, TDG передаёт их HTTP-клиенту без изменений.
Полный список параметров конфигурации auth_providers и опций
встроенного верификатора oidc_jwks приведены в
справочнике по настройке auth_providers.
Провайдерами можно управлять без ручного редактирования конфигурации
кластера. В GraphQL-схеме admin, в разделе account_provider:
auth_provider_configвозвращает настроенных провайдеров и возможности (capabilities), публикуемые каждым доступным верификатором;auth_provider_add(name, type, options),auth_provider_update(name, newName, type, options)иauth_provider_delete(name)изменяют провайдеры. Параметрoptionsпередаётся как строка YAML.
В веб-интерфейсе есть страница настроек Keycloak. Её поля соответствуют
возможностям (capabilities), которые публикует выбранный верификатор:
при смене верификатора неподдерживаемые им опции скрываются, а
поддерживаемые показываются. Значения audience, authorized_parties,
типов токенов и алгоритмов вводятся по одному значению на строку.
Встроенный верификатор также показывает настройки прокси (включая
секретное поле учётных данных) и транспортные настройки за кнопкой
«Advanced HTTP options», которая открывается автоматически, когда одна из
этих опций имеет значение, отличное от значения по умолчанию. Если
очистить поле типов токенов, сохраняется пустой список — токен
принимается независимо от значения поля typ; поле, оставленное со
значением верификатора по умолчанию, не записывается в конфигурацию,
поэтому открытие и сохранение провайдера его не меняет. Отдельное поле
пользовательских опций предназначено для верификаторов, зарегистрированных
во время выполнения и объявляющих имена опций, для которых в форме нет
полей; для встроенного oidc_jwks оно не предлагается, поскольку его
опции форма покрывает полностью. Опция, которую не объявляет ни провайдер,
ни выбранный верификатор, отклоняется при применении конфигурации. Пока
страница открыта, столбец рабочего состояния обновляется каждые 30 секунд;
это состояние текущего экземпляра TDG, а не агрегированное состояние
кластера.
Узнать больше: Управление настройками через GraphQL API.
Токен принимается, когда его поле aud пересекается с audience.
Конфигурация клиента в Keycloak определяет, что попадет в поле aud.
Чтобы поле содержало идентификатор TDG, администратор области добавляет
клиенту маппер протокола Audience. Без него aud содержит значение
области по умолчанию — такое есть у каждого токена этой области и оно
ничего не ограничивает.
Клиентов также можно ограничить по полю azp. Keycloak указывает в этом
поле идентификатор клиента, которому выдан токен, и клиент не влияет на
это значение. Опция max_token_age дополнительно ограничивает возраст
принятого токена, когда время жизни токена нельзя изменить в Keycloak.
options:issuer: https://keycloak.example.com/realms/tdgaudience: [account]authorized_parties:- tdg-connectormax_token_age: 300
Каждое правило в roles сопоставляет роль TDG со списком групп.
Параметр role хранит имя существующей системной или пользовательской
роли TDG (например admin, supervisor или user). Если в поле указана
несуществующая роль, она отклоняется при валидации конфигурации.
Опция groups представляет собой непустой список групп, которые TDG ищет
по полю, заданному в параметре groups_claim. Для опции достаточно
одного совпадения, само поле при этом может быть строкой или списком
строк. Пользователю назначаются все роли, чьи правила совпали. Токен с
корректной подписью, но без подходящего правила отклоняется.
Роль также можно сопоставить по идентификатору клиента Keycloak. Этот
вариант подходит для сервисного аккаунта, который аутентифицируется через
Client Credentials. Задайте groups_claim: azp и перечислите ожидаемые
идентификаторы клиентов в опции groups. Здесь azp — это скалярное
поле авторизованной стороны, а не роль Keycloak. Используйте также
authorized_parties, если к TDG могут обращаться только выбранные
клиенты. Предпочитайте azp специфичному для Keycloak полю клиента
сервисного аккаунта: в Keycloak 21 он был переименован из clientId в
client_id, тогда как azp содержит идентификатор клиента во всех
версиях. Проверено с Keycloak 19, 20, 21, 22, 24 и 26.
Токен принимается, если соблюдаются условия ниже:
- заголовок токена содержит
kidи допустимый алгоритм; - если задана опция
header_types, заголовок JOSEtypвходит вheader_types; - поле
typв payload Keycloak входит вclaim_types(по умолчанию принимаетсяBearer); - подпись подтверждается ключом с этим значением
kidиз локального снимка; issравенissuer;audпересекается сaudience;- если задана опция
authorized_parties,azpвходит вauthorized_parties; expприсутствует и ещё не истёк (с учётомclock_skew);nbf, если присутствует, уже наступил;iat, если присутствует, не в будущем, а при заданномmax_token_ageон обязателен и токен не старше этого значения;sub— непустая строка;- совпало хотя бы одно правило из опции
roles.
Каждая из трёх частей токена должна быть в канонической форме base64url
без паддинга (RFC 7515, раздел 2). Паддинг, стандартный алфавит + и
/, пробелы, другие символы или ненулевые хвостовые биты делают токен
некорректным (malformed), так что один токен имеет ровно одно написание.
Из JWKS берутся только те ключи, которые подходят для проверки подписи
(use: sig или отсутствие use; и verify в key_ops, когда этот
участник присутствует). Ключ с указанным alg используется только для
этого алгоритма. Ключ без alg используется со всеми включёнными
алгоритмами, совместимыми с его типом и кривой для ECDSA. RSA-ключи
короче 2048 бит отклоняются. Токен сопоставляется со своим ключом только
по значению kid, поэтому ключ без kid пропускается. Keycloak всегда
его задаёт, а провайдер идентификации, публикующий единственный
безымянный ключ, не поддерживается.
Подписи проверяются по локальному снимку набора ключей. TDG никогда не обращается к сети во время аутентификации. С Keycloak связывается только фоновый обработчик, поэтому задержки Keycloak не влияют на время ответа TDG.
Первая перезагрузка ключей планируется в фоне при применении
конфигурации, при этом применение конфигурации никогда не ждёт Keycloak.
Если Keycloak в этот момент недоступен, конфигурация всё равно
применяется, и после этого запускается экземпляр. Аутентификация
завершается неудачей (fail closed), пока обработчик не загрузит снимок.
Применение конфигурации происходит при каждом перезапуске, и недоступный
провайдер идентификации не должен мешать запуску кластера. Неизменённый
провайдер сохраняет свой верификатор и локальный снимок ключей, когда
конфигурацию меняет другой провайдер. Снимок перезагружается каждые
refresh_interval секунд. Токен с неизвестным kid обычно сигнализирует
о том, что Keycloak сменил ключи. Такой токен отклоняется, но после этого
запускается обработчик, поэтому следующая попытка выполняется успешно.
Такие принудительные перезагрузки ограничены по частоте опцией
min_refresh_interval, чтобы поток неизвестных ключей не превратился в
поток запросов к Keycloak. Как только снимок становится старше jwks_ttl,
токены перестают приниматься даже с корректной подписью, поскольку снимок
больше не считается актуальным. Значение по умолчанию составляет один
час, поскольку ключи подписи меняются редко, а токены несут собственный
срок действия — короткий TTL превращал бы краткие сбои Keycloak в
неудачные входы.
Каждый экземпляр кластера запускает собственный обработчик и хранит
собственный снимок, поскольку аутентифицировать запрос может любой
экземпляр. Каждая перезагрузка загружает сначала документ discovery, а
затем набор ключей. То есть для одного провайдера Keycloak выполняет два
запроса на экземпляр каждые refresh_interval. При заданном jwks_url
выполняется только один запрос. Непредвиденная ошибка внутри обработчика
сохраняется в журнал и считается неудачной перезагрузкой, обработчик при
этом продолжает работать.
В журнал экземпляра попадают только смены состояния — по одной строке на переход:
- когда перезагрузка ключей начала завершаться ошибкой (с указанием причины);
- когда перезагрузка восстановилась.
Отдельные отклонения токенов не записываются в журнал — путь
аутентификации доступен без аутентификации, поэтому строка на каждый
токен позволила бы любому вызывающему переполнить журнал. Успешные и
отклонённые решения внешней аутентификации записываются в журнал аудита
с уровнем INFO. Записи Keycloak содержат настроенный провайдер и, после
проверки токена, его sub и идентификаторы сопоставленных ролей TDG.
Отклонение токенов также содержит причину ошибки (reason) из закрытого
набора кодов: malformed, unsupported_header, header_type,
unknown_key, keys_unavailable, keys_stale, bad_signature, issuer,
audience, authorized_party, claim_type, missing_exp, expired,
not_yet_valid, issued_in_future, missing_iat, too_old,
no_subject, role_unresolved, no_mapped_role и invalid_token для
всего остального. Запись называет kid и alg заголовка JOSE как
unverified_kid и unverified_alg, поскольку они читаются до проверки
подписи. После проверки подписи запись также содержит подлинные поля
sub, azp, aud и exp, чтобы отклонение по полю можно было
объяснить, не доверяя вызывающему. Поля токена, не прошедшего проверку
подписи, никогда не сообщаются. Управляющие символы в любых этих
значениях заменяются. Тело ответа сохраняет общую причину, а текст ошибки
верификатора токена по-прежнему не журналируется и не возвращается:
возвращается только код ошибки из списка выше.
Запрос auth_provider_config также возвращает допускающий null
runtime-статус status для верификаторов, которые его публикуют.
Встроенный OIDC-верификатор сообщает один из статусов (ready,
degraded, stale, unavailable или closed) вместе с количеством и
возрастом кэшированных ключей, временными метками перезагрузки, очищенной
ошибкой перезагрузки и признаком ожидающей принудительной перезагрузки:
- статусы
readyиdegradedозначают, что остался пригодный снимок; - статусы
staleиunavailableозначают отказ (fail closed).
Такой же статус управляет проблемами Cartridge:
- неудачная перезагрузка с пригодными ключами — предупреждение;
- отсутствие пригодного снимка — критично.
Тела ответов, токены, сертификаты и учётные данные никогда не
раскрываются через этот статус. Статус, как и снимок, относится к одному
экземпляру: auth_provider_config сообщает об экземпляре, обслужившем
запрос, а проблемы Cartridge и метрики приходят с каждого экземпляра.
Поддерживаемые метрики приведены в разделе Метрики аутентификации и OAuth 2.0.
Пользовательский способ проверки токена Keycloak можно зарегистрировать
при запуске, после этого он становится доступен через опцию
token_verifier. Регистрацию необходимо выполнять на верхнем уровне
модуля роли, включённого в cartridge.cfg, до того, как TDG начнёт
валидацию конфигурации. Загрузка из управляемой конфигурацией директории
пользовательских ролей не является поддерживаемым механизмом регистрации.
Для верификатора интроспекции, который должен принимать непрозрачные
(не-JWT) токены, необходимо задать значение
accepts_opaque_tokens = true. Маршрутизация по идентификатору (issuer)
JWT для таких токенов намеренно пропускается, поскольку у непрозрачного
токена нет локально читаемого идентификатора. Непрозрачный токен,
который верификатор не распознал, передаётся следующему провайдеру и
встроенной аутентификации TDG:
local keycloak = require('account_provider.keycloak')keycloak.register_token_verifier('custom_runtime', {validate_config = function(options, provider_cfg, prefix, checks)-- Deterministic option checks, run before the config is applied.end,create = function(options, provider_cfg)-- Build caches and workers while configuration is being applied.local verifier = create_verifier(options, provider_cfg)return {verify = function(token)return verifier:verify(token)end,close = function()verifier:close()end,}end,capabilities = {alg = 'CUSTOM',options = {'issuer', 'audience'},accepts_opaque_tokens = true,},})
Верификатор может задать поле code в возвращаемой ошибке, и TDG
передаст в нём один из кодов ошибки (reason).
Поля (claims), относящиеся к этой ошибке, передаются только для
встроенного верификатора, для которого TDG выполняет проверку подписи
самостоятельно.
Здесь подходит верификатор интроспекции (RFC 7662): verify выполняется
на пути аутентификации каждого запроса, поэтому кэшируйте его результат с
коротким TTL и ограниченным размером, сами проверяйте active, exp и
aud и оборачивайте встроенный верификатор
(require('account_provider.keycloak.oidc').verifier()), когда провайдер
выдаёт JWT, а интроспекция лишь добавляет проверку отзыва. Не задавайте
accepts_opaque_tokens, если непрозрачные токены не ожидаются. Внешние
провайдеры проверяются до собственных токенов TDG, поэтому верификатор,
включивший опцию, также получает токены приложений TDG, которые никогда
нельзя отправлять другой стороне.
Провайдер экспортирует методы compile_role_rules(provider_cfg) и
map_roles(payload, rules) для пользовательского типа провайдера,
которому нужно обычное сопоставление поля групп с ролями TDG. Метод
compile_role_rules() подготавливает настроенные правила — вызывайте его
при создании провайдера, а не на каждый запрос. Метод map_roles()
возвращает role_ids, role_names — идентификаторы и имена всех ролей,
чьи правила совпали. Если метод не может разрешить совпавшую несистемную
роль, возвращается nil, nil, err. Разрешённый идентификатор роли
сохраняется в правиле.
Также можно зарегистрировать пользовательский тип провайдера — для провайдеров идентификации, которые не покрывает Keycloak:
local external_auth = require('account_provider.external_auth')external_auth.register_provider_type('custom', {validate_config = function(provider_cfg, prefix)return trueend,create = function(provider_cfg)return {auth = function(request)-- accept, reject, or fallbackreturn {decision = 'fallback'}end,}end,})
После регистрации такой тип настраивается в параметре auth_providers со
значением type: custom.
create выполняется при применении конфигурации, а не на каждый запрос,
поэтому дорогая инициализация должна быть там. Новый набор экземпляров
строится до того, как TDG переключится на него: если их создание не
удалось, ранее работавшая конфигурация остаётся активной, а уже
построенные экземпляры закрываются. После успешного переключения или при
удалении провайдера close старого экземпляра вызывается ровно один раз.
Текст ошибки обратного вызова не возвращается вызывающему и не
сохраняется в журнале. Обратный вызов содержит опции провайдера и
bearer-токен, так что его сообщение может содержать секрет и находиться
вне контроля TDG. В обратном вызове сообщается только имя провайдера и
верификатора. Верификатор должен сохранять в журнал собственные смены
состояния — так же, как встроенный сообщает, если снимок JWKS перестал
обновляться. Детерминированные проверки опций относятся к
validate_config, чей результат сообщается администратору в виде ошибки
валидации конфигурации.