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:
parent
98586f1779
commit
c405d3c344
1 changed files with 159 additions and 0 deletions
159
docs/adr/0020-sign-in-with-simplelogin.md
Normal file
159
docs/adr/0020-sign-in-with-simplelogin.md
Normal 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.
|
||||
Loading…
Reference in a new issue