Войти через 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 method | S256 (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_access | Authenticated с refresh-токеном. Ротация — как настроен realm. |
| Отзыв токена | Есть. Публичный клиент отправляет только client_id | logout() отзывает токен, tokenRevoked = true. |
| End-session | Есть. Прямой 302 с state, если отправлены id_token_hint и зарегистрированный URI | sessionEnded = 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, а не с этих страниц.
- Keycloak, securing applications, эндпоинты OIDC: https://www.keycloak.org/securing-apps/oidc-layers
- Keycloak server admin guide, базовые настройки клиента (client authentication, standard flow, valid redirect URIs): https://raw.githubusercontent.com/keycloak/keycloak/main/docs/documentation/server_admin/topics/clients/oidc/con-basic-settings.adoc
- Keycloak server admin guide, offline-токены: https://raw.githubusercontent.com/keycloak/keycloak/main/docs/documentation/server_admin/topics/sessions/offline.adoc
- Keycloak server admin guide, таймауты сессий и «Revoke Refresh Token»: https://raw.githubusercontent.com/keycloak/keycloak/main/docs/documentation/server_admin/topics/sessions/timeouts.adoc
Что дальше
- Подключите клиент к приложению: Android, iOS или Flutter.
- Узнайте, что означает каждая категория ошибок: Состояния сессии и ошибки.
- Посмотрите руководство по другому провайдеру: Ory Hydra.