Repository navigation
Releases: versolauth/versola-sdk
Release list
0.2.0
Versola OAuth SDK 0.2.0
Server errors are now handled by the SDK itself, Flutter apps can call their API with a DPoP-bound token, iOS finishes an interrupted sign-in on its own, and the session survives a lot of situations that used to end it. The API stays production ready; the interfaces below changed because they had to.
What's new
- Server errors,
429andRetry-After. A provider that answers 408, 429, 500, 502, 503 or 504 is retried where a repeat is safe, within one retry budget per call.Retry-After(seconds or an HTTP date) is honoured and never shortened; when it doesn't fit the budget, the SDK stops, pauses requests to that origin and hands your app one error that carries the wait. New causeRecoverable.Server(status, retryAfterMillis, attempts, elapsedMillis)(.server(...)on iOS,RecoverableCause.serveron Flutter). The minimal handler is still onecatchofRecoverableand a retry button. - Flutter:
OAuthClient.httpClient(). Apackage:httpclient that sends your request through the native SDK with the token attached, so DPoP-bound tokens work from Dart and no token or proof crosses the bridge. Wrap it with your own interceptors or retry clients; adioadapter is in the docs. The plugin now depends onpackage:http(>=1.6.0 <2.0.0). - iOS: a sign-in interrupted by process death finishes on its own.
OAuthClient.createresumes a pending authorization when the redirect arrives;resumePendingAuthorization()stays but is optional. - Redirects. A redirect whose
stateis not the awaited one is ignored while a login waits, so another app or a stale link can no longer cancel the user's sign-in.login(),stepUp()andlogout()end a resume that is only waiting for a redirect, and a livelogin()is bounded by the request's ten minutes. The Android redirect activity ignores intents for other URIs and no longer raises the app over the browser tab. - Android:
minCompileSdkper module. Each AAR now declares what it really needs (14 to 31, not 37). An app still needscompileSdk37 today, because OkHttp's own AAR declares it.
Fixes that keep users signed in
- A refresh stores the rotated refresh token before the ID token is judged; a rejected ID token on refresh is a
Configurationerror and the session staysAuthenticated. An unreachable key set no longer drops the rotation. - A missing grant no longer wipes a pending authorization or the DPoP key.
- Android: a storage fault or a cancelled caller no longer signs the user out; a permanently invalidated storage key replaces itself; the pending request survives a locked key; StrongBox gets one retry before falling back to the TEE.
- iOS: a locked device gives
unlockRequiredinstead of a raw CryptoKit error; DPoP learns the server clock fromDate(a phone with a wrong clock keeps its session); a sign-in without a refresh token keeps the DPoP key; a refresh keeps the stored ID token; a reinstalled app opens signed out. - A blocked SDK call no longer starves
Dispatchers.IO: the SDK's own blocking work runs on its own dispatcher, so an app can callexecute()from IO coroutines.
Hardening
- Discovered endpoints must be
https(plainhttponly forlocalhost,127.0.0.1and[::1]in local development); the RFC 9207issis compared byte for byte; secrets no longer appear in error messages ortoString. - Discovery and key-set caches expire (5 minutes to 24 hours, from
Cache-Control); iOS caps response bodies at 1 MiB, gives each request a deadline derived from the remaining budget, and bypasses the URL cache for these fetches. - After an ID token is rejected at login, the issued refresh token is revoked (best effort).
- Release builds: signed artefacts, Gradle dependency verification, build provenance attestation.
Upgrading
- Exhaustive
whenandswitchbreak.Recoverable.Cause.Server(Kotlin),.server(Swift) andRecoverableCause.server(Dart) are new cases: add a branch, or let your default branch take them. - Android: the on-disk format changes in one direction. Stored secrets are bound to their slot; 0.1.0 data is read and upgraded on first use. Rolling an app back to 0.1.0 afterwards signs users out.
- Android: new default methods on
CredentialStore(replaceRefreshToken,saveIdToken) and onProviderMetadataCache; existing implementations keep working. - Swift Package Manager: five empty products are gone (
VersolaCore,VersolaDPoP,VersolaStorage,VersolaBrowser,VersolaURLSession).VersolaOAuthis the only product; a dependency that names one of the others no longer resolves. - Cached discovery and key-set files from 0.1.0 are fetched once more on first use.
- Flutter plugin: the iOS git dependency now builds through Flutter's symlinked plugin path; the example and docs use
httpClient().
Requirements
Android minSdk 26, compileSdk 37 (OkHttp's requirement); iOS 17.4; Flutter 3.47.x and Dart 3.13.x.
Full changelog: 0.1.0...0.2.0
0.1.0
Versola OAuth SDK
First release of the Versola OAuth SDK for native apps: an OAuth 2.0 / OpenID Connect client for
Android (Kotlin), iOS (Swift 6) and Flutter (Dart). It works with any standards-conforming
provider. The SDK's tests run against Keycloak 26.4 and Ory Hydra v26.2.0.
It signs the user in with the authorization code flow and PKCE S256 in the system browser,
validates the ID token, keeps the session in platform secure storage, refreshes tokens without
races, and attaches the access token to API calls.
Highlights
- Sign-in in the system browser only. Auth Tab or Custom Tabs on Android,
ASWebAuthenticationSessionon iOS. No web view can be passed in. - Safe by default. PKCE
S256, a freshstateandnonceper request,isschecked on the
redirect (RFC 9207), and the ID token validated (signature,iss,aud,exp,nonce) before
anything is stored. None of these can be switched off. - Refresh without races. Concurrent callers share one token request (single-flight). A rotated
refresh token is persisted before the old one is dropped. Retries are bounded by a per-operation
budget. - Secure storage. Android: the refresh token is wrapped by an AES-256-GCM Keystore key, under
noBackupFilesDir. iOS: Keychain withAfterFirstUnlockThisDeviceOnly, not synchronizable.
Flutter: the refresh token never leaves the native side. - Typed errors. Four categories on every platform:
Recoverable,Reauthenticate,StepUp,
Configuration. - Seven session states:
Unauthenticated,Authorizing,Authenticated,Refreshing,
StepUpRequired,Locked,LoggingOut. Observed as aFlow(Kotlin),AsyncStream(Swift) or
Stream(Dart). - No client secret. Public clients only; the configuration has no field for one.
Features
Authorization and sign-in
- Authorization code flow with PKCE
S256, RFC 8252 native-app redirects. - Discovery (OpenID Connect Discovery 1.0), or explicit endpoints when discovery is not available.
- Pushed authorization requests (RFC 9126), used when the provider advertises them.
login()options: scopes,prompt,login_hint,ui_locales,acr_values,max_age,
RFC 8707resourceindicators, extra parameters, and an ephemeral browser session (best effort).- Step-up authentication (RFC 9470):
stepUp(...)runs a new authorization with the required
acr_values/max_age.
Tokens and sessions
- Authorization code exchange and refresh at the token endpoint, with single-flight coordination and
refresh-token rotation. - Revocation (RFC 7009) on sign-out.
- ID-token validation against the provider's JWKS, with a bounded JWKS refetch on an unknown
kid. - UserInfo (OpenID Connect Core).
- Cold-start recovery: a sign-in whose redirect reaches a process that was killed mid-browser
completes on the next launch. Android restores the pending request; iOS useshandleRedirectand
resumePendingAuthorization.
Proof of possession
- DPoP (RFC 9449), opt-in. The key is hardware-backed where the device has it: StrongBox, then TEE
on Android; Secure Enclave on iOS.keyAttestation()reports the tier that was actually obtained.
Calling your API
- Android: an OkHttp interceptor (
interceptor(),authenticatedClient(),call()) that attaches the
token to the configuredprotectedOriginsonly. - iOS:
authenticatedSession(origins:), aURLSessionwrapper that does not follow redirects and
refreshes once on a 401. - Flutter:
accessToken()returns the short-lived access token; attach it with your HTTP client.
Sign-out
- RP-initiated logout when the provider has an end-session endpoint; a local wipe otherwise.
LogoutOutcomesays which one happened.
Platform
- Android:
io.versola:oauth-androidon Maven Central, packageversola.oauth,minSdk26,
compileSdk36. The protocol core is inoauth-core(plain JVM). - iOS: Swift Package Manager product
VersolaOAuth, moduleOAuthClient, iOS 17.4 and later. Ships
a privacy manifest. - Flutter:
versola_oauth0.1.0, Dart>=3.13.0 <3.14.0, Flutter 3.47. Android and iOS glue over
the native SDKs; Swift Package Manager integration on iOS.
Standards
RFC 6749, RFC 7636, RFC 8252, RFC 9207, RFC 9126, RFC 9449, RFC 7009, RFC 9470, RFC 8707;
OpenID Connect Core 1.0, Discovery 1.0, RP-Initiated Logout 1.0; RFC 7515 and RFC 7517 for JWS and JWK.
Retry policy
Defaults for one operation (a refresh, or a call that triggers one):
| Setting | Default |
|---|---|
| Attempts per origin | 3 |
| Attempts across all origins | 4 |
| Initial backoff | 500 ms, doubled per attempt |
| Total budget | 20 s |
The budget is bounded by construction, so a retry storm cannot outlive it.
Testing and conformance
- Conformance scenarios in
spec/conformance/, numbered 01 to 33. Scenarios 24 to 33 cover the
generic facade against Keycloak and Ory Hydra; scenario 33 proves DPoP end to end against Keycloak
(#86). - Unit tests on each platform: JVM for the Android core (JDK 21 to build, Java 17 bytecode),
swift testfor iOS,flutter testfor the plugin. - Device tier: the browser leg is exercised against Keycloak on an Android emulator and an iOS
simulator (#87). - Flutter bridge: conformance scenarios 24 to 32 run through the plugin
(#68).
Install
Android
dependencies {
implementation("io.versola:oauth-android:0.1.0")
}iOS: add the package https://github.com/versolauth/versola-sdk at from: "0.1.0" and link
the product VersolaOAuth.
Flutter
flutter pub add versola_oauthDocumentation
- Documentation site: the SDK section is being added to docs.versola.io.
Full Changelog: https://github.com/versolauth/versola-sdk/commits/0.1.0