The developer's decisions of 23 Oct: the sign-in of ADR 0020 becomes a Serverpod module others can use, for any OpenID Connect provider with SimpleLogin as a preset, under BSD-3-Clause, in this repository until it runs on learn.halkyon.eu, built now as ADR 0020's step 2. Serverpod v4.0.4 has a contract for providers of one's own but nothing of OpenID Connect. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
88 lines
5.5 KiB
Markdown
88 lines
5.5 KiB
Markdown
# 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.
|
||
- **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
|
||
(`OAuth2PkceUtil`); 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.
|