Руководства по провайдерам — Versola Docs
/docs
0.1.0versola.ioGitHub

Руководства по провайдерам

Минимальный контракт OpenID-провайдера для Versola OAuth SDK, руководства по провайдерам, поведение при лимитах запросов и настройка redirect, общая для всех.

OAuthClient из Versola OAuth SDK (Android, iOS, Flutter) работает с любым провайдером, который выполняет короткий контракт. На этой странице описан контракт, перечислены провайдеры с руководствами, показано, как лимиты запросов провайдера доходят до приложения, и разобрана настройка redirect, одинаковая для всех. Условие одно: провайдер должен быть OpenID Provider и поддерживать authorization code flow с PKCE для публичного клиента.

Руководства

ПровайдерРуководствоВерсия в тестах SDK
KeycloakKeycloak26.4
Ory HydraOry Hydrav26.2.0 (self-hosted, образ с открытым кодом)

Интеграционные тесты Android в CI SDK проходят весь сценарий против контейнера провайдера: вход, вызов с токеном, UserInfo, отзыв токена и выход, а еще подмененные ID-токены, PAR, явные эндпоинты и клиента без refresh-токенов. Те же тесты для iOS и Flutter гоняются против тех же контейнеров. Браузерный этап — это диалог входа провайдера, который тест проходит по HTTP. В каждом руководстве сказано, что увидели тесты.

Минимальный контракт

Провайдер работает с OAuthClient, если выполняет все из списка.

  • Discovery или явные эндпоинты. Документ по адресу {issuer}/.well-known/openid-configuration, у которого член issuer равен настроенному issuer. Один завершающий слеш с любой стороны при сравнении не учитывается. Без discovery вы передаете ProviderEndpoints, и тогда issuer передать нельзя.
  • Authorization code flow с PKCE S256 для публичного клиента. У клиента нет секрета, запрос токена несет только client_id. SDK всегда отправляет S256.
  • response_type=code, state возвращается в redirect.
  • ID-токен с асимметричной подписью (RS256, PS256, ES256, ES384, ES512, RS384, RS512, PS384 или PS512), ключи по адресу jwks_uri. none и HMAC отклоняются.
  • Claim iss в ID-токене равен найденному issuer байт в байт.

Все остальное необязательно, SDK подстраивается под то, что говорит discovery.

Чего у провайдера нетЧто делает SDK
Refresh-токенаСессия заканчивается ошибкой Reauthenticate(SessionExpired), когда истекает access-токен. Запрос токена не отправляется.
Эндпоинта отзываВыход пропускает отзыв и возвращает tokenRevoked = false.
end_session_endpointВыход стирает локальное состояние и возвращает sessionEnded = false, пока вы не передали URL в endSessionEndpoint.
Эндпоинта UserInfouserInfo() падает с Configuration(ProviderUnsupported).
PARSDK отправляет обычный authorization-запрос.
DPoPТокены остаются bearer.

Провайдер не поддерживается, если он требует client secret от нативного приложения, принимает только plain PKCE или не принимает PKCE вовсе, подписывает ID-токены только HMAC или объявляет issuer, не совпадающий с его discovery-документом. Шаблонный issuer — как раз такое несовпадение.

Общая настройка redirect для всех провайдеров

Что регистрировать у провайдера, зависит от провайдера, это в каждом руководстве. Что приложение делает с redirect, везде одно и то же.

SDK принимает три вида redirectUri: URL https (проверенный App Link или Universal Link, предпочтительно), loopback-URL или private-use схему вроде com.example.app:/oauth. Private-use схема работает, но SDK пишет предупреждение при каждом create(): ту же схему может зарегистрировать любое приложение на устройстве. Открытый http на хост, не являющийся loopback, отклоняется. На iOS отклоняется и открытый loopback-URL, потому что ASWebAuthenticationSession его принять не может.

Настройка платформы, которая не зависит от провайдера, описана на страницах Android, iOS и Flutter. Ниже то, от чего зависит, дойдет ли redirect до приложения.

Зарегистрируйте у провайдера redirect URI в точности так, как он записан в OAuthConfiguration.redirectUri. Затем объявите в манифесте приложения redirect-activity SDK заново, с intent-фильтром. Манифест самого SDK объявляет activity без фильтра, а manifest merger объединяет два объявления. Фильтр ниже покрывает и путь выхода.

<activity
    android:name="versola.sdk.browser.VersolaRedirectActivity"
    android:exported="true"
    tools:node="merge">
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https" android:host="app.example.com" android:path="/oauth" />
        <data android:scheme="https" android:host="app.example.com" android:path="/signed-out" />
    </intent-filter>
</activity>

