Keycloak — Versola Docs
/docs
0.1.0versola.ioGitHub

Keycloak

Как подключить Versola OAuth SDK к Keycloak 26.4 через публичный клиент, с настройками realm, конфигурацией SDK для каждой платформы и результатами тестов.

Войти через Keycloak с помощью OAuthClient из Versola OAuth SDK можно, создав в realm публичный клиент и передав SDK issuer этого realm. Руководство опирается на собственные интеграционные тесты SDK: они шли против quay.io/keycloak/keycloak:26.4 в режиме разработки со специально подготовленным тестовым realm.

Сначала прочитайте Руководства по провайдерам: там минимальный контракт и настройка redirect, общая для всех провайдеров.

Что настроить в Keycloak

Realm и issuer

Discovery-документ лежит по адресу {base}/realms/{realm}/.well-known/openid-configuration, issuer — {base}/realms/{realm}. Откройте discovery-документ в браузере и скопируйте его член issuer: именно это значение нужно настроить. В режиме разработки Keycloak строит issuer, все эндпоинты и action формы входа из Host запроса, так что то, что вы видите, зависит от способа обращения. В production hostname фиксируется в собственной конфигурации.

Клиент

Создайте для приложения клиент OpenID Connect. В таблице ниже то, что задает тестовый realm. В обратных кавычках имена атрибутов, которые он использует при импорте realm.

НастройкаЗначениеЗачем
Client authenticationВыключено (publicClient: true)Нативное приложение не хранит секрет, а SDK не отправляет секрет.
Standard flowВключено (standardFlowEnabled)Authorization code flow — единственный flow, который использует SDK.
Implicit flow, direct access grants, service accountsВыключеноНе используются. В тестовом realm они выключены.
Valid redirect URIsТочная строка redirectUri, по записи на платформу, если они различаютсяKeycloak сравнивает точно и с учетом регистра. Wildcard допустим только в конце шаблона.
Valid post logout redirect URIsТочная строка postLogoutRedirectUri (attributes."post.logout.redirect.uris")Без зарегистрированного URI Keycloak не возвращает пользователя в приложение после выхода.
PKCE code challenge methodS256 (attributes."pkce.code.challenge.method")Keycloak отклоняет запрос без S256. SDK всегда его отправляет.
Web originsПустоНативное приложение не делает кросс-доменных запросов из браузера.

Redirect URI для Android App Links и iOS Universal Links — обычные записи в этом списке. Тестовый realm регистрирует один URL https. Берите форму https, как и везде (настройка redirect).

Scope и refresh-токены

По умолчанию Keycloak выдает refresh-токен в authorization code flow и без особого scope: тестовый клиент запрашивает только openid profile и получает его. В тестовом realm offline_access еще перечислен как необязательный client scope.

В Keycloak offline_access означает другое: он запрашивает offline-токен. По документации Keycloak offline-токен не ограничен таймаутами SSO-сессии, остается действительным после выхода пользователя, его нужно использовать хотя бы раз в 30 дней (или в пределах настройки Offline Session Idle у realm) и для него у пользователя должна быть роль realm offline_access. Не добавляйте его, если такое поведение вам не нужно. SDK сам его никогда не добавляет. При create() он пишет одно предупреждение, если scopes_supported содержит offline_access, а ваши scope — нет. Для Keycloak в предупреждении сказано, что refresh-токен может не выдаться, а Keycloak выдает его в любом случае.

Ротация refresh-токенов

Ротация — настройка realm: Revoke Refresh Token в Realm settings, Tokens. Тестовый realm задает revokeRefreshToken: true и refreshTokenMaxReuse: 0. Когда она включена, повторное использование уже обмененного refresh-токена дает 400 invalid_grant («Maximum allowed refresh token reuse exceeded») и отзывает сессию. SDK сохраняет каждый новый токен раньше, чем забывает старый, трактует invalid_grant как Reauthenticate(GrantRejected) и никогда его не повторяет. Задавайте ее сознательно.

Если приложение убито после того, как Keycloak обменял токен, но до сохранения нового, следующий refresh окажется повтором и закончится GrantRejected.

Выход

Эндпоинт end-session у Keycloak — .../protocol/openid-connect/logout, и discovery его объявляет. Если передать id_token_hint, зарегистрированный post_logout_redirect_uri и state, он отвечает прямым 302 на post-logout URI с state, без страницы подтверждения. Без id_token_hint Keycloak показывает страницу подтверждения.

Эндпоинт отзыва — .../protocol/openid-connect/revoke. Публичный клиент отправляет client_id вместе с token и token_type_hint и больше ничего.

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

issuer ниже — это {base}/realms/{realm} с именем realm example. Завершающего слеша нет, потому что Keycloak его не объявляет.

Kotlin

import versola.oauth.OAuthClient
import versola.oauth.OAuthConfiguration

val client = OAuthClient.create(
    context,
    OAuthConfiguration(
        issuer = "https://id.example.com/realms/example",
        clientId = "example-app",
        redirectUri = "https://app.example.com/oauth",
        postLogoutRedirectUri = "https://app.example.com/signed-out",
        scopes = setOf("openid", "profile"),
    ),
)

Swift

import OAuthClient

let configuration = try OAuthConfiguration(
    issuer: "https://id.example.com/realms/example",
    clientId: "example-app",
    redirectUri: "https://app.example.com/oauth",
    postLogoutRedirectUri: "https://app.example.com/signed-out",
    scopes: ["openid", "profile"]
)
let client = try await OAuthClient.create(
    configuration: configuration,
    presentationAnchor: appKeyWindow  // from the Provider guides page
)

