Migrate from AppAuth to the Versola OAuth SDK — Versola Docs
/docs
0.1.0versola.ioGitHub

Migrate from AppAuth to the Versola OAuth SDK

Replace AppAuth-Android, AppAuth-iOS or flutter_appauth with the Versola OAuth SDK: concept mapping, behaviour changes and before and after code per platform.

To migrate from AppAuth-Android, AppAuth-iOS or flutter_appauth (which wraps both) to the Versola OAuth SDK, you replace the request, redirect and token-storage code with one OAuthClient and one login() call. This page maps AppAuth concepts to the SDK, lists what you stop doing and what behaves differently, and shows before and after code for Android, iOS and Flutter. The condition: the SDK serves public clients that sign in through the system browser, so read What does AppAuth do that the SDK does not do? first.

Prerequisites

  • An app that signs in with AppAuth-Android, AppAuth-iOS or flutter_appauth today.
  • A provider client you can register as public: no secret, PKCE S256.
  • The provider’s page in Providers, for example Keycloak, to check scopes, redirect URIs and sign-out.

What changes when you replace AppAuth?

  • Your app stops holding authentication state. There is no AuthState to serialize and no refresh token in your hands. OAuthClient owns storage, refresh and sign-out, and you observe a session.
  • Sign-in is one call, login(). PKCE, state, nonce, redirect validation, the code exchange and ID-token validation happen inside it and cannot be switched off.
  • The Versola OAuth SDK protects every stored token with the Android Keystore or the iOS Keychain.
  • Users sign in once after the update, because existing refresh tokens cannot be imported (see What happens to existing users and tokens?).
  • Provider failures reach your app as four error categories, and the SDK repeats safe requests itself (see How does error handling change?).

How do AppAuth concepts map to the SDK?

Kotlin names are from versola.oauth (and versola.oauth.core for the error and state types), Swift names from the module OAuthClient (package product VersolaOAuth), Dart names from package:versola_oauth/versola_oauth.dart.

