Миграция с AppAuth на Versola OAuth SDK — Versola Docs
/docs
0.1.0versola.ioGitHub

Миграция с AppAuth на Versola OAuth SDK

Замена AppAuth-Android, AppAuth-iOS или flutter_appauth на Versola OAuth SDK: соответствие понятий, изменения в поведении и код до и после для каждой платформы.

Чтобы перейти с 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
Конфигурация провайдера через discoveryAndroid AuthorizationServiceConfiguration.fetchFromIssuer; iOS OIDAuthorizationService.discoverConfiguration(forIssuer:); Flutter issuer или discoveryUrlOAuthConfiguration(issuer = ...). Discovery запускается лениво при первой операции, кэшируется на диске и перечитывается через 24 часа или после сбоя, который мог вызвать смену эндпоинта.
Конфигурация провайдера через эндпоинтыAndroid new AuthorizationServiceConfiguration(authorizationEndpoint, tokenEndpoint); iOS OIDServiceConfiguration(authorizationEndpoint:tokenEndpoint:); Flutter serviceConfigurationOAuthConfiguration(endpoints = ProviderEndpoints(...)). Обязательны issuer, authorizationEndpoint, tokenEndpoint и jwksUri: ID token, который нельзя проверить, отклоняется. Несовместимо с issuer.
Discovery по нестандартному URLAndroid 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 authorizeAndExchangeCodeclient.login(options): один suspend-вызов, который запускает браузер, проверяет redirect, обменивает код и проверяет ID token.
Объект сервисаAndroid AuthorizationService; iOS OIDAuthorizationServiceOAuthClient, создается через 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 отдает OIDIDTokenclient.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.RedirectUriReceiverActivityIntent filter на versola.sdk.browser.VersolaRedirectActivity, лучше всего с проверенным App Link. В режиме Auth Tab браузер возвращает redirect прямо SDK.
Redirect, iOSapplication(_: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 preferEphemeralSessionLoginOptions.ephemeralBrowser, по возможности. По умолчанию выключена, потому что ломает passkey и single sign-on.
Свой HTTPAndroid AppAuthConfiguration с setConnectionBuilder; iOS OIDURLSessionProviderOAuthConfiguration.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 tokenAndroid 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.
issAndroid сверяет издателя ID token с discovery, если документ discovery есть.issuer в документе discovery должен совпасть с настроенным (один завершающий слеш не учитывается). iss в ID token должен совпасть с найденным значением байт в байт. iss в redirect проверяется (RFC 9207), если он есть, и обязателен, если его объявляет discovery. Издатель-шаблон не поддерживается.
PKCEREADME 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 из вашей конфигурации. Любой другой запрос проходит нетронутым.
Схема issuerAndroid разрешает 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 URIREADME рекомендует для Android пользовательскую схему; HTTPS поддерживается.Предпочтительна проверенная https-ссылка. Приватная схема работает и пишет предупреждение при каждом create(). Открытый http не на loopback отклоняется.
Данные во Flutterflutter_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.

Планируйте выкатку с учетом этого:

  1. Выпустите SDK в обновлении приложения и считайте первый запуск входом с нуля. Покажите свой экран входа. В большинстве случаев login() завершается по уже существующей сессии пользователя у провайдера: пароль вводить не нужно, если браузер еще авторизован.
  2. Удалите старое состояние. После первого успешного login() сотрите сохраненное состояние AppAuth: JSON AuthState в SharedPreferences или там, куда его положил ваш менеджер, архивированный OIDAuthState в Keychain или в defaults и токены, которые вернул flutter_appauth, если вы их сохраняли (например, в плагине secure storage). Забытый refresh token — это учетные данные, которыми никто не пользуется и которые никто не охраняет.
  3. При желании отзовите старый refresh token у провайдера до удаления, если у него есть эндпоинт отзыва (RFC 7009: client_id и token, для публичного клиента без секрета). SDK сделать это за вас не может, потому что этот токен он никогда не видит.
  4. Не снимайте старый redirect URI с регистрации, пока в ходу старые версии приложения. Если вы переходите с пользовательской схемы на https App Link или Universal Link, добавьте новый URI. Старый не заменяйте, пока старые версии не исчезнут.
  5. Если старый клиент использовал секрет, SDK нужен другой клиент у провайдера, зарегистрированный как публичный. Зарегистрируйте его рядом со старым и переведите приложение на него.
  6. 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)

  1. Зависимость. Удалите net.openid:appauth. Подключите SDK из Maven Central: в репозиториях должны быть google() и mavenCentral(). minSdk становится 26.

    dependencies {
        implementation("io.versola:oauth-android:<version>")
    }
  2. Манифест. Удалите плейсхолдер appAuthRedirectScheme и любой фильтр на net.openid.appauth.RedirectUriReceiverActivity. Добавьте intent filter на activity SDK для вашего redirect URI, фрагмент есть на странице Android. Для проверенного App Link еще отдайте assetlinks.json на хосте.

  3. Создайте клиент один раз, с тем же issuer, client id и scope. Используйте https redirect 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"),
        ),
    )
  4. Вход. Запрос, activity для redirect и обмен токена заменяются одним вызовом:

    scope.launch {
        try {
            client.login()
        } catch (e: VersolaError) {
            // Recoverable, Reauthenticate, StepUp or Configuration
        }
    }
  5. Вызовы вашего API. performActionWithFreshTokens и запись AuthState обратно замените interceptor в своем OkHttpClient или client.call(request):

    val http = OkHttpClient.Builder()
        .addInterceptor(client.interceptor())
        .build()

    Если нужно само значение, client.accessToken() возвращает AccessToken. Это запасной выход: токен, попавший в ваш код, можно по ошибке залогировать.

  6. Наблюдайте за сессией вместо чтения своего сохраненного состояния: подпишитесь на client.sessionState.

  7. Выход — client.logout(). Он возвращает LogoutOutcome(sessionEnded, tokenRevoked).

  8. Удалите старое хранилище 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)

  1. Зависимость. Уберите 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")]
        ),
    ]
  2. Entitlements для https redirect: Associated Domains для хоста и файл apple-app-site-association, как описано на странице iOS. Приватной схеме все это не нужно, но она пишет предупреждение.

  3. Создайте клиент (асинхронно, один раз). 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
    )
  4. Вход. Удалите 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) }
    }
  5. Вызовы вашего API идут через аутентифицированную сессию вместо performAction:

    let session = try client.authenticatedSession()
    let (data, response) = try await session.data(for: URLRequest(url: apiURL))

    Или вызовите try await client.accessToken() там, где нужно само значение.

  6. Наблюдайте через for await state in await client.sessionStates() { ... }.

  7. Выход — await client.logout(), он возвращает LogoutOutcome.

  8. Удалите архивированный 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)

  1. Зависимость. Замените flutter_appauth в pubspec.yaml на versola_oauth. Установка описана на странице Flutter. Уберите собственную настройку плагина: appAuthRedirectScheme на Android и пользовательскую схему CFBundleURLTypes на iOS, если только вы не оставляете redirect на пользовательскую схему.

  2. Нативная настройка в приложении-хосте. Android: intent filter для VersolaRedirectActivity. iOS: iOS 17.4 или новее, Associated Domains для https redirect и VersolaOAuthPlugin.presentationAnchor, если в приложении своя работа с окнами. См. страницу Flutter.

  3. Создание и использование:

    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
    }
  4. Уберите свое хранилище токенов. В Dart не остается ничего, похожего на учетные данные, поэтому удалите код, который хранит и обновляет токены. accessToken() обновляет токен нативно.

  5. Выход — client.logout(). Он возвращает LogoutOutcome.

  6. Ошибки. 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 и его публичных объявлениях.

Что дальше