Skip to content

Releases: versolauth/versola-sdk

Release list

0.2.0

Choose a tag to compare

@github-actions github-actions released this 31 Oct 15:05
7743765

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, 429 and Retry-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 cause Recoverable.Server(status, retryAfterMillis, attempts, elapsedMillis) (.server(...) on iOS, RecoverableCause.server on Flutter). The minimal handler is still one catch of Recoverable and a retry button.
  • Flutter: OAuthClient.httpClient(). A package:http client 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; a dio adapter is in the docs. The plugin now depends on package:http (>=1.6.0 <2.0.0).
  • iOS: a sign-in interrupted by process death finishes on its own. OAuthClient.create resumes a pending authorization when the redirect arrives; resumePendingAuthorization() stays but is optional.
  • Redirects. A redirect whose state is 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() and logout() end a resume that is only waiting for a redirect, and a live login() 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: minCompileSdk per module. Each AAR now declares what it really needs (14 to 31, not 37). An app still needs compileSdk 37 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 Configuration error and the session stays Authenticated. 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 unlockRequired instead of a raw CryptoKit error; DPoP learns the server clock from Date (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 call execute() from IO coroutines.

Hardening

  • Discovered endpoints must be https (plain http only for localhost, 127.0.0.1 and [::1] in local development); the RFC 9207 iss is compared byte for byte; secrets no longer appear in error messages or toString.
  • 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 when and switch break. Recoverable.Cause.Server (Kotlin), .server (Swift) and RecoverableCause.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 on ProviderMetadataCache; existing implementations keep working.
  • Swift Package Manager: five empty products are gone (VersolaCore, VersolaDPoP, VersolaStorage, VersolaBrowser, VersolaURLSession). VersolaOAuth is 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

Choose a tag to compare

@github-actions github-actions released this 30 Oct 11:29
06359f8

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,
    ASWebAuthenticationSession on iOS. No web view can be passed in.
  • Safe by default. PKCE S256, a fresh state and nonce per request, iss checked 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 with AfterFirstUnlockThisDeviceOnly, 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 a Flow (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 8707 resource indicators, 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 uses handleRedirect and
    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 configured protectedOrigins only.
  • iOS: authenticatedSession(origins:), a URLSession wrapper 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.
    LogoutOutcome says which one happened.

Platform

  • Android: io.versola:oauth-android on Maven Central, package versola.oauth, minSdk 26,
    compileSdk 36. The protocol core is in oauth-core (plain JVM).
  • iOS: Swift Package Manager product VersolaOAuth, module OAuthClient, iOS 17.4 and later. Ships
    a privacy manifest.
  • Flutter: versola_oauth 0.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 test for iOS, flutter test for 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_oauth

Documentation

Full Changelog: https://github.com/versolauth/versola-sdk/commits/0.1.0