AppAuth conceptAppAuth namesThis SDK
Provider configuration by discoveryAndroid AuthorizationServiceConfiguration.fetchFromIssuer; iOS OIDAuthorizationService.discoverConfiguration(forIssuer:); Flutter issuer or discoveryUrlOAuthConfiguration(issuer = ...). Discovery runs lazily on the first operation, is cached on disk and is refetched after 24 hours or on a failure that a changed endpoint could explain.
Provider configuration by endpointsAndroid new AuthorizationServiceConfiguration(authorizationEndpoint, tokenEndpoint); iOS OIDServiceConfiguration(authorizationEndpoint:tokenEndpoint:); Flutter serviceConfigurationOAuthConfiguration(endpoints = ProviderEndpoints(...)). issuer, authorizationEndpoint, tokenEndpoint and jwksUri are required, because an ID token that cannot be verified is refused. Exclusive with issuer.
Discovery at a non-standard URLAndroid fetchFromUrlNo equivalent. Give explicit endpoints.
The requestAndroid AuthorizationRequest.Builder; iOS OIDAuthorizationRequest; Flutter AuthorizationTokenRequestNot an object you build. clientId, redirectUri and scopes live in OAuthConfiguration; per-login choices are LoginOptions (scopes, prompt, loginHint, uiLocales, acrValues, maxAge, resources, extraParameters, ephemeralBrowser).
Running the flowAndroid AuthorizationService.performAuthorizationRequest or getAuthorizationRequestIntent, then performTokenRequest; iOS OIDAuthState.authState(byPresenting:presenting:); Flutter authorizeAndExchangeCodeclient.login(options): one suspending call that runs the browser, validates the redirect, exchanges the code and validates the ID token.
The service objectAndroid AuthorizationService; iOS OIDAuthorizationServiceOAuthClient, from OAuthClient.create(...). One instance per configuration per process.
Session stateAuthState (Android), OIDAuthState (iOS), the response objects (Flutter)Not exposed. Observe client.sessionState (Kotlin, a Flow) or client.sessionStates() (Swift AsyncStream, Dart Stream), or ask for the current value. Seven states, listed in Session states and errors.
Persisting the stateYou: AuthState.jsonSerializeString() to SharedPreferences (Android README); archive OIDAuthState (the iOS README says it is the only object to serialize); store the response (Flutter)The SDK: the refresh token and the ID token sit in a store wrapped by the Keystore or the Keychain, never in a place you can read. No API exports or imports it.
Fresh tokensAndroid authState.performActionWithFreshTokens(service, action) with execute(accessToken, idToken, ex); iOS authState.performAction { accessToken, idToken, error in }; Flutter token(TokenRequest(... refreshToken ...))client.accessToken() refreshes first if needed, with one refresh in flight however many callers. Or let the client attach the token: client.interceptor(origins), client.authenticatedClient(origins) or client.call(request) (Kotlin), client.authenticatedSession(origins:) (Swift). Flutter: accessToken() only.
Forcing a refreshAppAuth refreshes when the token is expiringclient.refresh() spends the stored refresh token once, now.
ID tokenAuthState carries it; Android validates a few claims, iOS exposes OIDIDTokenclient.claims (typed IdTokenClaims), never the raw token. userInfo() is separate.
End sessionAndroid EndSessionRequest.Builder(...).setIdTokenHint(...).setPostLogoutRedirectUri(...) and performEndSessionRequest; iOS OIDEndSessionRequest; Flutter endSession(EndSessionRequest(idTokenHint, postLogoutRedirectUrl))client.logout(LogoutOptions) revokes the refresh token when the provider advertises revocation, ends the provider session in the system browser when it advertises an end-session endpoint, and wipes local state. postLogoutRedirectUri is configuration. It returns LogoutOutcome(sessionEnded, tokenRevoked).
Redirect, AndroidManifest placeholder appAuthRedirectScheme, or an intent filter on net.openid.appauth.RedirectUriReceiverActivityAn intent filter on versola.sdk.browser.VersolaRedirectActivity, preferably a verified App Link. Under an Auth Tab the browser returns the redirect to the SDK directly.
Redirect, iOSThe app delegate’s application(_:open:options:) calls resumeExternalUserAgentFlow(...) on a stored currentAuthorizationFlowNothing in the normal case: ASWebAuthenticationSession returns the redirect to the SDK. For a Universal Link received while the app was cold, call await client.handleRedirect(url).
Redirect, FlutterAndroid placeholder or filter; iOS CFBundleURLSchemes in Info.plistThe same native setup as the Android and iOS rows, in the host app.
Browser choiceAndroid BrowserAllowList, BrowserDenyList, VersionedBrowserMatcher; iOS OIDExternalUserAgentIOSCustomBrowserNone. The SDK picks an Auth Tab where the device’s Custom Tabs provider supports it, a Custom Tab otherwise and any browser last. iOS uses ASWebAuthenticationSession.
Ephemeral sessionFlutter preferEphemeralSessionLoginOptions.ephemeralBrowser, best effort. Off by default because it breaks passkeys and single sign-on.
Custom HTTPAndroid AppAuthConfiguration with setConnectionBuilder; iOS OIDURLSessionProviderOAuthConfiguration.httpClient (OkHttp) or urlSession (Swift). Redirects are switched off on the SDK’s own requests whatever your client does.
Cancellation and errorsAndroid AuthorizationException; iOS NSError codes; Flutter FlutterAppAuthUserCancelledException and FlutterAppAuthPlatformExceptionFour categories: Recoverable, Reauthenticate, StepUp, Configuration (see Session states and errors). A user cancel is Recoverable(UserCancelled).

What do you stop doing?

  • Persisting AuthState. No serialization, no SharedPreferences or Keychain code for tokens, no AuthStateManager of your own. After a restart create() restores Authenticated without network I/O, or Authorizing if the app was killed mid-browser.
  • Coordinating refresh. Concurrent callers coalesce onto one refresh. A rotated refresh token is stored before the old one is forgotten and is never spent twice. invalid_grant is Reauthenticate(GrantRejected), not a retry.
  • Handling PKCE, state and nonce. The SDK generates them from the platform’s secure random source, persists them before the browser opens and validates them on return. You don’t pass, store or compare them.
  • Validating the ID token. It happens before the grant is persisted (see What behaves differently from AppAuth?). You don’t need a separate JWT library for it.
  • Handing a redirect to the library. On Android the SDK’s activity receives it, or the Auth Tab returns it. On iOS the system session returns it. Only a cold-start Universal Link needs handleRedirect.
  • Attaching tokens. The interceptor or the authenticated session adds Authorization to requests for the origins you named, refreshes once on a 401 and replays once.
  • Writing retry loops around 500, 503 and 429. The SDK repeats the requests that are safe to repeat and waits as long as the provider asks (see How does error handling change?).

