Чтобы перейти с AppAuth-Android, AppAuth-iOS или flutter_appauth (он оборачивает обе библиотеки) на Versola OAuth SDK, код запроса, redirect и хранения токенов заменяют одним OAuthClient и одним вызовом login(). На странице сопоставлены понятия AppAuth и SDK, перечислено, что вы перестаете делать и что ведет себя иначе, и показан код до и после для Android, iOS и Flutter. Условие: SDK рассчитан на публичные клиенты, которые входят через системный браузер, поэтому сначала прочитайте Что умеет AppAuth, но не умеет SDK?
Предварительные требования
- Приложение, которое сейчас входит через AppAuth-Android, AppAuth-iOS или
flutter_appauth. - Клиент у провайдера, который можно зарегистрировать как публичный: без секрета, с PKCE
S256. - Страница вашего провайдера в разделе Провайдеры, например Keycloak: там scope, redirect URI и выход.
Что меняется, когда вы заменяете AppAuth?
- Приложение больше не хранит состояние аутентификации. Нет
AuthState, который нужно сериализовать, и нет refresh token у вас в руках. Хранение, обновление и выход ведетOAuthClient, а вы наблюдаете за сессией. - Вход — один вызов,
login(). PKCE,state,nonce, проверка redirect, обмен кода и проверка ID token происходят внутри него и не отключаются. - Versola OAuth SDK защищает каждый сохраненный токен через Android Keystore или iOS Keychain.
- После обновления пользователи входят один раз, потому что существующие refresh token импортировать нельзя (см. Что будет с текущими пользователями и токенами?).
- Сбои провайдера приходят в приложение в виде четырех категорий ошибок, а безопасные запросы SDK повторяет сам (см. Как меняется обработка ошибок?).
Как понятия AppAuth соответствуют понятиям SDK?
Имена Kotlin взяты из versola.oauth (типы ошибок и состояний — из versola.oauth.core), имена Swift — из модуля OAuthClient (продукт пакета VersolaOAuth), имена Dart — из package:versola_oauth/versola_oauth.dart.
| Понятие AppAuth | Имена в AppAuth | Этот SDK |
|---|---|---|
| Конфигурация провайдера через discovery | Android AuthorizationServiceConfiguration.fetchFromIssuer; iOS OIDAuthorizationService.discoverConfiguration(forIssuer:); Flutter issuer или discoveryUrl | OAuthConfiguration(issuer = ...). Discovery запускается лениво при первой операции, кэшируется на диске и перечитывается через 24 часа или после сбоя, который мог вызвать смену эндпоинта. |
| Конфигурация провайдера через эндпоинты | Android new AuthorizationServiceConfiguration(authorizationEndpoint, tokenEndpoint); iOS OIDServiceConfiguration(authorizationEndpoint:tokenEndpoint:); Flutter serviceConfiguration | OAuthConfiguration(endpoints = ProviderEndpoints(...)). Обязательны issuer, authorizationEndpoint, tokenEndpoint и jwksUri: ID token, который нельзя проверить, отклоняется. Несовместимо с issuer. |
| Discovery по нестандартному URL | Android fetchFromUrl | Аналога нет. Задайте эндпоинты явно. |
| Запрос | Android AuthorizationRequest.Builder; iOS OIDAuthorizationRequest; Flutter AuthorizationTokenRequest | Это не объект, который вы собираете. clientId, redirectUri и scopes лежат в OAuthConfiguration, а выбор на один вход делается через LoginOptions (scopes, prompt, loginHint, uiLocales, acrValues, maxAge, resources, extraParameters, ephemeralBrowser). |
| Запуск потока | Android AuthorizationService.performAuthorizationRequest или getAuthorizationRequestIntent, затем performTokenRequest; iOS OIDAuthState.authState(byPresenting:presenting:); Flutter authorizeAndExchangeCode | client.login(options): один suspend-вызов, который запускает браузер, проверяет redirect, обменивает код и проверяет ID token. |
| Объект сервиса | Android AuthorizationService; iOS OIDAuthorizationService | OAuthClient, создается через OAuthClient.create(...). Один экземпляр на конфигурацию в процессе. |
| Состояние сессии | AuthState (Android), OIDAuthState (iOS), объекты ответов (Flutter) | Не отдается. Наблюдайте за client.sessionState (Kotlin, Flow) или client.sessionStates() (Swift AsyncStream, Dart Stream) либо запросите текущее значение. Семь состояний, они перечислены на странице Состояния сессии и ошибки. |
| Сохранение состояния | Вы сами: AuthState.jsonSerializeString() в SharedPreferences (README Android); архив OIDAuthState (README iOS называет его единственным объектом для сериализации); сохранение ответа (Flutter) | SDK: refresh token и ID token лежат в хранилище, обернутом ключом из Keystore или Keychain, и недоступны вашему коду. API экспорта и импорта нет. |
| Свежие токены | Android authState.performActionWithFreshTokens(service, action) с execute(accessToken, idToken, ex); iOS authState.performAction { accessToken, idToken, error in }; Flutter token(TokenRequest(... refreshToken ...)) | client.accessToken() при необходимости сначала обновляет токен, причем в полете один refresh при любом числе вызывающих. Или поручите клиенту подставлять токен: client.interceptor(origins), client.authenticatedClient(origins) или client.call(request) (Kotlin), client.authenticatedSession(origins:) (Swift). Flutter: только accessToken(). |
| Принудительное обновление | AppAuth обновляет токен, когда он истекает | client.refresh() один раз расходует сохраненный refresh token прямо сейчас. |
| ID token | Его несет AuthState; Android проверяет несколько claim, iOS отдает OIDIDToken | client.claims (типизированный IdTokenClaims), сам токен не отдается. userInfo() — отдельный вызов. |
| Завершение сессии | Android EndSessionRequest.Builder(...).setIdTokenHint(...).setPostLogoutRedirectUri(...) и performEndSessionRequest; iOS OIDEndSessionRequest; Flutter endSession(EndSessionRequest(idTokenHint, postLogoutRedirectUrl)) | client.logout(LogoutOptions) отзывает refresh token, если провайдер объявляет отзыв, завершает сессию провайдера в системном браузере, если он объявляет end-session-эндпоинт, и стирает локальное состояние. postLogoutRedirectUri задается в конфигурации. Возвращает LogoutOutcome(sessionEnded, tokenRevoked). |
| Redirect, Android | Плейсхолдер манифеста appAuthRedirectScheme или intent filter на net.openid.appauth.RedirectUriReceiverActivity | Intent filter на versola.sdk.browser.VersolaRedirectActivity, лучше всего с проверенным App Link. В режиме Auth Tab браузер возвращает redirect прямо SDK. |
| Redirect, iOS | application(_:open:options:) в app delegate вызывает resumeExternalUserAgentFlow(...) у сохраненного currentAuthorizationFlow | В обычном случае ничего: redirect возвращает ASWebAuthenticationSession. Для Universal Link, пришедшей при холодном старте приложения, вызовите await client.handleRedirect(url). |
| Redirect, Flutter | Плейсхолдер или фильтр на Android; CFBundleURLSchemes в Info.plist на iOS | Та же нативная настройка, что в строках про Android и iOS, в приложении-хосте. |
| Выбор браузера | Android BrowserAllowList, BrowserDenyList, VersionedBrowserMatcher; iOS OIDExternalUserAgentIOSCustomBrowser | Нет. SDK берет Auth Tab, если его поддерживает провайдер Custom Tabs на устройстве, иначе Custom Tab, в последнюю очередь любой браузер. На iOS — ASWebAuthenticationSession. |
| Эфемерная сессия | Flutter preferEphemeralSession | LoginOptions.ephemeralBrowser, по возможности. По умолчанию выключена, потому что ломает passkey и single sign-on. |
| Свой HTTP | Android AppAuthConfiguration с setConnectionBuilder; iOS OIDURLSessionProvider | OAuthConfiguration.httpClient (OkHttp) или urlSession (Swift). Перенаправления в собственных запросах SDK выключены независимо от вашего клиента. |
| Отмена и ошибки | Android AuthorizationException; коды NSError на iOS; Flutter FlutterAppAuthUserCancelledException и FlutterAppAuthPlatformException | Четыре категории: Recoverable, Reauthenticate, StepUp, Configuration (см. Состояния сессии и ошибки). Отмена пользователем — это Recoverable(UserCancelled). |
Что вы перестаете делать?
- Хранить
AuthState. Ни сериализации, ни кода для токенов вSharedPreferencesили Keychain, ни собственногоAuthStateManager. После перезапускаcreate()без сетевых запросов восстанавливаетAuthenticated, а если приложение убили посреди браузера —Authorizing. - Координировать обновление. Параллельные вызывающие сливаются в один refresh. Новый refresh token сохраняется раньше, чем забывается старый, и дважды не тратится.
invalid_grant— этоReauthenticate(GrantRejected), а не повод для повтора. - Возиться с PKCE,
stateиnonce. SDK генерирует их из защищенного источника случайных чисел платформы, сохраняет до открытия браузера и проверяет при возврате. Вы их не передаете, не храните и не сравниваете. - Проверять ID token. Проверка идет до того, как грант сохраняется (см. Что в поведении отличается от AppAuth?). Отдельная JWT-библиотека для нее не нужна.
- Передавать redirect библиотеке. На Android его принимает activity SDK или возвращает Auth Tab. На iOS его возвращает системная сессия.
handleRedirectнужен только для Universal Link при холодном старте. - Подставлять токены. Interceptor или аутентифицированная сессия добавляет
Authorizationк запросам на названные вами origin, один раз обновляет токен при 401 и один раз повторяет запрос. - Писать циклы повторов вокруг 500, 503 и 429. SDK повторяет запросы, которые безопасно повторять, и ждет столько, сколько просит провайдер (см. Как меняется обработка ошибок?).
Что в поведении отличается от AppAuth?
В местах ниже SDK проверяет больше, чем AppAuth. Поэтому провайдер, который работал с AppAuth, при первом запуске может дать ошибку Configuration: она показывает, что нужно исправить у провайдера или в регистрации клиента.
| Область | AppAuth | Этот SDK |
|---|---|---|
| ID token | Android IdToken.validate проверяет iss, aud/azp, exp, iat и для code grant nonce; в его исходниках сказано, что он полагается на TLS и подпись не проверяет. iOS OIDIDToken разбирает токен и, по документации, подпись не проверяет. | Подпись проверяется по jwks_uri из закрытого набора асимметричных алгоритмов (none и HMAC отклоняются). Затем, до сохранения чего-либо, проверяются iss, aud/azp, exp, iat, nonce, auth_time при заданном maxAge и at_hash. Отказ — это Configuration(IdTokenRejected(reason)). Те же правила действуют для ID token в ответе на refresh. |
iss | Android сверяет издателя ID token с discovery, если документ discovery есть. | issuer в документе discovery должен совпасть с настроенным (один завершающий слеш не учитывается). iss в ID token должен совпасть с найденным значением байт в байт. iss в redirect проверяется (RFC 9207), если он есть, и обязателен, если его объявляет discovery. Издатель-шаблон не поддерживается. |
| PKCE | README iOS: PKCE используется, если его поддерживает сервер. | Всегда S256, никогда не plain и не выключен. Провайдер, у которого в code_challenge_methods_supported нет S256, дает ProviderUnsupported("S256"). |
| Тип клиента | Секрет клиента можно передать (Android ClientSecretBasic, ClientSecretPost; в примере README iOS передается clientSecret). | Только публичные клиенты. clientSecret нет ни в одном типе конфигурации. |
| Браузер | Системный браузер; WebView не поддерживается. | Только системный браузер, а web view в публичном API невыразим. |
| Хранение токенов | Ваш код. | SDK: Keystore (Android) или Keychain (iOS, AfterFirstUnlockThisDeviceOnly, без синхронизации). На Android файлы лежат в noBackupFilesDir, поэтому резервная копия не воспроизведет их на другом устройстве. |
| Ошибки | Типы исключений и коды платформы. | Четыре категории. error_description прикладывается для логов, по нему никогда не сопоставляют. |
| Сбои сервера и повторы | Ваш код. | SDK повторяет безопасные запросы при 408, 429, 500, 502, 503 и 504 и учитывает Retry-After при 429 и 503. См. Ошибки сервера, 429 и Retry-After. |
| Открытие браузера | Вызывает приложение. | Так же, и SDK никогда не открывает его сам: Reauthenticate — диагноз, а когда запускать login(), решаете вы. login() в состоянии Authorizing отклоняется с InvalidConfiguration. |
| Redirect при холодном старте | Нет. | Redirect, дошедший до холодно запущенного процесса, завершает отложенный запрос (он хранится до 10 минут) и публикует Authenticated в sessionState. |
| Токены и origin | Что ваш код сделает с токеном, то и будет. | Аутентифицированный клиент прикрепляет токен только к protectedOrigins из вашей конфигурации. Любой другой запрос проходит нетронутым. |
| Схема issuer | Android разрешает setSkipIssuerHttpsCheck(true) для тестов. | Только https, а http допустим лишь на loopback-хосте. Флага нет. |
| Минимальная платформа | README AppAuth-Android: API 16+, App Links с API 23+. README AppAuth-iOS: iOS 15+, HTTPS-redirect с iOS 17.4+. | minSdk 26 на Android; iOS 17.4 для пакета. |
| Redirect URI | README рекомендует для Android пользовательскую схему; HTTPS поддерживается. | Предпочтительна проверенная https-ссылка. Приватная схема работает и пишет предупреждение при каждом create(). Открытый http не на loopback отклоняется. |
| Данные во Flutter | flutter_appauth возвращает в Dart access, refresh и ID token. | Dart получает от accessToken() короткоживущий access token и больше ничего, похожего на учетные данные. |
Что умеет AppAuth, но не умеет SDK?
Versola OAuth SDK — библиотека для публичных клиентов, потока authorization code и системного браузера. У возможностей AppAuth, которые в эту схему не входят, аналогов нет: публичных символов для них нет ни на одной из трех платформ. Сверьте список со своей текущей настройкой до начала работы.
- Секреты клиентов и конфиденциальные клиенты. Поля для секрета в конфигурации нет, так задумано (RFC 8252, раздел 8.5). Если клиент у провайдера использует секрет, зарегистрируйте для приложения отдельный публичный клиент без секрета. Провайдер, который требует секрет от нативного приложения, не поддерживается.
- Другая аутентификация клиента на token endpoint (в AppAuth-Android есть тип
ClientAuthentication). Запрос токена несет толькоclient_id. - Динамическая регистрация клиента (
RegistrationRequest/OIDRegistrationRequestв AppAuth). Аналога нет. - Встроенные user agent. Web view передать нельзя.
- Выбор браузера. Нет ни списка разрешенных, ни списка запрещенных, ни своего браузера, ни своего intent для Custom Tabs.
- Discovery по своему URL (
fetchFromUrl). Используйте явные эндпоинты. - Разделение потока. У Flutter
authorizeс последующимtoken(code)и у ручногоperformTokenRequestаналога нет.login()атомарен, и приложению ни код авторизации, ни verifier, ни nonce не отдаются. - Чтение и подстановка refresh token. Ваш бэкенд не получит refresh token приложения, а у
token(TokenRequest(... refreshToken: ...))аналога нет.refresh()расходует сохраненный токен. - Сырой ID token.
claimsотдает типизированные claims проверенного токена, а сам токен хранится зашифрованным только дляid_token_hint. Если бэкенду нужен именно ID token, этот SDK его дать не может. - Отключение проверок.
setNonce(null)иsetSkipIssuerHttpsCheck(true)(AppAuth-Android), issuer не поhttps, отсутствующийissи непроверенный ID token отклоняются. Провайдер, которому нужно что-то из этого, с SDK работать не сможет. - Несколько аккаунтов одновременно. Одна сессия на конфигурацию. Конфигурация с другим issuer или
clientId— отдельный клиент со своим хранилищем. - macOS, tvOS и десктоп. AppAuth-iOS документирует поддержку macOS и tvOS. SDK рассчитан на Android и iOS, а Flutter — на эти две платформы.
- Параметр в запросе токена.
extraParametersуходит в запрос авторизации. Запрос токена несет то, что собирает SDK, иresource, если вы задалиresources. - Свой user agent и перехват запросов за пределами HTTP-клиента. Единственная точка вмешательства —
httpClient/urlSession.
Что будет с текущими пользователями и токенами?
Существующие refresh token импортировать нельзя, поэтому каждый пользователь входит один раз после обновления. Ни одно публичное объявление трех платформ (дамп API Android, public-поверхность OAuthClient в Swift, экспорты versola_oauth в Dart) не принимает refresh token, ID token или сериализованное состояние, и в типах конфигурации нет поля для них. Хранилища, где лежат эти данные, — внутренняя обвязка за маркером @VersolaInternalApi на Android и package-доступом на iOS. Первый create() ничего не находит и сообщает Unauthenticated.
Планируйте выкатку с учетом этого:
- Выпустите SDK в обновлении приложения и считайте первый запуск входом с нуля. Покажите свой экран входа. В большинстве случаев
login()завершается по уже существующей сессии пользователя у провайдера: пароль вводить не нужно, если браузер еще авторизован. - Удалите старое состояние. После первого успешного
login()сотрите сохраненное состояние AppAuth: JSONAuthStateвSharedPreferencesили там, куда его положил ваш менеджер, архивированныйOIDAuthStateв Keychain или в defaults и токены, которые вернулflutter_appauth, если вы их сохраняли (например, в плагине secure storage). Забытый refresh token — это учетные данные, которыми никто не пользуется и которые никто не охраняет. - При желании отзовите старый refresh token у провайдера до удаления, если у него есть эндпоинт отзыва (RFC 7009:
client_idиtoken, для публичного клиента без секрета). SDK сделать это за вас не может, потому что этот токен он никогда не видит. - Не снимайте старый redirect URI с регистрации, пока в ходу старые версии приложения. Если вы переходите с пользовательской схемы на
httpsApp Link или Universal Link, добавьте новый URI. Старый не заменяйте, пока старые версии не исчезнут. - Если старый клиент использовал секрет, SDK нужен другой клиент у провайдера, зарегистрированный как публичный. Зарегистрируйте его рядом со старым и переведите приложение на него.
clientIdи redirect URI могут остаться прежними. SDK хранит данные под ключом из issuer иclientIdи хранилище AppAuth не читает.
Как меняется обработка ошибок?
AppAuth сообщает о сбое типом исключения или кодом платформы (AuthorizationException на Android, NSError на iOS, исключения FlutterAppAuth во Flutter), а повторы оставляет приложению. Versola OAuth SDK относит каждый сбой к одной из четырех категорий, и категория подсказывает приложению, что делать:
| Категория | Что делает приложение |
|---|---|
Recoverable | Предложить повтор. Отмена пользователем — Recoverable(UserCancelled). |
Reauthenticate | Отправить пользователя на login(). Отклоненный refresh token (invalid_grant) — Reauthenticate(GrantRejected). |
StepUp | Вызвать stepUp. |
Configuration | Исправить развертывание. В продакшене не перехватывать молча. |
Когда провайдер отвечает 500, 503 или 429, SDK повторяет запросы, которые безопасно повторять, ждет столько, сколько просит Retry-After, и только потом возвращает одну ошибку Recoverable. Своих циклов повторов для этих случаев приложению не нужно. Правила и код для каждой платформы — на странице Ошибки сервера, 429 и Retry-After.
Как перейти с AppAuth-Android?
Часть про AppAuth ниже сокращена из примеров README. Часть про SDK — целевой API.
До (AppAuth-Android)
// build.gradle: manifestPlaceholders = ['appAuthRedirectScheme': 'com.example.app']
AuthorizationServiceConfiguration.fetchFromIssuer(Uri.parse("https://id.example.com")) { config, ex ->
val request = AuthorizationRequest.Builder(
config!!, "example-app", ResponseTypeValues.CODE, Uri.parse("com.example.app:/oauth"),
).setScope("openid profile").build()
// performAuthorizationRequest(request, completionIntent, cancelIntent), then in the
// completion activity: AuthorizationResponse.fromIntent(intent), AuthState.update(...),
// authService.performTokenRequest(resp.createTokenExchangeRequest(), callback)
}
// Later, with state restored from AuthState.jsonDeserialize(json):
authState.performActionWithFreshTokens(authService) { accessToken, idToken, ex ->
// write the updated AuthState back, then call the API with accessToken
}
После (Versola OAuth SDK)
-
Зависимость. Удалите
net.openid:appauth. Подключите SDK из Maven Central: в репозиториях должны бытьgoogle()иmavenCentral().minSdkстановится 26.dependencies { implementation("io.versola:oauth-android:<version>") } -
Манифест. Удалите плейсхолдер
appAuthRedirectSchemeи любой фильтр наnet.openid.appauth.RedirectUriReceiverActivity. Добавьте intent filter на activity SDK для вашего redirect URI, фрагмент есть на странице Android. Для проверенного App Link еще отдайтеassetlinks.jsonна хосте. -
Создайте клиент один раз, с тем же issuer, client id и scope. Используйте
httpsredirect URI, если его можно зарегистрировать у провайдера.val client = OAuthClient.create( context, OAuthConfiguration( issuer = "https://id.example.com", clientId = "example-app", redirectUri = "https://app.example.com/oauth", postLogoutRedirectUri = "https://app.example.com/signed-out", scopes = setOf("openid", "profile"), protectedOrigins = setOf("https://api.example.com"), ), ) -
Вход. Запрос, activity для redirect и обмен токена заменяются одним вызовом:
scope.launch { try { client.login() } catch (e: VersolaError) { // Recoverable, Reauthenticate, StepUp or Configuration } } -
Вызовы вашего API.
performActionWithFreshTokensи записьAuthStateобратно замените interceptor в своемOkHttpClientилиclient.call(request):val http = OkHttpClient.Builder() .addInterceptor(client.interceptor()) .build()Если нужно само значение,
client.accessToken()возвращаетAccessToken. Это запасной выход: токен, попавший в ваш код, можно по ошибке залогировать. -
Наблюдайте за сессией вместо чтения своего сохраненного состояния: подпишитесь на
client.sessionState. -
Выход —
client.logout(). Он возвращаетLogoutOutcome(sessionEnded, tokenRevoked). -
Удалите старое хранилище
AuthState(см. Что будет с текущими пользователями и токенами?).
Как перейти с AppAuth-iOS?
До (AppAuth-iOS)
OIDAuthorizationService.discoverConfiguration(forIssuer: issuer) { configuration, error in
guard let configuration else { return }
let request = OIDAuthorizationRequest(
configuration: configuration, clientId: clientID,
clientSecret: clientSecret, // the README's sample passes a secret; a public client has none
scopes: [OIDScopeOpenID, OIDScopeProfile], redirectURL: redirectURI,
responseType: OIDResponseTypeCode, additionalParameters: nil)
appDelegate.currentAuthorizationFlow =
OIDAuthState.authState(byPresenting: request, presenting: self) { authState, error in
// keep authState
}
}
func application(_ app: UIApplication, open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
if let flow = self.currentAuthorizationFlow {
try? flow.resumeExternalUserAgentFlow(url) // abridged: the README also handles the error
self.currentAuthorizationFlow = nil
return true
}
return false
}
authState?.performAction { accessToken, idToken, error in
// call the API with accessToken
}
После (Versola OAuth SDK)
-
Зависимость. Уберите AppAuth из пакета или проекта Xcode. Добавьте публичный репозиторий SDK и продукт
VersolaOAuth, затемimport OAuthClient. Пакету нужна iOS 17.4.dependencies: [ .package(url: "https://github.com/versolauth/versola-sdk", from: "<version>"), ], targets: [ .target( name: "App", dependencies: [.product(name: "VersolaOAuth", package: "versola-sdk")] ), ] -
Entitlements для
httpsredirect: Associated Domains для хоста и файлapple-app-site-association, как описано на странице iOS. Приватной схеме все это не нужно, но она пишет предупреждение. -
Создайте клиент (асинхронно, один раз).
presentationAnchor— замыкание, которое возвращает окно, из которого показывается лист браузера:let configuration = try OAuthConfiguration( issuer: "https://id.example.com", clientId: "example-app", redirectUri: "https://app.example.com/oauth", postLogoutRedirectUri: "https://app.example.com/signed-out", scopes: ["openid", "profile"], protectedOrigins: ["https://api.example.com"] ) let client = try await OAuthClient.create( configuration: configuration, presentationAnchor: { window } // a UIWindow the app owns ) -
Вход. Удалите
currentAuthorizationFlowи обвязкуapplication(_:open:options:):do { try await client.login() } catch let error as OAuthError { // .recoverable, .reauthenticate, .stepUp or .configuration }Оставьте один обработчик, только для Universal Link, которые приходят при холодном старте приложения:
.onOpenURL { url in Task { _ = await client.handleRedirect(url) } } -
Вызовы вашего API идут через аутентифицированную сессию вместо
performAction:let session = try client.authenticatedSession() let (data, response) = try await session.data(for: URLRequest(url: apiURL))Или вызовите
try await client.accessToken()там, где нужно само значение. -
Наблюдайте через
for await state in await client.sessionStates() { ... }. -
Выход —
await client.logout(), он возвращаетLogoutOutcome. -
Удалите архивированный
OIDAuthState(см. Что будет с текущими пользователями и токенами?).
Как перейти с flutter_appauth?
До (flutter_appauth)
final appAuth = FlutterAppAuth();
final result = await appAuth.authorizeAndExchangeCode(
AuthorizationTokenRequest(
'<client_id>',
'<redirect_url>',
issuer: '<issuer>',
scopes: ['openid', 'profile', 'offline_access'],
),
);
// You store result's tokens, and later refresh with
// appAuth.token(TokenRequest('<client_id>', '<redirect_url>', refreshToken: ..., ...)).
await appAuth.endSession(EndSessionRequest(
idTokenHint: '<idToken>',
postLogoutRedirectUrl: '<postLogoutRedirectUrl>',
issuer: '<issuer>',
));
После (versola_oauth)
-
Зависимость. Замените
flutter_appauthвpubspec.yamlнаversola_oauth. Установка описана на странице Flutter. Уберите собственную настройку плагина:appAuthRedirectSchemeна Android и пользовательскую схемуCFBundleURLTypesна iOS, если только вы не оставляете redirect на пользовательскую схему. -
Нативная настройка в приложении-хосте. Android: intent filter для
VersolaRedirectActivity. iOS: iOS 17.4 или новее, Associated Domains дляhttpsredirect иVersolaOAuthPlugin.presentationAnchor, если в приложении своя работа с окнами. См. страницу Flutter. -
Создание и использование:
final client = await OAuthClient.create(const OAuthConfiguration( issuer: 'https://id.example.com', clientId: 'example-app', redirectUri: 'https://app.example.com/oauth', postLogoutRedirectUri: 'https://app.example.com/signed-out', scopes: {'openid', 'profile'}, )); client.sessionStates().listen((state) => print(state)); try { await client.login(); final token = await client.accessToken(); // the only credential that crosses the bridge } on StepUpError catch (e) { await client.stepUp(e); } on ReauthenticateError { // sign in again } on RecoverableError { // retry, or ask the user again } on ConfigurationError { // a deployment mistake } -
Уберите свое хранилище токенов. В Dart не остается ничего, похожего на учетные данные, поэтому удалите код, который хранит и обновляет токены.
accessToken()обновляет токен нативно. -
Выход —
client.logout(). Он возвращаетLogoutOutcome. -
Ошибки.
FlutterAppAuthUserCancelledExceptionпревращается вRecoverableError, а платформенное исключение — в подходящую категорию.
Аутентифицированного HTTP-клиента в Dart нет. Вызывайте свой API со значением, которое возвращает accessToken(), и запрашивайте его заново для каждого запроса или пачки запросов, а не кэшируйте.
Чек-лист миграции
- Клиент у провайдера публичный, у него включен PKCE
S256, и нигде не используется секрет. - Настроенный
issuerравен членуissuerв документе discovery. -
offline_accessуказан вscopes, если провайдеру он нужен для refresh token (см. Провайдеры). - Redirect URI зарегистрирован у провайдера точно так, как настроен, а для
httpsотдаютсяassetlinks.jsonили файлapple-app-site-associationприложения. -
postLogoutRedirectUriзарегистрирован у провайдера. - Никакой код не читает, не пишет и не отправляет refresh token или сырой ID token.
- Старый
AuthState(или архивированныйOIDAuthState, или хранилище токенов в Dart) удален после первого успешногоlogin(). - Ваш экран входа обрабатывает четыре категории ошибок, а ошибка
Configurationв продакшене не перехватывается молча.
Источники
Факты об AppAuth на этой странице прочитаны 2026-10-30. Часть про SDK основана на собственной спецификации API SDK и его публичных объявлениях.
- README AppAuth-Android: https://github.com/openid/AppAuth-Android. Использован для конфигурации и discovery, запроса и intent, redirect (
appAuthRedirectScheme,RedirectUriReceiverActivity), храненияAuthStateиperformActionWithFreshTokens,EndSessionRequest, секретов клиентов и динамической регистрации, списков разрешенных и запрещенных браузеров, утверждения про WebView, PKCE, уровней API,setNonce(null)иsetSkipIssuerHttpsCheck. - AppAuth-Android
IdToken.java: проверки ID token и утверждение, что подпись не проверяется. - README AppAuth-iOS: https://github.com/openid/AppAuth-iOS и https://raw.githubusercontent.com/openid/AppAuth-iOS/master/README.md. Использован для discovery,
OIDAuthorizationRequest,OIDAuthState.authState(byPresenting:presenting:),resumeExternalUserAgentFlow,performAction, user agent, WebView, PKCE, HTTPS redirect и требованияwebcredentials, минимальных версий ОС. - Индекс API AppAuth-iOS, список классов (end session, регистрация, запрос токена): https://openid.github.io/AppAuth-iOS/docs/latest/annotated.html, и
OIDIDToken: https://openid.github.io/AppAuth-iOS/docs/latest/interface_o_i_d_i_d_token.html. - README
flutter_appauth: https://github.com/MaikuB/flutter_appauth и https://raw.githubusercontent.com/MaikuB/flutter_appauth/master/flutter_appauth/README.md.
Что дальше
- Android, iOS и Flutter: настройка SDK на каждой платформе.
- Состояния сессии и ошибки: четыре категории ошибок и семь состояний сессии, которые обрабатывает ваш экран входа.
- Ошибки сервера, 429 и Retry-After: что SDK повторяет сам и что делает с остальным приложение.
- Провайдеры: redirect URI, scope и выход для вашего провайдера, например Keycloak.