Продукт Swift-пакета называется VersolaOAuth, а импортируется модуль OAuthClient.

Dart

import 'package:versola_oauth/versola_oauth.dart';

final client = await OAuthClient.create(const OAuthConfiguration(
  issuer: 'https://id.example.com/realms/example',
  clientId: 'example-app',
  redirectUri: 'https://app.example.com/oauth',
  postLogoutRedirectUri: 'https://app.example.com/signed-out',
  scopes: {'openid', 'profile'},
));

Замечания к значениям

  • scopes: openid добавляется всегда. Добавляйте profile, email и другие, как разрешают scope клиента. offline_access не добавляйте, если вам не нужен offline-токен (см. «Scope и refresh-токены»).
  • postLogoutRedirectUri: задайте его. sessionEnded в результате выхода равен true только тогда, когда браузер вернулся на этот URI с state, который отправил SDK. Без него провайдер может завершить сессию, а результат все равно скажет false. На Android в Auth Tab значение https тоже должно быть проверенным App Link.
  • endSessionEndpoint: не нужен, Keycloak его объявляет.
  • pushedAuthorization: оставьте значение по умолчанию. Keycloak объявляет PAR, поэтому WhenAdvertised отправляет запрос через PAR, и интеграционные тесты шли именно так.
  • tokenBinding: оставьте Bearer по умолчанию. О DPoP см. строку DPoP ниже.

Чего ожидать

ВозможностьKeycloak 26.4Что делает SDK
Refresh-токенВыдается без offline_accessAuthenticated с refresh-токеном. Ротация — как настроен realm.
Отзыв токенаЕсть. Публичный клиент отправляет только client_idlogout() отзывает токен, tokenRevoked = true.
End-sessionЕсть. Прямой 302 с state, если отправлены id_token_hint и зарегистрированный URIsessionEnded = true, когда redirect вернулся с совпавшим state.
UserInfoЕстьuserInfo() работает, sub сверяется с ID-токеном.
PARОбъявлен pushed_authorization_request_endpoint, require_pushed_authorization_requests: false, публичный клиент принятWhenAdvertised по умолчанию отправляет запрос через PAR и открывает браузер только с client_id и request_uri. Отказ (например, из-за незарегистрированного redirect URI) — Configuration(AuthorizationRejected) до открытия браузера: Keycloak отвечает 400 invalid_request «Invalid parameter: redirect_uri».
RFC 9207 iss в redirectОбъявлен и отправляетсяПроверяется при каждом redirect.
DPoPОбъявлен dpop_signing_alg_values_supported с ES, RS и PS 256–512 и EdDSAПо умолчанию Bearer. Запрос DPoP без требования использует DPoP, раз объявлен ES256.
Алгоритм ID-токенаПо умолчанию RS256. В списке discovery еще HS256/384/512 и EdDSAЗакрытый набор SDK отклоняет HMAC, что бы ни перечислял discovery, и принимает RS256.
Заголовок Date в ответе токен-эндпоинтаОтсутствуетПоправка на расхождение с серверным временем не включается. iat и exp сверяются с часами устройства в пределах clockSkewTolerance.
Лимиты запросовТолько защита от перебора (пароль, OTP), по умолчанию выключена. 429 и Retry-After в документации нетНастраивать нечего. 429 или 503 с Retry-After от прокси впереди учитывается (Ошибки сервера, 429 и Retry-After)

Известные ограничения

  • Повтор refresh после сбоя заканчивается Reauthenticate(GrantRejected) (см. «Ротация refresh-токенов»).
  • Realm, который требует обязательное действие, шаг MFA или экран согласия, покажет эти страницы в браузере. SDK в них не участвует.

Как это проверялось

Интеграционные тесты Android шли против quay.io/keycloak/keycloak:26.4 в CI SDK, и все прошли. Драйверы тестов для iOS и Flutter прогоняют те же тесты против того же контейнера. Что проверяли тесты:

  • Вход, вызов защищенного источника с токеном, UserInfo и выход. В запросе токена не было client_secret, ID-токен проверялся раньше, чем сессия становилась Authenticated, и ни один запрос к незащищенному источнику не уходил с заголовком Authorization.
  • Подмененные ID-токены. ID-токен, подписанный заново, с неверным nonce, с плохой подписью или алгоритмом, с неверными iss, aud, exp, iat или auth_time, отклонялся с соответствующей причиной.
  • Явные эндпоинты. Весь flow прошел по явным эндпоинтам, ни одного запроса к пути /.well-known/ не было.
  • PAR. PAR применялся, когда объявлен, отклоненный PAR всплывал как AuthorizationRejected до браузера, а несовпадения Never и Required давали ProviderUnsupported.
  • Варианты выхода. Выход прогнали без end_session_endpoint в discovery, с неудачным отзывом и с закрытым браузером.
  • Нет refresh-токена. Клиент, зарегистрированный без refresh-токенов и с access-токенами на десять секунд, заканчивал Reauthenticate(SessionExpired) без запроса токена.
  • Куда уходит токен. Токен прикреплялся к настроенному источнику и больше ни к какому.

Тестовый realm одноразовый: режим разработки, sslRequired: none, тестовый пользователь.

Источники

Прочитано 2026-10-30. То, что наблюдали тесты, взято из интеграционных тестов SDK, а не с этих страниц.

Что дальше