What behaves differently from AppAuth?

The SDK checks more than AppAuth does in the places below. A provider that worked with AppAuth can therefore fail with a Configuration error on the first run, which tells you what the provider or the client registration needs to fix.

AreaAppAuthThis SDK
ID tokenAndroid IdToken.validate checks iss, aud/azp, exp, iat and, for the code grant, the nonce; its source says it relies on TLS instead of verifying the signature. iOS OIDIDToken parses the token and, per its documentation, does not verify the signature.The signature is verified against jwks_uri with a closed set of asymmetric algorithms (none and HMAC refused). Then iss, aud/azp, exp, iat, nonce, auth_time under maxAge and at_hash are checked, before anything is persisted. A refusal is Configuration(IdTokenRejected(reason)). The same rules apply to an ID token on a refresh response.
issAndroid checks the ID token’s issuer against discovery when a discovery document is present.The discovery document’s issuer must equal the configured one (one trailing slash is ignored). The ID token’s iss must equal the discovered value byte for byte. The redirect’s iss is checked (RFC 9207) when present, and required when discovery advertises it. A templated issuer is unsupported.
PKCEThe iOS README says PKCE is used if the server supports it.Always S256, never plain, never off. A provider that lists code_challenge_methods_supported without S256 is ProviderUnsupported("S256").
Client typeA client secret can be supplied (Android ClientSecretBasic, ClientSecretPost; the iOS README’s sample passes a clientSecret).Public clients only. No clientSecret exists in any configuration type.
BrowserSystem browser; WebView not supported.System browser only, and a web view is unrepresentable in the public API.
Token storageYour code.The SDK, in the Keystore (Android) or the Keychain (iOS, AfterFirstUnlockThisDeviceOnly, not synchronized). On Android the files sit under noBackupFilesDir, so a backup cannot replay them on another device.
ErrorsPlatform exception types and codes.Four categories. error_description is attached for logging and never matched on.
Server errors and retriesYour code.The SDK repeats safe requests on 408, 429, 500, 502, 503 and 504 and honours Retry-After on 429 and 503. See Server errors, 429 and Retry-After.
Opening the browserThe app calls it.Same, and the SDK never opens it on its own: Reauthenticate is a diagnosis, and you decide when login() runs. login() while Authorizing is refused with InvalidConfiguration.
Cold-start redirectNone.A redirect that reaches a cold-started process completes the pending request (kept for up to 10 minutes) and publishes Authenticated on sessionState.
Tokens and originsWhatever your code does with the token.The authenticated client attaches a token only to the protectedOrigins you configured. Any other request passes untouched.
Issuer schemeAndroid allows setSkipIssuerHttpsCheck(true) for testing.https only, with http accepted on a loopback host only. No flag.
Platform floorAppAuth-Android README: API 16+, App Links API 23+. AppAuth-iOS README: iOS 15+, HTTPS redirects iOS 17.4+.minSdk 26 on Android; iOS 17.4 for the package.
Redirect URIA custom scheme is the README’s recommendation on Android; HTTPS is supported.An https verified link is preferred. A private-use scheme works and logs a warning on every create(). Cleartext non-loopback http is refused.
Flutter dataflutter_appauth returns access, refresh and ID tokens to Dart.Dart gets the short-lived access token from accessToken() and nothing else credential-shaped.

What does AppAuth do that the SDK does not do?

