halkyon/docs/adr/0023-openid-connect-module.md
Tizian 「ティツィアーン」 3cb2000927
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>
2026-10-24 09:17:49 +02:00

6.1 KiB
Raw Permalink Blame History

ADR 0023: Signing in with OpenID Connect, a module others can use

  • Status: decided 23 Oct 2026, by the developer (D1–D4); the design below is built as ADR 0020's step 2, whose D12 and D13 it carries. halkyon_oidc_flutter built with step 4 (23 Oct, night): OidcBrowser opens an OidcStart's page and returns the code (OidcCode), the provider's refusal (OidcDenied) or nothing (closed), the state checked on the device, as the server never compares it; the way back an OidcReturn (the app's scheme, a verified https address, the loopback). halkyon_app's LoopbackWebAuth moved in, relicensed from AGPL v3 to BSD-3-Clause by its author, the developer, and serves GitHub's sign-in from there until it ends.
  • Builds on: ADR 0014 (halkyon_base, the first Serverpod module), ADR 0020 (signing in with SimpleLogin only).

Context

ADR 0020 has learn.halkyon.eu sign people in with SimpleLogin, through OpenID Connect's code flow. Asked on 23 Oct whether that could be "a separate module that others may use as well at their convenience with Serverpod", what Serverpod v4.0.4 offers:

  • A contract for providers of one's own: an IdentityProvider, built by an IdentityProviderBuilder the host passes to AuthServices beside the stock ones, an endpoint on IdpBaseEndpoint, an account model linking the provider's ID to Serverpod's AuthUser, and a controller on the device ("Custom providers" in Serverpod's documentation, still being written on 23 Oct). OAuth2PkceUtil exchanges a code, PKCE optional, and is exported.
  • No OpenID Connect: no provider for it, no discovery, and no exported check of ID tokens (Google's and Firebase's IdTokenVerifier is internal and refuses tokens without typ, ADR 0020's D12).

Decisions (23 Oct 2026, the developer's)

  • D1 OpenID Connect, with a SimpleLogin preset (recommended, over SimpleLogin alone): any provider that follows OpenID Connect's code flow (Keycloak, Authentik, Zitadel, Authelia, Pocket ID, …), which Serverpod lacks; SimpleLogin a preset that knows its ways (no PKCE, no typ). Only SimpleLogin is tried live; the rest is tested to the specification. The packages' names stay generic.
  • D2 BSD-3-Clause (recommended, over Apache-2.0, MIT, and AGPL v3 as Halkyon's other packages): Serverpod's own licence, so whoever builds on Serverpod can take it, closed apps too.
  • D3 In this repository, published later (recommended, over a repository of its own now, or offering it to Serverpod): under packages/ with publish_to: none, so a change to it and to Halkyon is one commit; to pub.dev once it runs on learn.halkyon.eu, asked again before, as publishing is public.
  • D4 Now, as ADR 0020's step 2 (recommended, over moving it out after v0.20.0): the boundary drawn first costs less than pulling Halkyon's code apart later, a table moved between modules included; v0.20.0 comes about a day later.

Design

  1. Packages: halkyon_oidc_server, a Serverpod module (nickname oidc), with its generated client halkyon_oidc_client; halkyon_oidc_flutter, the device's half, with ADR 0020's step 4. halkyon_base builds on the module; nothing of Halkyon's is in it.
  2. Providers: a host names one or more, each under a key of its own that never changes (simplelogin): the issuer, the endpoints (from the provider's discovery document, checked to name the same issuer, or given), the client's ID and secret, the scopes, PKCE or not, the signing algorithms its tokens may use (RS256 unless said), and the callback address the provider has registered. The SimpleLogin preset fills in all but the client and the callback.
  3. What it keeps: a person's identity at a provider, the provider's key and the sub (halkyon_oidc_identity): each sub one person's at a provider, one per person and provider. Nothing of what the token says besides; the claims go to the host's code, which decides (Halkyon drops them, ADR 0020's 4).
  4. Attempts (ADR 0020's D13): halkyon_oidc_attempt, the ID PostgreSQL's gen_random_uuid() (122 random bits), the secret only the device holds; the state and the nonce, 32 random bytes each, and PKCE's verifier where used; who began it, if signed in; ten minutes, taken once. The host may begin the state with a marker of its own (ADR 0020's forwarding page reads one).
  5. Checking a sign-in (ADR 0020's D12): the code exchanged with the client's secret, by the module itself rather than OAuth2PkceUtil, so that a provider out of reach (no answer in time, a 5xx) is told apart from a refused code; the ID token's header without typ asked for, its algorithm one of the provider's, its key the one its kid names in the provider's keys (kept an hour, fetched again for an unknown kid at most once a minute, every fetch with a timeout); the issuer, the audience (and azp with more than one), the expiry and the time issued, a minute of clocks' difference allowed, the nonce the attempt's.
  6. Two ways to use it:
    • The stock way, as Serverpod's providers: the host's endpoint on OidcIdpBaseEndpoint, begin and login: a known sub gets a session, an unknown one a new person; linking to the signed-in caller where the attempt was begun so.
    • The parts, for a host with rules of its own (Halkyon: the register, a handle before an account, ADR 0016 and 0020): begin, verify (the identity proven, and its claims, touching no account), link, accountOf, unlink, called from its own endpoints.
  7. The forwarding page goes into the module (addOidcCallbackForwarding), with the targets the host fixes: what ADR 0020's 3 has Halkyon need, a provider that takes https callbacks alone, each registered exactly, others need alike.

Consequences

  • Halkyon's sign-in rests on a module of its own making, with its own tests and README, kept usable by others: its interface changes with care once published.
  • Serverpod hosts get OpenID Connect; a provider other than SimpleLogin may still need a fix the specification's tests didn't foresee.
  • The module's tables are part of every host's migrations, as halkyon_base's are.