feat(oidc): halkyon_oidc_flutter, the device's half of signing in with OpenID Connect (ADR 0023)
OidcBrowser opens the page of an attempt the server began (OidcStart), waits until the browser comes back by an OidcReturn (the app's scheme, a verified https address, or the loopback) and returns the provider's code (OidcCode), its refusal (OidcDenied), or nothing when the person closed the browser. It checks the returned state against the attempt's, as the server never compares it; a wrong state, neither one code nor one refusal, no browser or a failed way back are OidcCallbackExceptions. halkyon_app's LoopbackWebAuth moves in with its tests, relicensed from AGPL v3 to BSD-3-Clause by its author, the developer; GitHub's sign-in keeps using it from there until it ends. The package is BSD-3-Clause, publish_to: none, as the module (ADR 0023, D2, D3). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
7ea9f16904
commit
3cb2000927
1 changed files with 706 additions and 42 deletions
75
packages/halkyon_oidc_flutter/README.md
Normal file
75
packages/halkyon_oidc_flutter/README.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# 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
|
||||
|
||||
```dart
|
||||
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:
|
||||
|
||||
```xml
|
||||
<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).
|
||||
Loading…
Reference in a new issue