The Versola OAuth SDK is a public-client, authorization-code, system-browser library. AppAuth features outside that shape have no counterpart: the three platforms declare no public symbol for them. Check this list against your current setup before you start.

  • Client secrets and confidential clients. The configuration has no secret field, by design (RFC 8252 section 8.5). If your provider client uses a secret, register a secret-less public client for the app. A provider that insists on a secret from a native app is unsupported.
  • Other client authentication at the token endpoint (AppAuth-Android has a ClientAuthentication type). The token request carries client_id only.
  • Dynamic client registration (AppAuth RegistrationRequest / OIDRegistrationRequest). No counterpart.
  • Embedded user agents. A web view cannot be passed in.
  • Choosing the browser. No allow list, deny list, custom browser or custom Custom Tabs intent.
  • Discovery at a custom URL (fetchFromUrl). Use explicit endpoints.
  • Splitting the flow. Flutter’s authorize followed by token(code), or a manual performTokenRequest, has no counterpart. login() is atomic, and no authorization code, verifier or nonce is ever handed to the app.
  • Reading or injecting the refresh token. Your backend cannot receive the app’s refresh token, and token(TokenRequest(... refreshToken: ...)) has no counterpart. refresh() spends the stored token.
  • The raw ID token. claims exposes the typed claims of the validated token, and the raw token stays encrypted for id_token_hint only. If a backend needs the ID token itself, this SDK cannot supply it.
  • Turning off checks. setNonce(null) and setSkipIssuerHttpsCheck(true) (AppAuth-Android), a non-https issuer, a missing iss and an unverified ID token are all refused. A provider that needs one of them can’t be used with the SDK.
  • Several accounts at once. One session per configuration. A configuration with another issuer or clientId is a separate client with its own storage.
  • macOS, tvOS and desktop. AppAuth-iOS documents macOS and tvOS support. The SDK targets Android and iOS, and Flutter on those two.
  • A parameter on the token request. extraParameters goes on the authorization request. The token request carries what the SDK builds, plus resource when you gave resources.
  • Custom user-agent or request interception beyond the HTTP client. httpClient / urlSession is the only seam.

What happens to existing users and tokens?

Existing refresh tokens cannot be imported, so every user signs in once after the update. No public declaration of the three platforms (the Android API dump, the Swift public surface of OAuthClient, the Dart exports of versola_oauth) accepts a refresh token, an ID token or a serialized state, and the configuration types have no field for one. The credential stores that hold them are internal wiring, behind the @VersolaInternalApi opt-in marker on Android and package access on iOS. The first create() finds nothing and reports Unauthenticated.

Plan the rollout around that:

  1. Ship the SDK in the app update and treat the first launch as signed out. Show your sign-in screen. In most cases login() completes with the user’s existing provider session, with no password entry if the browser is still signed in.
  2. Delete the old state. After the first successful login(), remove the persisted AppAuth state: the AuthState JSON in SharedPreferences or wherever your own manager put it, the archived OIDAuthState in the Keychain or in defaults, and the tokens flutter_appauth returned if you kept them (for example in a secure-storage plugin). A leftover refresh token is a credential that nobody uses and nobody guards.
  3. Optionally revoke the old refresh token at the provider before you delete it, if the provider has a revocation endpoint (RFC 7009: client_id and token, no secret for a public client). The SDK can’t do it for you, because it never sees that token.
  4. Keep the old redirect URI registered while old app versions are in use. If you move from a custom scheme to an https App Link or Universal Link, add the new URI. Don’t replace the old one until the old versions are gone.
  5. If the old client used a secret, the SDK needs a different provider client, registered as public. Register it next to the old one and move the app over.
  6. The same clientId and redirect URI can stay. The SDK keys its storage by issuer and clientId, and it does not read AppAuth’s.

How does error handling change?

AppAuth reports a failure as a platform exception type or code (AuthorizationException on Android, NSError on iOS, FlutterAppAuth exceptions in Flutter) and leaves retries to your app. The Versola OAuth SDK sorts every failure into one of four categories, and the category tells the app what to do:

CategoryWhat your app does
RecoverableOffer a retry. A user cancel is Recoverable(UserCancelled).
ReauthenticateSend the user to login(). A rejected refresh token (invalid_grant) is Reauthenticate(GrantRejected).
StepUpCall stepUp.
ConfigurationFix the deployment. Don’t catch it silently in production.

When a provider answers 500, 503 or 429, the SDK repeats the requests that are safe to repeat, waits as long as Retry-After asks, and then returns one Recoverable error. Your app needs no retry loops of its own for these cases. The rules, with code for each platform, are on Server errors, 429 and Retry-After.

How do I migrate from AppAuth-Android?

The AppAuth side below is abridged from the README’s examples. The SDK side is the target API.

Before (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
}

