Войти через Ory Hydra с помощью OAuthClient из Versola OAuth SDK можно, зарегистрировав публичный клиент и передав SDK issuer Hydra. Руководство опирается на собственные интеграционные тесты SDK: они шли против oryd/hydra:v26.2.0 с референсным приложением входа и согласия от Ory oryd/hydra-login-consent-node:v26.2.0.
Сначала прочитайте Руководства по провайдерам: там минимальный контракт и настройка redirect, общая для всех провайдеров.
Hydra — сервер OAuth 2.0 и OpenID Connect без пользовательского интерфейса. Экраны входа, согласия и выхода — это приложение, которое вы пишете или разворачиваете и на которое указываете Hydra. Все, что пользователь видит во время login() и logout(), приходит из этого приложения, а не из SDK и не из Hydra.
Что настроить в Hydra
Конфигурация сервера
Эти ключи взяты из hydra.yml тестовой среды. Значения — заглушки.
urls:
self:
issuer: https://auth.example.com/
public: https://auth.example.com/
login: https://login.example.com/login
consent: https://login.example.com/consent
logout: https://login.example.com/logout
secrets:
system:
- replace-with-your-own-generated-secret
oauth2:
pkce:
enforced_for_public_clients: true
urls.self.issuerобъявляется дословно, вместе с завершающим слешем, в discovery и вissкаждого ID-токена. Hydra строит свой issuer и все URL, которые отдает браузеру, изurls.self.issuerиurls.self.public, а не из запроса. Выберите значение один раз и передайте SDK ту же строку.login,consentиlogout— URL вашего пользовательского приложения.oauth2.pkce.enforced_for_public_clients: trueзаставляет Hydra отклонять запрос публичного клиента без code challenge. SDK всегда отправляетS256, так что от этой настройки он не зависит. Включите ее, чтобы другие клиенты не могли пропустить PKCE. Документация Ory говорит, что по умолчанию PKCE можно использовать без принуждения, а требовать его можно для всех клиентов или только для публичных.
Клиент
Зарегистрируйте приложение как публичный клиент. Тестовая среда импортирует массив клиентов командой Hydra CLI.
hydra import oauth2-client clients.json --endpoint http://hydra.example.com:4445
Одиночный объект отклоняется. Файл — массив JSON.
[
{
"client_id": "example-app",
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "openid offline_access profile email",
"redirect_uris": ["https://app.example.com/oauth"],
"post_logout_redirect_uris": ["https://app.example.com/signed-out"],
"skip_consent": false
}
]
token_endpoint_auth_method: noneделает клиент публичным. В CLI Hydra по умолчанию стоитclient_secret_basic, поэтомуnoneнужно задать явно.redirect_urisсравниваются точно. Wildcard не допускается, custom-схемы вродеcom.example.app:/callbackдопускаются, аjavascript:,data:иvbscript:отклоняются. Зарегистрируйте точную строкуredirectUriдля каждой платформы. Это одинаково относится к Android App Links, iOS Universal Links и custom-схемам.post_logout_redirect_urisдолжен содержать точную строкуpostLogoutRedirectUri. Использовать можно только зарегистрированные URI.scopeперечисляет все scope, которые клиент может запрашивать, включаяoffline_access. Клиент, у которого его в scope нет, refresh-токен не получит.
Scope и refresh-токены
Refresh-токен выдается, когда offline_access запрошен и предоставлен: клиенту должно быть разрешено его запрашивать, а ваше приложение согласия должно принять его для пользователя. Тесты наблюдали токен для openid profile offline_access и отсутствие токена для openid profile. Discovery перечисляет scopes_supported: [offline_access, offline, openid]. Тестовое приложение согласия предоставляет все scope, которые просит запрос. Ваше не должно отбрасывать offline_access.
Ротация refresh-токенов
Refresh-токены Hydra всегда одноразовые, а повторное использование отзывает токены, связанные с тем же согласием. Необязательный льготный период — oauth2.grant.refresh_token.rotation_grace_period. Он выключен, пока не задан, и не превышает 5 минут, не считая повторных использований. Он смягчает повтор после сбоя. Страница Ory называет его обходным путем, ослабляющим обнаружение повторного использования, а правило SDK от него не меняется: invalid_grant — это Reauthenticate(GrantRejected), без повторов.
Выход
Эндпоинт end-session у Hydra — {public}/oauth2/sessions/logout. Если передать id_token_hint, зарегистрированный post_logout_redirect_uri и state, он перенаправляет браузер на ваш интерфейс выхода (urls.logout, с logout_challenge), который должен принять или отклонить запрос. После этого Hydra перенаправляет на post-logout URI с state. Тестовое приложение подтверждает выход, если пользователь не отказался. Если ваш интерфейс выхода так и не принимает запрос, браузер не возвращается, и logout() возвращает sessionEnded = false.
Эндпоинт отзыва — /oauth2/revoke. Публичный клиент отправляет client_id с token и никакой другой аутентификации клиента. Отозванный refresh-токен после этого дает 400 invalid_grant на токен-эндпоинте.
Конфигурация SDK
issuer — в точности то, что в urls.self.issuer, включая завершающий слеш. При сравнении с discovery SDK принимает обе формы (один завершающий слеш там не учитывается), но iss ID-токена сверяется с найденным значением байт в байт, поэтому пишите то, что объявляет Hydra.
Kotlin
import versola.oauth.OAuthClient
import versola.oauth.OAuthConfiguration
val client = OAuthClient.create(
context,
OAuthConfiguration(
issuer = "https://auth.example.com/",
clientId = "example-app",
redirectUri = "https://app.example.com/oauth",
postLogoutRedirectUri = "https://app.example.com/signed-out",
scopes = setOf("openid", "profile", "offline_access"),
),
)
Swift
import OAuthClient
let configuration = try OAuthConfiguration(
issuer: "https://auth.example.com/",
clientId: "example-app",
redirectUri: "https://app.example.com/oauth",
postLogoutRedirectUri: "https://app.example.com/signed-out",
scopes: ["openid", "profile", "offline_access"]
)
let client = try await OAuthClient.create(
configuration: configuration,
presentationAnchor: appKeyWindow // from the Provider guides page
)
Dart
import 'package:versola_oauth/versola_oauth.dart';
final client = await OAuthClient.create(const OAuthConfiguration(
issuer: 'https://auth.example.com/',
clientId: 'example-app',
redirectUri: 'https://app.example.com/oauth',
postLogoutRedirectUri: 'https://app.example.com/signed-out',
scopes: {'openid', 'profile', 'offline_access'},
));
Замечания к значениям
scopes: включитеoffline_access, иначе у сессии нет refresh-токена, и она заканчивается вместе с access-токеном. SDK сам его никогда не добавляет. Discovery его перечисляет, поэтому конфигурация без него получает одно предупреждение приcreate().postLogoutRedirectUri: задайте его и зарегистрируйте.sessionEndedравенtrueтолько тогда, когда браузер вернулся на него сstate, который отправил SDK.endSessionEndpoint: не нужен, discovery его объявляет.pushedAuthorization: оставьте значение по умолчанию. Hydra не объявляет PAR, поэтомуWhenAdvertisedотправляет обычный запрос, аRequiredдал быProviderUnsupported("par").tokenBinding: оставьтеBearer. Hydra не объявляет DPoP, поэтому требование DPoP даетProviderUnsupported("dpop"), а запрос DPoP без требования возвращается к bearer с предупреждением.
Чего ожидать
| Возможность | Hydra v26.2.0 | Что делает SDK |
|---|---|---|
| Discovery | issuer дословно из urls.self.issuer. jwks_uri под /.well-known/jwks.json | Загружает {issuer}/.well-known/openid-configuration и сравнивает issuer. |
| Refresh-токен | Только если offline_access запрошен и предоставлен | С ним: Authenticated с refresh-токеном. Без него: сессия заканчивается с access-токеном ошибкой Reauthenticate(SessionExpired), запрос токена не отправляется. |
| Отзыв токена | Есть. Публичный клиент отправляет только client_id | При успехе tokenRevoked = true. |
| End-session | Есть, через ваш интерфейс выхода | sessionEnded = true, когда браузер вернулся с state. |
| UserInfo | Есть. Непрозрачные access-токены принимаются | userInfo() работает. |
| PAR | Не объявлен | Обычный authorization-запрос. |
RFC 9207 iss в redirect | Не объявлен и не отправляется | Не требуется, раз не объявлен. Проверяется, если есть. |
| DPoP | Не объявлен | Bearer. |
| Алгоритм ID-токена | Только RS256 | Принимается. |
Заголовок Date в ответе токен-эндпоинта | Отсутствует | Поправки на серверное время нет. Часы устройства сверяются в пределах clockSkewTolerance. |
| Лимиты запросов | Self-hosted: в конфигурации лимитов нет. Ory Network: 429 с x-ratelimit-reset, Retry-After не упомянут | Обычный backoff, потому что SDK не читает x-ratelimit-reset. 429 или 503 с Retry-After учитывается (Ошибки сервера, 429 и Retry-After) |
Известные ограничения
- Нужно приложение входа, согласия и выхода. SDK его предоставить не может.
- Нет
issв redirect, значит SDK не может применить против Hydra защиту от mix-up по RFC 9207. ПроверкаstateиissID-токена остаются. - Повтор refresh после сбоя заканчивается
Reauthenticate(GrantRejected). Льготный период это смягчает.
Как это проверялось
Интеграционные тесты Android шли против oryd/hydra:v26.2.0 в CI SDK. Тесты отправки PAR к Hydra не применимы, потому что она не объявляет PAR, а два теста ProviderUnsupported для PAR выполняются. Драйверы тестов для iOS и Flutter запускают те же тесты. Что выполнялось:
- Вход со scope
openid profile offline_access, вызов с токеном, UserInfo, отзыв токена и выход через интерфейс выхода, нигде нетclient_secret. - Подмененные ID-токены отклонялись с соответствующей причиной.
- Явные эндпоинты. Весь flow прошел по явным эндпоинтам, без запроса discovery.
- Несовпадения PAR. Случаи
Neverпри обязательном PAR иRequiredбез эндпоинта далиProviderUnsupported("par"). - Варианты выхода без
end_session_endpoint, с неудачным отзывом и с закрытым браузером. - Нет refresh-токена. Клиент с grant type только
authorization_code, scope безoffline_accessи временем жизни access-токена 10 с закончилReauthenticate(SessionExpired). - Куда уходит токен. Токен прикреплялся только к настроенному источнику.
Стенд работал с DSN=memory и serve all --dev (обычный HTTP, cookie без флага secure). SDK обращался к нему через прокси драйвера тестов на фиксированном порту, потому что Hydra берет свои URL из конфигурации. Диалог входа занял около десяти HTTP-переходов через референсное приложение Ory.
Источники
Прочитано 2026-10-30. То, что наблюдали тесты, взято из интеграционных тестов SDK, а не с этих страниц.
- Ory, OAuth2-клиенты (публичный клиент, redirect URI, post-logout URI, grant types): https://www.ory.com/docs/hydra/guides/oauth2-clients
- Ory, OIDC logout (
id_token_hint,post_logout_redirect_uri,state, зарегистрированные URI): https://www.ory.com/docs/oauth2-oidc/oidc-logout - Ory, authorization code flow (принудительный PKCE,
offline_access, согласие, S256): https://www.ory.com/docs/oauth2-oidc/authorization-code-flow - Ory, graceful token refresh (льготный период ротации): https://www.ory.com/docs/hydra/guides/graceful-token-refresh
Что дальше
- Подключите клиент к приложению: Android, iOS или Flutter.
- Узнайте, что означает каждая категория ошибок: Состояния сессии и ошибки.
- Посмотрите руководство по другому провайдеру: Keycloak.