Provider guides — Versola Docs
/docs
0.1.0versola.ioGitHub

Provider guides

The minimum contract an OpenID provider meets for the Versola OAuth SDK, the provider guides, rate-limit behaviour per provider, and the redirect setup every provider shares.

The OAuthClient of the Versola OAuth SDK (Android, iOS, Flutter) works with any provider that meets a short contract. This page lists that contract, the providers with a guide, how each provider’s rate limits reach your app, and the redirect setup that is the same for all of them. The condition: your provider must be an OpenID Provider that supports the authorization code flow with PKCE for a public client.

The guides

ProviderGuideVersion in the SDK’s tests
KeycloakKeycloak26.4
Ory HydraOry Hydrav26.2.0 (self-hosted, open source image)

The Android integration tests drive the full flow against the provider’s container in the SDK’s CI: sign-in, an authenticated call, UserInfo, revocation and sign-out, plus tampered ID tokens, PAR, explicit endpoints and a client without refresh tokens. The iOS and Flutter test drivers run the same tests against the same containers. The browser leg is the provider’s login conversation driven over HTTP. Each guide says what the tests observed.

The minimum contract

A provider works with OAuthClient when it offers all of the following.

  • Discovery, or explicit endpoints. A document at {issuer}/.well-known/openid-configuration whose issuer member equals the issuer you configured. One trailing slash on either side is ignored for that comparison. Without discovery you give ProviderEndpoints instead, and then you cannot also give issuer.
  • Authorization code flow with PKCE S256 for a public client. The client has no secret and the token request carries only client_id. The SDK always sends S256.
  • response_type=code with state echoed on the redirect.
  • An asymmetrically signed ID token (RS256, PS256, ES256, ES384, ES512, RS384, RS512, PS384 or PS512) with its keys at jwks_uri. none and HMAC algorithms are refused.
  • An iss claim in the ID token equal to the discovered issuer, byte for byte.

Everything else is optional, and the SDK adapts to what discovery says.

Provider does not offerWhat the SDK does
A refresh tokenThe session ends with Reauthenticate(SessionExpired) when the access token expires. No token request is made.
A revocation endpointSign-out skips revocation and reports tokenRevoked = false.
An end_session_endpointSign-out wipes local state and reports sessionEnded = false, unless you supply the URL with endSessionEndpoint.
A UserInfo endpointuserInfo() fails with Configuration(ProviderUnsupported).
PARThe SDK sends the plain authorization request.
DPoPTokens are bearer tokens.

A provider is unsupported when it requires a client secret from a native app, accepts only plain PKCE or none, signs ID tokens only with HMAC, or announces an issuer that does not match its discovery document. A templated issuer is such a mismatch.

Redirect setup shared by every provider

What you register with the provider is provider-specific and is in each guide. What the app does with the redirect is the same everywhere.

The SDK accepts three kinds of redirectUri: an https URL (a verified App Link or Universal Link, preferred), a loopback URL, or a private-use scheme such as com.example.app:/oauth. A private-use scheme works, but the SDK logs a warning on every create(), because any app on the device can register the same scheme. Cleartext http to a non-loopback host is refused. On iOS a cleartext loopback URL is refused too, because ASWebAuthenticationSession cannot receive it.

Platform setup that does not depend on the provider is on the Android, iOS and Flutter pages. The parts that decide whether a redirect arrives are below.

Register the redirect URI with the provider exactly as it appears in OAuthConfiguration.redirectUri. Then re-declare the SDK’s redirect activity in the app manifest with an intent filter for it. The SDK’s own manifest declares the activity without a filter, and the manifest merger unions the two. The filter below also covers the sign-out path.

<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>

The host must be one you control, and it must serve Digital Asset Links (https://app.example.com/.well-known/assetlinks.json) for the app’s package name and signing certificate. The SDK opens an Auth Tab where the device’s browser supports it and a Custom Tab otherwise. Under an Auth Tab the browser checks that the redirect host belongs to your app. If it cannot, login() fails with Configuration(InvalidConfiguration), and the cause is the assetlinks.json, not the provider. The same applies to postLogoutRedirectUri when it is an https URL: without a verified host the tab closes unverified and logout() reports sessionEnded = false.

For a private-use scheme the intent filter carries android:scheme="com.example.app" instead.

The Android artefact is io.versola:oauth-android on Maven Central, and minSdk is 26.

For an https redirect URI the SDK starts an ASWebAuthenticationSession with an https(host:path:) callback, which needs iOS 17.4 (the package’s platform floor). The app needs an Associated Domains entitlement for the redirect host, and the apple-app-site-association file on that host must list the app under webcredentials. An applinks association alone is not enough. AppAuth-iOS, which uses the same system API, documents the same requirement (README of AppAuth-iOS).

You supply the window the sheet is presented from, once, when you create the client. A minimal anchor for a single-window app, as an illustration:

import AuthenticationServices
import UIKit

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

A Universal Link that reaches the app while it is cold goes to client.handleRedirect(url), which returns true when the URL was the redirect the SDK was waiting for. In SwiftUI:

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

Flutter

The plugin is a binding over the native SDKs and has no redirect handling of its own. Do the Android manifest work above in the app’s android/ project, and the iOS entitlement work in the Runner target. On iOS the plugin presents the sheet from the key window of the foreground scene. An app with its own window handling sets VersolaOAuthPlugin.presentationAnchor before the first create. Dart has no authenticated HTTP client: accessToken() is the one call that hands you a credential.

How do provider rate limits reach your app?

Providers answer an overloaded token endpoint with a 429, and some add Retry-After. The Versola OAuth SDK honours Retry-After on 429 and 503, seconds or HTTP date, in preference to its own backoff, and never shortens the wait to fit its budget. A value that doesn’t fit stops the operation, and your app gets Recoverable with the cause Server and retryAfter set. Until it has passed, the SDK sends no request to that origin. Without the header, a 429 is repeated under the ordinary retry budget like a 5xx.

The provider sendsWhat the SDK does
Retry-After on a 429 or 503Waits for it, or hands it to your app
Only its own headers (X-RateLimit-*, X-Rate-Limit-*, x-ratelimit-reset)Ordinary backoff. The SDK doesn’t read those headers
No rate-limit headersOrdinary backoff

There is nothing to configure per provider. Don’t raise the retry maxima to ride out a limit: they are hard bounds. The retry rules, with the code for each platform, are on Server errors, 429 and Retry-After.

Reading a failure

Errors fall into four categories, and the category tells the app what to do: Recoverable (offer a retry, or wait for retryAfter), Reauthenticate (send the user to login()), StepUp (call stepUp) and Configuration (a deployment mistake: fix it, don’t catch it in production). Most first-run provider problems arrive as Configuration.

You seeUsually means
DiscoveryIssuerMismatchThe configured issuer is not what the provider’s discovery document announces. Copy the issuer member exactly.
InvalidConfiguration after the browser closes (Android)The redirect host is not a verified App Link of this app.
IdTokenRejected(reason)The ID token failed validation: algorithm, iss, aud, exp, nonce or signature. Retrying never fixes it.
ProviderUnsupported(capability)The configuration asks for something the provider does not advertise (dpop, par, S256), or userInfo() has no endpoint.
AuthorizationRejectedThe provider refused a pushed authorization request (PAR), for example an unregistered redirect URI.

An unregistered redirect URI on a plain (non-PAR) request shows an error page in the browser that the SDK cannot see. It looks the same as the user closing the tab. Check the registered string first. The full list is on Session states and errors.

Next steps