After (Versola OAuth SDK)

  1. Dependency. Remove net.openid:appauth. Add the SDK from Maven Central, with google() and mavenCentral() in your repositories. minSdk becomes 26.

    dependencies {
        implementation("io.versola:oauth-android:<version>")
    }
  2. Manifest. Remove the appAuthRedirectScheme placeholder and any net.openid.appauth.RedirectUriReceiverActivity filter. Add the intent filter on the SDK’s activity for your redirect URI; the snippet is on the Android page. For a verified App Link, also serve assetlinks.json on the host.

  3. Create the client once, with the same issuer, client id and scopes. Use an https redirect URI if you can register one with the provider.

    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. Sign in. Replace the request, the redirect activity and the token exchange with one call:

    scope.launch {
        try {
            client.login()
        } catch (e: VersolaError) {
            // Recoverable, Reauthenticate, StepUp or Configuration
        }
    }
  5. Call your API. Replace performActionWithFreshTokens and the AuthState write-back with the interceptor on your own OkHttpClient, or with client.call(request):

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

    If you need the value, client.accessToken() returns an AccessToken. It is the escape hatch, and a token handed to your code is one you can log.

  6. Observe the session instead of reading your stored state: collect client.sessionState.

  7. Sign out with client.logout(). It returns LogoutOutcome(sessionEnded, tokenRevoked).

  8. Delete the old AuthState storage (see What happens to existing users and tokens?).

How do I migrate from AppAuth-iOS?

Before (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
}

After (Versola OAuth SDK)

  1. Dependency. Remove AppAuth from your package or Xcode project. Add the SDK’s public repository and the VersolaOAuth product, then import OAuthClient. The package needs 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 for an https redirect: Associated Domains for the host, and the apple-app-site-association file, as described on the iOS page. A private-use scheme needs none of that and logs a warning.

  3. Create the client (async, once). The presentationAnchor is a closure that returns the window the browser sheet is presented from:

    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. Sign in. Delete currentAuthorizationFlow and the application(_:open:options:) plumbing:

    do {
        try await client.login()
    } catch let error as OAuthError {
        // .recoverable, .reauthenticate, .stepUp or .configuration
    }

    Keep one handler, only for Universal Links that arrive while the app is cold:

    .onOpenURL { url in
        Task { _ = await client.handleRedirect(url) }
    }
  5. Call your API with the authenticated session instead of performAction:

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

    Or call try await client.accessToken() where you need the value.

  6. Observe with for await state in await client.sessionStates() { ... }.

  7. Sign out with await client.logout(), which returns a LogoutOutcome.

  8. Delete the archived OIDAuthState (see What happens to existing users and tokens?).

How do I migrate from flutter_appauth?

Before (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>',
));

After (versola_oauth)

  1. Dependency. Replace flutter_appauth in pubspec.yaml with versola_oauth. The Flutter page covers the install. Remove the plugin’s own setup: appAuthRedirectScheme on Android and the CFBundleURLTypes custom scheme on iOS, unless you keep a custom-scheme redirect.

  2. Native setup in the host app. Android: the VersolaRedirectActivity intent filter. iOS: iOS 17.4 or newer, Associated Domains for an https redirect, and VersolaOAuthPlugin.presentationAnchor if the app has its own window handling. See the Flutter page.

  3. Create and use:

    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. Remove your token storage. Nothing credential-shaped is kept in Dart, so delete the code that stores and refreshes tokens. accessToken() refreshes natively.

  5. Sign out with client.logout(). It returns a LogoutOutcome.

  6. Errors. FlutterAppAuthUserCancelledException becomes RecoverableError, and the platform exception becomes the matching category.

Dart has no authenticated HTTP client. Call your API with the value accessToken() returns, and call it again for each request or batch rather than caching it.

Migration checklist

  • The provider client is public and has PKCE S256; no secret is used anywhere.
  • The configured issuer equals the discovery document’s issuer member.
  • offline_access is in scopes if the provider needs it for a refresh token (see Providers).
  • The redirect URI is registered with the provider exactly as configured, and for https the app’s assetlinks.json or apple-app-site-association file is served.
  • postLogoutRedirectUri is registered with the provider.
  • No code reads, writes or sends a refresh token or a raw ID token.
  • The old AuthState (or archived OIDAuthState, or Dart token storage) is deleted after the first successful login().
  • Your sign-in screen handles the four error categories, and a Configuration error is not caught silently in production.

Sources

The AppAuth facts on this page were read on 2026-10-30. The SDK side comes from the SDK’s own API specification and public declarations.

Next steps