docs(adr): signing in with SimpleLogin only, and no anonymous accounts (ADR 0020)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-23 13:41:22 +02:00
commit c405d3c344
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -0,0 +1,159 @@
# ADR 0020: Signing in with SimpleLogin only
- **Status:** decided 23 Oct 2026, by the developer (D1–D11 below); the details under
*Proposal* that no decision names are proposed, for review before building.
- **Builds on:** ADR 0010 (email sign-in), ADR 0011 (profile pages), ADR 0016 (the
register), ADR 0017 (a password, TOTP and the keys).
- **Changes:** ADR 0017's way in on an instance that offers SimpleLogin (its keys, their
wrapping and the recovery phrase stay as they are), and every instance's anonymous
accounts, which go (ADR 0010's start without an account, ADR 0016's 6, ADR 0017's 9).
## Context
The developer asked on 23 Oct to limit signing in to "only SimpleLogin/Proton single sign
in". Proton offers no OpenID Connect sign-in to other apps; SimpleLogin, which belongs to
Proton, does ("Sign in with SimpleLogin"), and its own login page offers "Log in with
Proton". What SimpleLogin does, from its source (`simple-login/app`, 22 Oct) and from tries
against app.simplelogin.io on 23 Oct:
- **OpenID Connect's code flow:** `https://app.simplelogin.io/oauth2/authorize`,
`/oauth2/token`, `/jwks`; ID tokens signed with RS256, carrying the `nonce` the
authorization asked for and what the person shares (address, name, avatar); each code is
taken once. **No PKCE**; the client secret is always required, so only a server can
exchange a code.
- **New apps are closed by default since 22 Oct** (`ALLOW_NEW_OAUTH_CLIENTS`); "existing
websites keep working as usual". app.simplelogin.io still let the developer make apps on
23 Oct: one for development, one for learn.halkyon.eu ("Halkyon") and a few in reserve.
Other operators may not get an app any more.
- **Dev Mode:** until SimpleLogin approves an app, its consent page warns "… is in Dev
Mode and isn't approved (yet) by SimpleLogin". Approval is asked for on the app's page.
- **Redirects:** each must be registered exactly (an unregistered loopback address was
refused on 23 Oct), and the settings page takes https addresses only.
- **The ID (`sub`) belongs to one person in one app** (`sub` is the person's link to the
app, `ClientUser`), whatever the discovery document's `public` says. It changes when the
person removes the app in SimpleLogin's list, deletes the alias they shared with it, or
deletes their SimpleLogin account. Once someone has allowed an app, later sign-ins skip
the consent page and give the same ID.
- **The consent page always shares an address, a name and an avatar:** the app can't
choose (`Client.get_scopes`). The address is the person's own or an alias, by default a
new one, which counts against their aliases.
## Decisions (23 Oct 2026, the developer's)
- **D1 SimpleLogin only** on learn.halkyon.eu (over SimpleLogin as an extra way in beside
ADR 0017's, which was recommended): no address, no TOTP and no password signs in there.
- **D2 A password still unlocks the keys** (recommended, over dropping the keys): it never
leaves the device; ADR 0017's keys, wrapping and recovery phrase stay.
- **D3 Per instance:** each operator chooses the ways; operators without a SimpleLogin app
keep ADR 0017's.
- **D4 No anonymous accounts, anywhere** (recommended): every account has a way in and a
handle; the first start needs a connection and an account.
- **D5 A handle for everyone**, suggested at sign-up from the name SimpleLogin shares
(recommended), confirmed or changed by the person; the name is then dropped. Its public
page exists only once the person goes public (recommended): something published or the
profile filled in.
- **D6 An ID nobody has, signed out: ask first** (recommended): "New here?" makes an
account; "I already have one" takes the handle and the recovery phrase (8).
- **D7 A broken link is mended with the handle and the recovery phrase** (the developer's,
over the phrase alone, which was recommended): every account has a handle now (D5).
- **D8 A hard cut-off on learn.halkyon.eu, 30 days after the release** (over the day
GitHub ends, 22 Jan 2027, which was recommended): from then on every session of an
account without SimpleLogin ends, GitHub and email codes sign nobody in there, and the
instance offers SimpleLogin alone.
- **D9 Phones come back through verified links** (recommended): Android's App Links and
iOS's https callbacks with associated domains, for learn.halkyon.eu; the forwarding
page with the app's scheme for other instances, the loopback on desktops.
- **D10 Accounts without SimpleLogin are deleted 30 days after the cut-off**
(recommended, over keeping them unreachable): nobody can add SimpleLogin signed out;
the reminder names both days.
- **D11 An account left unfinished (no keys) is deleted after a day** (recommended, over
finishing it at the next sign-in); its handle is free again.
- **Also (23 Oct):** the developer's own account moves by adding SimpleLogin to it while
signed in with GitHub; the instance's SimpleLogin app goes up for approval once
halkyon.app/learn exists, before learn.halkyon.eu switches.
## Proposal
1. **The instance's ways.** The operator's SimpleLogin app: `simpleLoginClientId` and
`simpleLoginClientSecret` in the passwords, on the register (ADR 0016), which alone
exchanges codes. `SIGN_IN_WAYS`: `simplelogin`, `password` or both; unset, every way
whose keys are there. On learn.halkyon.eu `simplelogin`. The instance's basics say
which ways it offers.
2. **One SimpleLogin app per instance, the register's.** Every service of the instance
signs in through it (Todo's too), as ADR 0016 has them sign in through the register;
one app means one ID per person on the instance.
3. **The flow.** The app asks its server to begin (`simpleLogin.begin`, with or without a
session; a member forwards to the register): the register keeps an attempt (a random
ID the app holds, a `nonce`, which app and platform, the caller if signed in; ten
minutes, taken once) and answers the authorization address. The app opens it in the
browser (`flutter_web_auth_2`, as GitHub's). SimpleLogin sends the browser to
`https://<register's host>/simplelogin-callback`, the one address registered. On a
phone with learn.halkyon.eu, the system hands that address straight to the app (D9).
Elsewhere the instance's page there forwards code and `state` to the app: the
desktop's loopback (`http://127.0.0.1:43823/simplelogin-callback`, each app its own
port) or the phone's scheme (`app.halkyon.learn://simplelogin-callback`). Its targets
are fixed per app and platform, chosen by a marker in `state`, so it sends nobody
elsewhere; SimpleLogin's errors (a denial) are forwarded alike. The app hands the code
with its attempt's ID to `simpleLogin.complete`.
4. **Checking.** The register exchanges the code and checks the ID token: RS256 against
SimpleLogin's keys (`/jwks`, kept, fetched again for an unknown key ID), the issuer
`https://app.simplelogin.io`, the audience its client ID, not expired, and the `nonce`
the attempt's. So a code caught on its way to the app is worthless: it can only
complete the attempt it was asked for, whose ID never left the app. (A hostile app
starting an attempt of its own is what D9's verified links stop: SimpleLogin answers
without a consent page for an app already allowed.) It keeps the `sub` alone
(`simplelogin_identity`: account ID, `sub`, each unique); the name serves only as the
handle's suggestion at sign-up (D5); address, name and avatar are never stored, and
SimpleLogin's userinfo is never asked.
5. **A known ID** gives a session for its account (on a member, the member's own, ADR
0016). A signed-in caller adds SimpleLogin to their account; a `sub` another account
has is refused, and nobody is switched to another account.
6. **Signing up** (D6, "New here?"): the handle (suggested, D5), then the keys on the
device: made (ADR 0017, 3), a password to unlock them (asked twice, never sent), the
recovery phrase shown once and three of its words typed back; the register keeps what
ADR 0017 keeps of them (`keys.set`: salt, costs, public keys, both wrappings), without
SRP's verifier, an address or TOTP where SimpleLogin is the way in. Until the keys are
set the account is unfinished: its session does nothing else, and one unfinished for a
day is deleted (D11).
7. **A new device** gets the wrapped keys and the salt with the session, and the password
unwraps them there. Changing it takes the old one on the device (the keys rewrapped and
sent); a lost password with the phrase rewraps them; without it, new keys, and what was
encrypted with the old ones is lost (ADR 0017, 6). No TOTP of ours: SimpleLogin and
Proton have their own second factor.
8. **Mending a broken link** (D7, "I already have one"): the handle and the recovery
phrase. The register answers the keys wrapped under the phrase and a nonce; the device
unwraps them and signs (`halkyon key proof v1|`, as ADR 0017's resets); the new `sub`
takes the old one's place. A handle nobody has gets a decoy wrapping and the same
refusal, as ADR 0017 does for addresses; at most 10 tries an hour per handle and 30
per network.
9. **Handles** (D5): required at sign-up, on every instance; `/@handle` answers as for an
unknown handle until the person publishes something or fills in their profile; asking
whether a handle is free stays limited per person and network, since its answer tells
that the handle exists.
10. **Anonymous accounts go** (D4): `anonymousIdp` is removed; the first start offers the
instance's ways and other instances. On learn.halkyon.eu, anonymous people can add
SimpleLogin (and a handle and keys) until the cut-off (D8); another instance's
anonymous people add a password until a cut-off their operator sets
(`ANONYMOUS_SESSIONS_END`, a day, as `GITHUB_SIGN_IN_ENDS`).
11. **The cut-off on learn.halkyon.eu** (D8), 30 days after the release: the app reminds
everyone without SimpleLogin from the release on, saying the day and what follows;
then their sessions end, and 30 days later their accounts are deleted (D10).
12. **The privacy page** says that SimpleLogin learns when someone signs in to Halkyon,
that the instance keeps only SimpleLogin's ID for its app, and that the name helps
suggest a handle once and the rest of what the consent page shares is dropped; the
sign-in screen says so before the browser opens. Store listings, the README and the
website stop saying that the app starts with an anonymous account.
## Consequences
- People need a SimpleLogin account (a Proton account gives one) and a connection to sign
up or sign in on a new device; nothing works signed out, on any instance.
- The instance depends on SimpleLogin's sign-in for apps, which SimpleLogin closed to new
apps on 22 Oct: if it ends altogether, the operator turns `password` on, and people set
a password and an address (ADR 0017); their keys stay.
- Operators who can't get a SimpleLogin app keep ADR 0017's way in; the code keeps both.
- Everyone has a handle, which stays private until they go public.
- The keys' password is one more thing to keep, and the recovery phrase also mends a lost
SimpleLogin link.
- GitHub's sign-in on phones has the weakness D9 avoids (its callback is the scheme)
until it ends.