Ory Hydra — Versola Docs
/docs
0.1.0versola.ioGitHub

Ory Hydra

Как подключить Versola OAuth SDK к self-hosted Ory Hydra v26.2.0 через публичный клиент, с настройками сервера и клиента и результатами тестов SDK.

Войти через 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
Discoveryissuer дословно из 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 и iss ID-токена остаются.
  • Повтор 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, а не с этих страниц.

Что дальше