Хост должен быть вашим и отдавать Digital Asset Links (https://app.example.com/.well-known/assetlinks.json) для имени пакета и сертификата подписи приложения. SDK открывает Auth Tab, если браузер устройства его поддерживает, и Custom Tab в остальных случаях. В Auth Tab браузер проверяет, что хост redirect принадлежит вашему приложению. Если проверить не получилось, login() падает с Configuration(InvalidConfiguration), и причина в assetlinks.json, а не у провайдера. То же верно для postLogoutRedirectUri, если это URL https: без проверенного хоста вкладка закрывается без проверки, и logout() возвращает sessionEnded = false.

Для private-use схемы в intent-фильтре вместо этого стоит android:scheme="com.example.app".

Артефакт Android — io.versola:oauth-android на Maven Central, minSdk — 26.

Для redirect URI с https SDK запускает ASWebAuthenticationSession с callback https(host:path:), для него нужна iOS 17.4 (минимальная платформа пакета). Нужен entitlement Associated Domains для хоста redirect, а файл apple-app-site-association на этом хосте должен указывать приложение в webcredentials. Одной связки applinks недостаточно. AppAuth-iOS, который использует тот же системный API, документирует то же требование (README AppAuth-iOS).

Окно, из которого показывается панель входа, вы передаете один раз при создании клиента. Минимальный пример для приложения с одним окном, для иллюстрации:

import AuthenticationServices
import UIKit

@MainActor
func appKeyWindow() -> ASPresentationAnchor {
    UIApplication.shared.connectedScenes
        .compactMap { $0 as? UIWindowScene }
        .flatMap(\.windows)
        .first(where: \.isKeyWindow) ?? ASPresentationAnchor()
}

Universal Link, который застал приложение при холодном старте, передайте в client.handleRedirect(url): метод возвращает true, если URL — это redirect, которого ждал SDK. В SwiftUI:

.onOpenURL { url in
    Task { _ = await client.handleRedirect(url) }
}

Flutter

Плагин — обертка над нативными SDK, своей обработки redirect у него нет. Работу с манифестом Android, описанную выше, сделайте в проекте android/ приложения, а настройку entitlements iOS — в таргете Runner. На iOS плагин показывает панель из key window активной сцены. Приложение со своей работой с окнами задает VersolaOAuthPlugin.presentationAnchor до первого create. HTTP-клиента с авторизацией в Dart нет: учетные данные отдает только вызов accessToken().

Как лимиты провайдера доходят до приложения?

Перегруженный эндпоинт токена провайдеры отвечают 429, а некоторые добавляют Retry-After. Versola OAuth SDK учитывает Retry-After на 429 и 503, в секундах или датой HTTP, вместо собственного backoff и никогда не сокращает ожидание под свой бюджет. Значение, которое в бюджет не помещается, останавливает операцию, и приложение получает Recoverable с причиной Server и заданным retryAfter. Пока оно не прошло, SDK не отправляет запросов к этому источнику. Без заголовка 429 повторяется в обычном бюджете повторов, как 5xx.

Что присылает провайдерЧто делает SDK
Retry-After на 429 или 503Ждет сам или отдает значение приложению
Только собственные заголовки (X-RateLimit-*, X-Rate-Limit-*, x-ratelimit-reset)Обычный backoff. Эти заголовки SDK не читает
Заголовков лимитов нетОбычный backoff

Настраивать для каждого провайдера ничего не нужно. Не поднимайте максимумы повторов, чтобы пересидеть лимит: это жесткие границы. Правила повторов и код для каждой платформы — на странице Ошибки сервера, 429 и Retry-After.

Как читать ошибку

Ошибки делятся на четыре категории, и категория подсказывает приложению, что делать: Recoverable (предложить повтор или дождаться retryAfter), Reauthenticate (отправить пользователя на login()), StepUp (вызвать stepUp) и Configuration (ошибка развертывания: исправьте ее, а не перехватывайте в production). Большинство проблем первого запуска с провайдером приходят как Configuration.

Что вы видитеОбычно значит
DiscoveryIssuerMismatchНастроенный issuer не совпадает с тем, что объявляет discovery-документ провайдера. Скопируйте член issuer точно.
InvalidConfiguration после закрытия браузера (Android)Хост redirect — не проверенный App Link этого приложения.
IdTokenRejected(reason)ID-токен не прошел проверку: алгоритм, iss, aud, exp, nonce или подпись. Повтор не поможет.
ProviderUnsupported(capability)Конфигурация просит то, чего провайдер не объявляет (dpop, par, S256), или у userInfo() нет эндпоинта.
AuthorizationRejectedПровайдер отклонил pushed authorization request (PAR), например из-за незарегистрированного redirect URI.

Незарегистрированный redirect URI в обычном запросе (не PAR) показывает страницу ошибки в браузере, которой SDK не видит. Со стороны это неотличимо от закрытой пользователем вкладки. Сначала проверьте зарегистрированную строку. Полный список — на странице Состояния сессии и ошибки.

Что дальше