2026-10-23 19:54:55 +02:00
|
|
|
|
# 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).
|
feat(oidc): a Serverpod module for signing in with OpenID Connect, SimpleLogin a preset (ADR 0023)
halkyon_oidc_server and its generated client halkyon_oidc_client: any
provider following OpenID Connect's code flow, beside Serverpod's own
providers, with a preset for SimpleLogin (no PKCE, no `typ`). Nothing
of Halkyon's is in it; BSD-3-Clause, `publish_to: none` for now.
- Attempts in `halkyon_oidc_attempt` (ADR 0020's D13): the ID
PostgreSQL's gen_random_uuid(), a state and a nonce of 32 random
bytes, PKCE's verifier where used, the caller if signed in; ten
minutes, taken once. A host may begin the state with a marker.
- The code exchanged by the module itself, so a provider out of reach
(a timeout, a 5xx) is told apart from a refused code; the ID token
checked with dart_jsonwebtoken's algorithms (ADR 0020's D12): the
provider's algorithms alone, the key its `kid` names, of the right
type; issuer, audience and `azp`, expiry, time issued, `nbf`, a
minute's leeway; the attempt's nonce; a `sub`.
- Keys kept an hour, fetched again for an unknown `kid` at most once
a minute; discovery checked against the issuer; every request with
a timeout.
- Identities in `halkyon_oidc_identity`: the provider's key and the
`sub`, one per person and provider, deleted with the person.
- The stock endpoint (`OidcIdpBaseEndpoint`), and the parts for a host
with rules of its own: begin, verify, keep and takeKept, link,
accountOf, unlink; merging people; a limit per network.
- The forwarding page, moved here from halkyon_base in the next commit.
The package's main library is the code generator's, as halkyon_base's;
a host imports `package:halkyon_oidc_server/oidc.dart`. ADR 0023's
point 5 no longer names OAuth2PkceUtil, which the module doesn't use.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 20:28:37 +02:00
|
|
|
|
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
|
2026-10-23 19:54:55 +02:00
|
|
|
|
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.
|