halkyon/packages/halkyon_oidc_flutter
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Tizian 「ティツィアーン」 e87b156676
feat(app): signing in with SimpleLogin on the device, its data layer (ADR 0020)
SimpleLoginRepository signs in with SimpleLogin in the browser
(OidcBrowser: the app's scheme on Android, iOS and macOS, the app's port
on the loopback on Linux and Windows, D9), then makes the account's keys
and signs someone new up, opens an account's keys with the password, gives
an account of before its first keys, or links a new SimpleLogin account in
place of a lost one with the recovery phrase (D7), which opens the keys on
the device first: a phrase that doesn't fit sends no proof, and no
password is asked after. An anonymous person adding SimpleLogin keeps the
keys their device holds. Nothing is the device's until `enter`.

SimpleLoginGateway calls halkyon_base's `simpleLogin` and `keys`
endpoints (ServerpodSimpleLoginGateway), with a fake for tests, as does
FakeOidcBrowser. The calls made on clients of their own (signed out, or as
a session not yet the device's) move from the password gateway into
BaseCalls, which both share.

Fixes BaseProtocol, the client of halkyon_base alone, which didn't read
halkyon_oidc's classes: every refusal of SimpleLogin's sign-in on a
signed-out call would have reached the app as a failure. OidcBrowser keeps
its loopback private, so that a fake can implement it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 09:17:50 +02:00
..
lib feat(app): signing in with SimpleLogin on the device, its data layer (ADR 0020) 2026-10-24 09:17:50 +02:00
test feat(oidc): halkyon_oidc_flutter, the device's half of signing in with OpenID Connect (ADR 0023) 2026-10-24 09:17:49 +02:00
.gitignore feat(oidc): halkyon_oidc_flutter, the device's half of signing in with OpenID Connect (ADR 0023) 2026-10-24 09:17:49 +02:00
analysis_options.yaml feat(oidc): halkyon_oidc_flutter, the device's half of signing in with OpenID Connect (ADR 0023) 2026-10-24 09:17:49 +02:00
CHANGELOG.md feat(oidc): halkyon_oidc_flutter, the device's half of signing in with OpenID Connect (ADR 0023) 2026-10-24 09:17:49 +02:00
LICENSE feat(oidc): halkyon_oidc_flutter, the device's half of signing in with OpenID Connect (ADR 0023) 2026-10-24 09:17:49 +02:00
pubspec.yaml feat(oidc): halkyon_oidc_flutter, the device's half of signing in with OpenID Connect (ADR 0023) 2026-10-24 09:17:49 +02:00
README.md feat(oidc): halkyon_oidc_flutter, the device's half of signing in with OpenID Connect (ADR 0023) 2026-10-24 09:17:49 +02:00

halkyon_oidc_flutter

The device's half of halkyon_oidc (halkyon_oidc_server, halkyon_oidc_client), signing in with OpenID Connect for Serverpod v4. It opens the provider's page for an attempt the server began, waits until the browser comes back, and checks that it came back for that attempt. The code it returns goes back to the server, which ends the sign-in. Halkyon signs in with it (ADR 0020, ADR 0023); nothing of Halkyon's is in it.

Using it

final browser = OidcBrowser(loopback: LoopbackWebAuth(appName: 'My App'));

// The app's endpoint on OidcIdpBaseEndpoint (halkyon_oidc_server's README).
final start = await client.signIn.begin(provider: 'keycloak');
switch (await browser.open(start, const OidcReturn.scheme('my.app'))) {
  case null:
    // The person closed the browser.
  case OidcDenied(:final error):
    // The person said no at the provider (`access_denied`).
  case OidcCode(:final code):
    final session = await client.signIn.login(attempt: start.attempt, code: code);
}

open throws an OidcCallbackException when the browser came back for another sign-in (wrongState), with neither one code nor one refusal (malformed), when there's no browser (noBrowser), or when the way back failed otherwise (failed).

The state is checked here, on the device: the server keeps the attempt's nonce, which ties the code to the attempt, but never compares the state.

The way back

Pick one per sign-in, before opening the browser: each must match the redirect the server's provider names, or the address its forwarding page sends the browser on to (addOidcCallbackForwarding).

  • OidcReturn.scheme('my.app') on Android, iOS and macOS. The system's browser sheet catches the scheme, even after a redirect: Chrome's Auth Tab (Chrome 132 on), a Custom Tab after the person's click on the consent page in other browsers, and ASWebAuthenticationSession. Android needs flutter_web_auth_2's CallbackActivity with the scheme, for the browsers without Auth Tab:

    <activity android:name="com.linusu.flutter_web_auth_2.CallbackActivity"
              android:exported="true" android:taskAffinity="">
      <intent-filter android:label="flutter_web_auth_2">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="my.app" />
      </intent-filter>
    </activity>
    
  • OidcReturn.loopback(port: 43823, path: '/oidc-callback') on Linux and Windows: LoopbackWebAuth opens the system browser and waits on 127.0.0.1 alone (RFC 8252, 7.3), five minutes at most, until cancel. It takes the place of flutter_web_auth_2's own, which ends the wait whenever the window regains focus. Give each app its own port.

  • OidcReturn.https(host: 'example.org', path: '/oidc-callback') where the system hands the address to the app alone: Android's App Links with Chrome's Auth Tab (the host's assetlinks.json), iOS v17.4 and macOS v14.4 on with associated domains (webcredentials:). It protects only where nothing else can come back: a hostile app that begins a sign-in of its own may ask for any way back the server still forwards to. On Android it fails where the app isn't verified, as a debug build isn't, and in browsers without Auth Tab (Firefox, as of v156).

A code caught on its way is worthless without the attempt's ID, which only the device holds, so the scheme and the loopback are safe for the sign-ins the app begins itself.

Licence

BSD-3-Clause, as Serverpod's own (ADR 0023, D2).