2026-10-23 13:41:22 +02:00
|
|
|
|
# ADR 0020: Signing in with SimpleLogin only
|
|
|
|
|
|
|
feat(app)!: no anonymous accounts on the device (ADR 0020, D4, D25)
Every account has a way in, so the apps make no anonymous account any
more. The start screen shows whenever nobody is signed in, a first
start included, and offers SimpleLogin, a password and GitHub for those
who have it, with no start without an account. An instance out of reach
is said so, with Try again, and one offering no way to create an account
too. An anonymous account's session left from before is deleted as the
app starts (D25), falling back to signing out on the device.
What only anonymous accounts needed goes: signing in anonymously, the
signed-out mark, adding GitHub to an account and switching to a GitHub
account from the Profile tab (with its Cancel, the only one), an
anonymous device's own keys, the warning that signing out deletes the
account, and the texts about keeping progress. Settings says on which
instance the account is signed in. Learn's publishing, reports and
suggestions and its learner count no longer ask whether the person has
an identity: everyone signed in has one.
The server still offers anonymous sign-in until the next commit, so
older apps keep working until v0.20.0 raises the minimum build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 14:50:55 +02:00
|
|
|
|
- **Status:** decided 23 Oct 2026, by the developer (D1–D20 below; D21–D28 on 24
|
2026-10-24 11:56:26 +02:00
|
|
|
|
Oct), the *Proposal* with it (D15), as D18 to D20 change it and D24 drops the
|
|
|
|
|
|
cut-off's machinery (D8's code, D10, D22, 10's `ANONYMOUS_SESSIONS_END`, 11, step 5).
|
2026-10-23 15:34:51 +02:00
|
|
|
|
- **Built (23 Oct 2026), step 1:** the forwarding page (3),
|
|
|
|
|
|
`/simplelogin-callback` on the register's web server (halkyon_base's
|
|
|
|
|
|
`addSimpleLoginCallbackRoute`, Caddy forwarding the path), ahead of the sign-in, so
|
|
|
|
|
|
development signs in through learn.halkyon.eu's page, which its SimpleLogin app
|
2026-10-23 23:35:46 +02:00
|
|
|
|
registers too. The state is `<app>-<way back>.<rest>`: the marker one of the module's
|
|
|
|
|
|
fixed table (Learn and Todo, the loopback on ports 43823 and 43824, or the app's
|
|
|
|
|
|
scheme; named `<app>-<platform>` until D9's correction), the rest 1 to 512 characters
|
|
|
|
|
|
of base64url. A 303 carries the code or
|
2026-10-23 15:34:51 +02:00
|
|
|
|
SimpleLogin's error (`deny`) with the state and nothing else; whatever is malformed,
|
|
|
|
|
|
repeated or not a GET is refused with a page that repeats nothing. No caching, no
|
|
|
|
|
|
referrer, nothing logged. Android's `assetlinks.json` for Halkyon Learn signed with
|
2026-10-23 23:35:46 +02:00
|
|
|
|
the release key is answered by `deploy/Caddyfile` on learn.halkyon.eu alone, held to
|
|
|
|
|
|
`tool/release/android-certificate.sha256` by a test: built for D9's verified links,
|
|
|
|
|
|
which its correction dropped, and kept, as it costs nothing.
|
feat(base): signing in with SimpleLogin through the register (ADR 0020, step 2)
The `simpleLogin` endpoint on halkyon_oidc's SimpleLogin preset: `begin`
for the app and platform (the state's marker for the forwarding page), at
most 30 an hour per network; `complete` with SimpleLogin's code, signed out
a session for the account with that SimpleLogin ID, signed in SimpleLogin
added to the caller's account, who becomes identified and anonymous no
longer. Someone new gets no account yet: their identity waits 30 minutes
under the attempt's ID (D16), with a handle suggested from their name,
`anna_mueller` (D17), for the sign-up of step 3.
The register alone holds the instance's SimpleLogin app
(`simpleLoginClientId`, `simpleLoginClientSecret`) and exchanges codes; a
member asks it for both steps and issues its own session. `SIGN_IN_WAYS`
says which ways the register offers, the instance's basics and the ways
in carry `simpleLogin`, GitHub links nobody more where SimpleLogin is
offered, and Learn's and Todo's pruning forgets the unfinished sign-ins
hourly. No migration: the tables are halkyon_oidc's, already in place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 21:03:15 +02:00
|
|
|
|
- **Built (23 Oct 2026), step 2:** the sign-in on the server's side (1 to 5), on
|
|
|
|
|
|
halkyon_oidc (ADR 0023) with its SimpleLogin preset: `simpleLogin.begin` and
|
|
|
|
|
|
`simpleLogin.complete` in halkyon_base, a member asking the register for both; the
|
|
|
|
|
|
instance's basics with `simpleLogin`, the ways in with it; the attempts and the
|
|
|
|
|
|
newcomers' identities forgotten hourly with the sign-up attempts. Otherwise than
|
|
|
|
|
|
proposed: the identities are halkyon_oidc's (`halkyon_oidc_identity`: account,
|
|
|
|
|
|
provider, subject; the provider `simplelogin`), not a table of halkyon_base's; the app
|
|
|
|
|
|
and platform are the state's marker rather than a column; a signed-in caller of a
|
|
|
|
|
|
member becomes a person on the register as the attempt begins, as linking would make
|
|
|
|
|
|
them; GitHub links nobody more where SimpleLogin is offered, as where passwords are
|
|
|
|
|
|
(ADR 0017); `SIGN_IN_WAYS` counts on the register, which members ask, and stops any
|
|
|
|
|
|
server with a name it doesn't know as it starts; sessions name the method
|
2026-10-23 21:19:31 +02:00
|
|
|
|
`simplelogin` (these deviations accepted by the developer, 23 Oct). Someone new gets
|
|
|
|
|
|
no account yet: their identity waits (D16), with a
|
feat(base): signing in with SimpleLogin through the register (ADR 0020, step 2)
The `simpleLogin` endpoint on halkyon_oidc's SimpleLogin preset: `begin`
for the app and platform (the state's marker for the forwarding page), at
most 30 an hour per network; `complete` with SimpleLogin's code, signed out
a session for the account with that SimpleLogin ID, signed in SimpleLogin
added to the caller's account, who becomes identified and anonymous no
longer. Someone new gets no account yet: their identity waits 30 minutes
under the attempt's ID (D16), with a handle suggested from their name,
`anna_mueller` (D17), for the sign-up of step 3.
The register alone holds the instance's SimpleLogin app
(`simpleLoginClientId`, `simpleLoginClientSecret`) and exchanges codes; a
member asks it for both steps and issues its own session. `SIGN_IN_WAYS`
says which ways the register offers, the instance's basics and the ways
in carry `simpleLogin`, GitHub links nobody more where SimpleLogin is
offered, and Learn's and Todo's pruning forgets the unfinished sign-ins
hourly. No migration: the tables are halkyon_oidc's, already in place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 21:03:15 +02:00
|
|
|
|
handle suggested (D17), for the sign-up of step 3. halkyon_oidc's `testing.dart` gives
|
|
|
|
|
|
hosts a fake SimpleLogin signing real ID tokens, which the module's tests use too.
|
feat(base): SimpleLogin accounts' keys without SRP, signing up in one call (ADR 0020, step 3)
An account that signs in with SimpleLogin keeps its keys under a password
that signs nobody in (D19): `account_password` keeps the password's own
proof key (Ed25519, `passwordProofKey`) in place of SRP's verifier, which
becomes nullable. The migration adds the column and drops the NOT NULL,
nothing else.
- `simpleLogin.signUp(attempt, handle, setup)`: someone new whose identity
waits under the attempt gets, in one transaction, an account
(identified), the link, a profile with the handle and the keys; then a
session (D18). Refused, the identity still waiting, for a setup out of
range and a handle refused as saving a profile refuses it.
- `simpleLogin.checkHandle(attempt, handle)`: whether someone new could
take a handle, at most 30 an hour per attempt and 60 per network.
- `simpleLogin.complete` says, for an account, whether it has its keys and
its handle, and keeps the sign-in ten minutes under its attempt, the
account as its caller: D20's confirmation for the changes to come.
- `keys.begin`, `keys.unlock`: the keys for a signed-in device once the
password is proven with its proof key, for a nonce, bound to the base
URL the device addressed; wrong proofs counted per session as ADR
0017's.
- An authenticator is enrolled only for a password that signs in.
- Members ask the register for all of it; `RemoteRegister` and the
instance API carry the four new operations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 21:49:44 +02:00
|
|
|
|
- **Built (23 Oct 2026), step 3 so far:** the password's own proof key (`halkyon_keys`'
|
|
|
|
|
|
subkey 3 of the master key, D19), which `account_password` keeps in place of SRP's
|
|
|
|
|
|
verifier for an account that signs in with SimpleLogin (the verifier becomes nullable:
|
|
|
|
|
|
a migration that drops nothing); `simpleLogin.signUp` in one call (D18) and
|
|
|
|
|
|
`simpleLogin.checkHandle`, both for someone new's identity alone; `keys.begin` and
|
|
|
|
|
|
`keys.unlock`, the proof counted as ADR 0017's; every sign-in into an account kept ten
|
feat(base): the changes of SimpleLogin accounts' keys, each with the password and a fresh sign-in (ADR 0020, D20)
Every change takes the password's proof by its own proof key (the
recovery phrase's for a lost password) and a SimpleLogin sign-in of the
last ten minutes on the device (`KeysOwnerProof`, its attempt), as ADR
0017's take the password and a code. The sign-in is checked before the
proof and taken with the change, so a wrong proof leaves it standing and
each sign-in serves one change.
- `keys.set(attempt, setup, handle?)`: the first keys of an account that
SimpleLogin was added to (anonymous before, or signing in with GitHub
or a code), with the handle where it has none; the sign-in alone, as
there is no password to prove yet.
- `keys.changePassword`, `keys.beginPhrase` and `keys.resetPassword` (a
lost password, with the phrase): every other session ends, in every
service, as ADR 0017's change has it.
- `keys.setRecoveryKeys`: a new phrase.
- `account.delete(keysProof)`: an account without SRP's verifier deletes
itself with the proof and the sign-in; `requireOwnerToDelete` no longer
asks such an account for SRP.
- `newKeysPasswordDigest` on both sides, held to each other by a test; a
challenge purpose of its own for the phrase's proof while signed in.
- Members ask the register for all of it; `RemoteRegister` and the
instance API carry the five new operations and the deletion's proof.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 22:11:49 +02:00
|
|
|
|
minutes under its attempt as D20's confirmation. Then the changes, each through the
|
|
|
|
|
|
register: the first keys (and handle, where none) of an account SimpleLogin was added
|
|
|
|
|
|
to (`keys.set`), with the sign-in alone, as there is no password to prove yet; a new
|
|
|
|
|
|
password, a lost one with the phrase (`keys.beginPhrase`, `keys.resetPassword`), a new
|
|
|
|
|
|
phrase and the deletion (`account.delete`'s `keysProof`), each with the proof and the
|
|
|
|
|
|
sign-in, which the change takes: one change for each sign-in. A new or lost password
|
feat(base): mending a broken SimpleLogin link with the handle and the recovery phrase (ADR 0020, D7)
Someone whose SimpleLogin identity is new to the register but who has an
account ("I already have one") names their handle and proves the
account's recovery phrase; the new identity takes the old one's place.
- `simpleLogin.beginMend(attempt, handle)`: the keys wrapped under the
phrase's key, the public keys and a nonce, for the account with that
handle (whatever its case). Only an account with keys and a SimpleLogin
identity is mended, a link it had, as nobody adds SimpleLogin signed
out (D10); a handle nobody has, or any other account's, gets a decoy
from `signInDecoyKey`, the same each time. At most 10 an hour per
handle and 30 per network.
- `simpleLogin.mend(attempt, proof)`: the phrase's proof for `mendLink`,
binding the attempt (`mendLinkDigest`, on both sides), so a proof
relayed from another sign-in links nobody's identity but the device's
own; checked for the waiting identity first, which leaves the proof
unspent. Every session of the account ends, in every service, and the
sign-in stands ten minutes for a change, such as a lost password.
- SimpleLogin needs `SIGN_IN_DECOY_KEY` alone of ADR 0017's three keys
(`signInDecoyKeyOf`); `addInstanceRoutes` stops a server with a
SimpleLogin app and without it (`checkSimpleLoginMending`).
- Members ask the register for both; `RemoteRegister` and the instance
API carry them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 22:26:58 +02:00
|
|
|
|
ends every other session, as ADR 0017's change does. Then mending a broken link
|
|
|
|
|
|
(8, D7): `simpleLogin.beginMend(attempt, handle)` and `simpleLogin.mend(attempt,
|
|
|
|
|
|
proof)`, the phrase's proof binding the attempt, so a relayed proof links nobody
|
|
|
|
|
|
else's identity. Otherwise than proposed: only an account with a SimpleLogin identity
|
|
|
|
|
|
is mended (D10: nobody adds SimpleLogin signed out), every other answered with a
|
|
|
|
|
|
decoy; mending ends every session of the account, as whoever came in through the old
|
|
|
|
|
|
link may still be in; and SimpleLogin needs `SIGN_IN_DECOY_KEY` alone of ADR 0017's
|
|
|
|
|
|
three keys, a server with a SimpleLogin app and without it stopping as it starts.
|
feat(base): handles hidden until their people go public, every answer about a new handle counted (ADR 0020, D5)
A handle's page and profiles.byHandle answer as for a handle nobody has
until its person goes public: a handle fixed by a publish, or a display
name, a bio, a link or an avatar (isPublic). A person's own profile, and
the host's own lookups (personWithHandle), are unchanged.
So every answer about a new handle now tells that an account has it,
and countHandleCheck counts them together on the register, 30 an hour
per person (or attempt) and 60 per network, refused beyond with
ProfileRefusal.tooManyHandleChecks: profile.checkHandle and saving a
profile with a handle new for the person, simpleLogin.checkHandle and
signUp, keys.set with a handle, and Halkyon Learn's inbox.block and
unblock, which look people up by handle. A person's own handle and what
is no handle aren't counted. The SimpleLogin limiters of commit 3 give
way to it, so a network's 60 are shared between people signed in and
people new. The register's checkHandle, saveProfile,
signUpWithSimpleLogin and setFirstKeys carry the visitor's network from
the member that asked.
Halkyon Learn's handle dialog words the new refusal (English, German).
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 22:39:14 +02:00
|
|
|
|
Then handles (9, D5): `/@handle` and `profiles.byHandle` answer as for a handle nobody
|
|
|
|
|
|
has until its person goes public (a handle fixed by a publish, or a display name, a
|
|
|
|
|
|
bio, a link or an avatar), the host's own code still finding whose a handle is.
|
|
|
|
|
|
Otherwise than proposed: not only asking whether a handle is free is limited, but
|
|
|
|
|
|
every answer about a new handle, counted together (`countHandleCheck`, 30 an hour per
|
|
|
|
|
|
person or attempt, 60 per network): saving a profile with one, signing up and the
|
|
|
|
|
|
first keys with one, and Halkyon Learn's blocks by handle, as each tells the same, so
|
|
|
|
|
|
a limit on checking alone would limit nothing; a person's own handle and what is no
|
|
|
|
|
|
handle aren't counted. The handle suggested at sign-up is not counted: it tells at
|
|
|
|
|
|
most that the name SimpleLogin shared is someone's handle already, once a sign-in.
|
|
|
|
|
|
Step 3's deviations accepted by the developer (23 Oct, night).
|
2026-10-24 11:56:26 +02:00
|
|
|
|
- **Built (24 Oct 2026), step 4 so far:** halkyon_app's sign-in with SimpleLogin (the
|
|
|
|
|
|
start screen's Continue with SimpleLogin and its sheet), then adding it to an account
|
|
|
|
|
|
(D21, D23): the apps read where their person stands (`SimpleLoginStandingCubit`:
|
|
|
|
|
|
nothing to show, linked, or missing) and show it in halkyon_app's
|
|
|
|
|
|
`SimpleLoginSection`, which reads it once where a `SimpleLoginStandingScope` is above
|
|
|
|
|
|
it, as Learn's Profile has it for its first line and GitHub's button too. A cut-off
|
|
|
|
|
|
day in the basics (`SIMPLELOGIN_REQUIRED_FROM`) with D22's banner was built and taken
|
feat(app)!: no anonymous accounts on the device (ADR 0020, D4, D25)
Every account has a way in, so the apps make no anonymous account any
more. The start screen shows whenever nobody is signed in, a first
start included, and offers SimpleLogin, a password and GitHub for those
who have it, with no start without an account. An instance out of reach
is said so, with Try again, and one offering no way to create an account
too. An anonymous account's session left from before is deleted as the
app starts (D25), falling back to signing out on the device.
What only anonymous accounts needed goes: signing in anonymously, the
signed-out mark, adding GitHub to an account and switching to a GitHub
account from the Profile tab (with its Cancel, the only one), an
anonymous device's own keys, the warning that signing out deletes the
account, and the texts about keeping progress. Settings says on which
instance the account is signed in. Learn's publishing, reports and
suggestions and its learner count no longer ask whether the person has
an identity: everyone signed in has one.
The server still offers anonymous sign-in until the next commit, so
older apps keep working until v0.20.0 raises the minimum build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 14:50:55 +02:00
|
|
|
|
out again before any release (D24). Then the apps' half of D25 (24 Oct, night): no
|
|
|
|
|
|
app makes an anonymous account. The start screen shows whenever nobody is signed in,
|
|
|
|
|
|
a first start included, and offers no start without an account. It says so when the
|
|
|
|
|
|
instance is out of reach (Try again), and when the instance offers no way to create
|
|
|
|
|
|
an account. An anonymous account's session left from before is deleted as the app
|
|
|
|
|
|
starts (`AuthRepository.endAnonymousSession`), so it goes at once rather than with
|
|
|
|
|
|
the next day's pruning. Where the instance can't be reached, the device only signs
|
|
|
|
|
|
it out. Adding GitHub, the Profile tab's GitHub sign-in with its Cancel, the
|
|
|
|
|
|
signed-out mark and an anonymous device's own keys went with it. The server's half
|
|
|
|
|
|
follows.
|
2026-10-23 13:41:22 +02:00
|
|
|
|
- **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.
|
2026-10-23 23:35:46 +02:00
|
|
|
|
- **D9 Apps come back through their scheme or the loopback** (corrected 23 Oct, night,
|
|
|
|
|
|
by the developer, after research; decided first as verified links for phones on
|
|
|
|
|
|
learn.halkyon.eu, Android's App Links and iOS's https callbacks): Android, iOS and
|
|
|
|
|
|
macOS come back through the app's scheme, which the system's browser sheet catches
|
|
|
|
|
|
(Chrome's Auth Tab, since Chrome 132, even after the forwarding page's redirect;
|
|
|
|
|
|
`ASWebAuthenticationSession`), Linux and Windows through the loopback.
|
|
|
|
|
|
`simpleLogin.begin` takes the way back (`SimpleLoginReturn`: `loopback`, `scheme`),
|
|
|
|
|
|
not the platform. Verified links protected nothing here: a code caught on its way is
|
|
|
|
|
|
worthless without the attempt's ID (4), and a hostile app beginning an attempt of its
|
|
|
|
|
|
own can ask for the scheme or the loopback, which other platforms need open (an
|
|
|
|
|
|
Android app can listen on `127.0.0.1` too). They cost: debug builds and other
|
|
|
|
|
|
instances need the scheme anyway, Firefox has no Auth Tab, Samsung Internet stays on
|
|
|
|
|
|
the page, and flutter_web_auth_2 (v5.1.0) has open bugs with https callbacks (#183,
|
|
|
|
|
|
#215). The scheme is what AppAuth calls the most widely supported.
|
2026-10-23 13:41:22 +02:00
|
|
|
|
- **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.
|
2026-10-23 19:49:38 +02:00
|
|
|
|
- **D12 The ID token checked with `dart_jsonwebtoken` itself** (23 Oct, evening,
|
|
|
|
|
|
recommended), already in the lock through Serverpod's sign-ins. The developer first
|
|
|
|
|
|
chose Serverpod's own verifier (`IdTokenVerifier`, which Google's and Firebase's
|
|
|
|
|
|
sign-ins use; not exported, so imported from its `src/`), which can't: SimpleLogin's
|
|
|
|
|
|
ID tokens carry no `typ` in their header (`jose_utils.make_id_token` writes `alg` and
|
|
|
|
|
|
`kid` alone, and jwcrypto 0.8 adds nothing), and the verifier calls
|
|
|
|
|
|
`JWT.verify` with its default `checkHeaderType: true`, so it refuses every one, and
|
|
|
|
|
|
takes no options to change that. It also tries every key rather than the one the
|
|
|
|
|
|
`kid` names, never fetches the keys again for an unknown `kid` (an hour of failed
|
|
|
|
|
|
sign-ins after SimpleLogin changes its key) and fetches them without a timeout. So:
|
|
|
|
|
|
`checkHeaderType` off, `RS256` alone, the key the `kid` names, fetched again for one
|
2026-10-23 19:54:55 +02:00
|
|
|
|
unknown, with a timeout (over adapting a copy of Serverpod's verifier, or `jose`); in
|
|
|
|
|
|
the OpenID Connect module the sign-in is built on (ADR 0023).
|
2026-10-23 19:49:38 +02:00
|
|
|
|
- **D13 Attempts in a table** (recommended, over the server's memory): a restart or a
|
|
|
|
|
|
deploy in the middle of a sign-in loses none, and the database takes each once.
|
|
|
|
|
|
- **D14 Steps 2 to 4 in one release** (recommended, over the server's side alone first):
|
|
|
|
|
|
the module's side, the keys without SRP with handles and without anonymous accounts,
|
|
|
|
|
|
and the apps' screens come as one release, so people get a sign-in that works; the
|
|
|
|
|
|
cut-off (step 5) follows 30 days after it.
|
|
|
|
|
|
- **D15 The proposal (1–12) as written** (23 Oct, evening); what building it shows
|
|
|
|
|
|
otherwise comes back to the developer.
|
feat(base): signing in with SimpleLogin through the register (ADR 0020, step 2)
The `simpleLogin` endpoint on halkyon_oidc's SimpleLogin preset: `begin`
for the app and platform (the state's marker for the forwarding page), at
most 30 an hour per network; `complete` with SimpleLogin's code, signed out
a session for the account with that SimpleLogin ID, signed in SimpleLogin
added to the caller's account, who becomes identified and anonymous no
longer. Someone new gets no account yet: their identity waits 30 minutes
under the attempt's ID (D16), with a handle suggested from their name,
`anna_mueller` (D17), for the sign-up of step 3.
The register alone holds the instance's SimpleLogin app
(`simpleLoginClientId`, `simpleLoginClientSecret`) and exchanges codes; a
member asks it for both steps and issues its own session. `SIGN_IN_WAYS`
says which ways the register offers, the instance's basics and the ways
in carry `simpleLogin`, GitHub links nobody more where SimpleLogin is
offered, and Learn's and Todo's pruning forgets the unfinished sign-ins
hourly. No migration: the tables are halkyon_oidc's, already in place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 21:03:15 +02:00
|
|
|
|
- **D16 A newcomer's identity waits 30 minutes** (23 Oct, night, recommended, over the
|
|
|
|
|
|
attempt's ten minutes or a day): long enough to choose a handle and make the keys, or
|
|
|
|
|
|
to find the recovery phrase and mend the link (8); after that they sign in with
|
|
|
|
|
|
SimpleLogin again, whose consent page they then skip. Nothing is reserved meanwhile,
|
|
|
|
|
|
the suggested handle neither.
|
|
|
|
|
|
- **D17 Handles suggested as `anna_mueller`** (23 Oct, night, recommended): the name in
|
|
|
|
|
|
lower case, its words joined by underscores; German's letters as German writes them
|
|
|
|
|
|
without (ü as ue, ß as ss), æ, œ and þ as ae, oe and th, every other Latin letter
|
|
|
|
|
|
without its mark (é as e, ø as o); apostrophes dropped, anything else between words;
|
|
|
|
|
|
cut to 30 characters, and where the handle is taken (whatever its case) or kept by the
|
|
|
|
|
|
instance, a number from 2 to 99 after it. None where nothing of the name is left (a
|
|
|
|
|
|
name in another script): the person types one.
|
2026-10-23 21:19:31 +02:00
|
|
|
|
- **D18 Signing up in one call** (23 Oct, night, recommended, over the proposal's two
|
|
|
|
|
|
steps): the device takes the handle, makes the keys, asks the password twice and shows
|
|
|
|
|
|
the recovery phrase (three of its words typed back), then `simpleLogin.signUp` makes
|
|
|
|
|
|
the account and its session together, the newcomer's identity taken once. No account is
|
|
|
|
|
|
ever unfinished, so D11's deletion after a day has nothing left to delete.
|
|
|
|
|
|
- **D19 The password proven with a proof key of its own, the keys only after it** (23
|
|
|
|
|
|
Oct, night, recommended, over the keys coming with the session, as Bitwarden's SSO
|
|
|
|
|
|
has it, and over keeping SRP): a third subkey of the password's Argon2id seeds an
|
|
|
|
|
|
Ed25519 key, whose public half the register keeps; the device signs a one-time nonce
|
|
|
|
|
|
with it as it signs with the recovery phrase's (ADR 0017, the same challenges and
|
|
|
|
|
|
message). A new device gets the wrapped keys once it has proven the password, so
|
|
|
|
|
|
someone who took over a SimpleLogin account can only guess the password online,
|
|
|
|
|
|
against the limits, never offline. SimpleLogin accounts keep no SRP verifier.
|
|
|
|
|
|
- **D20 Changes take the password and a fresh SimpleLogin sign-in** (23 Oct, night,
|
|
|
|
|
|
recommended, over the password's proof alone): a new password, a new recovery phrase
|
|
|
|
|
|
and deleting the account take the password's proof (the phrase's, for a lost
|
|
|
|
|
|
password) and a SimpleLogin sign-in of the last ten minutes, the two factors of ADR
|
|
|
|
|
|
0017's third reading with SimpleLogin's and Proton's own second factor in TOTP's
|
|
|
|
|
|
place.
|
2026-10-24 11:56:26 +02:00
|
|
|
|
- **D21 Adding SimpleLogin is a shared part of the account section** (24 Oct,
|
|
|
|
|
|
recommended, over a sheet of its own or each app's own screen): halkyon_app's
|
|
|
|
|
|
SimpleLogin part, which each app places above "Password and authenticator" (Learn in
|
|
|
|
|
|
its Profile tab's account card, below "Ways to sign in"; Todo in its account sheet):
|
|
|
|
|
|
a heading and Add SimpleLogin ("Sign in with SimpleLogin, or with your Proton
|
|
|
|
|
|
account."), which opens the sign-in sheet to add it; once added, one more way in, a
|
|
|
|
|
|
row without heading lined up with the others, and "SimpleLogin added." Learn's ways
|
|
|
|
|
|
in (GitHub, the address) stay listed. (Its reminder in red went with D24.)
|
|
|
|
|
|
- **D22 The reminder is a banner at the top of each app's home** (24 Oct, recommended,
|
|
|
|
|
|
over the section's red line alone or a notice once a week), in the place and colours
|
|
|
|
|
|
of the banner saying the app is too old for the instance, from the release on, not
|
|
|
|
|
|
dismissible, until SimpleLogin is added: both days and Add SimpleLogin. Dropped by
|
|
|
|
|
|
D24 the same day.
|
|
|
|
|
|
- **D23 Anonymous people keep their account with SimpleLogin alone** (24 Oct,
|
|
|
|
|
|
recommended, over offering GitHub beside it until the cut-off): where the instance
|
|
|
|
|
|
offers SimpleLogin, Learn's Profile offers an anonymous person Add SimpleLogin ("Add
|
|
|
|
|
|
SimpleLogin to keep it") and no longer "Sign in with GitHub", and GitHub is added to
|
|
|
|
|
|
nobody there (`GitHubOffer.linking`); the start screen still signs in a GitHub
|
|
|
|
|
|
account someone has, until GitHub ends.
|
|
|
|
|
|
- **D24 No cut-off machinery while the developer is learn.halkyon.eu's only person**
|
|
|
|
|
|
(24 Oct, the developer: "There is no other user than me, currently. So, is that really
|
|
|
|
|
|
necessary?", then "Please continue" to dropping it): the cut-off (D8) becomes
|
|
|
|
|
|
configuration. The developer adds SimpleLogin to their account (D21), then the
|
|
|
|
|
|
operator takes GitHub's OAuth app out of `deploy/.env` (`GITHUB_CLIENT_ID`,
|
|
|
|
|
|
`GITHUB_CLIENT_SECRET`), `SIGN_IN_WAYS=simplelogin` keeping passwords and codes off.
|
|
|
|
|
|
So no cut-off day, no reminder (11, D22, the section's red line), no ending of
|
|
|
|
|
|
sessions and no deletion 30 days later (D10), no step 5, and no
|
|
|
|
|
|
`ANONYMOUS_SESSIONS_END` (10), as no other instance has anonymous people to move.
|
|
|
|
|
|
Built and unreleased, the day and the banner went again at once (over keeping them
|
|
|
|
|
|
idle): nothing has read the basics' fields, and an instance with people to move can
|
|
|
|
|
|
take them back from git (`f60d52e7` on `feat/simplelogin-sign-in` before it was
|
|
|
|
|
|
amended). Anonymous accounts still go (D4) and handles are still required (D5): that
|
|
|
|
|
|
is where the design ends, not a migration.
|
feat(app)!: no anonymous accounts on the device (ADR 0020, D4, D25)
Every account has a way in, so the apps make no anonymous account any
more. The start screen shows whenever nobody is signed in, a first
start included, and offers SimpleLogin, a password and GitHub for those
who have it, with no start without an account. An instance out of reach
is said so, with Try again, and one offering no way to create an account
too. An anonymous account's session left from before is deleted as the
app starts (D25), falling back to signing out on the device.
What only anonymous accounts needed goes: signing in anonymously, the
signed-out mark, adding GitHub to an account and switching to a GitHub
account from the Profile tab (with its Cancel, the only one), an
anonymous device's own keys, the warning that signing out deletes the
account, and the texts about keeping progress. Settings says on which
instance the account is signed in. Learn's publishing, reports and
suggestions and its learner count no longer ask whether the person has
an identity: everyone signed in has one.
The server still offers anonymous sign-in until the next commit, so
older apps keep working until v0.20.0 raises the minimum build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 14:50:55 +02:00
|
|
|
|
- **D25 Anonymous accounts that exist are deleted** (24 Oct, night, recommended, over a
|
|
|
|
|
|
way to keep them until the developer has moved): the developer's own account isn't
|
|
|
|
|
|
anonymous. From the release on, an app signs an anonymous session out, and the
|
|
|
|
|
|
module's daily pruning deletes every anonymous person left, whatever their age,
|
|
|
|
|
|
through the same deletion as any account's (each service's own data with it). Nothing
|
|
|
|
|
|
turns an anonymous account into one with a way in any more: D23's anonymous half,
|
|
|
|
|
|
10's "until the cut-off", GitHub's upgrade, a password or SimpleLogin added to one.
|
|
|
|
|
|
- **D26 No handle suggested at password sign-up** (24 Oct, night, recommended, over the
|
|
|
|
|
|
address's part before the @): the field starts empty and says whether the handle is
|
|
|
|
|
|
free as it is typed; a suggestion from the address would show part of it to everyone.
|
|
|
|
|
|
- **D27 Removing a profile makes it private** (24 Oct, night, recommended, over
|
|
|
|
|
|
refusing it): the handle stays the account's, what is filled in goes (display name,
|
|
|
|
|
|
bio, links, avatar), and `/@handle` answers as for an unknown handle until the person
|
|
|
|
|
|
goes public again (D5). Once they have published something it is refused as before:
|
|
|
|
|
|
others refer to the handle.
|
|
|
|
|
|
- **D28 No prompt for an account without a handle** (24 Oct, night, recommended, over a
|
|
|
|
|
|
step after signing in): the developer sets theirs in Profile; a handle is asked where
|
|
|
|
|
|
one is required and the account has none (a password added, the keys set).
|
2026-10-23 13:41:22 +02:00
|
|
|
|
- **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
|
2026-10-23 23:35:46 +02:00
|
|
|
|
`https://<register's host>/simplelogin-callback`, the one address registered. The
|
|
|
|
|
|
instance's page there forwards code and `state` to the app (D9): the loopback on
|
|
|
|
|
|
Linux and Windows (`http://127.0.0.1:43823/simplelogin-callback`, each app its own
|
|
|
|
|
|
port) or the app's scheme on Android, iOS and macOS
|
|
|
|
|
|
(`app.halkyon.learn://simplelogin-callback`). Its targets
|
|
|
|
|
|
are fixed per app and way back, chosen by a marker in `state`, so it sends nobody
|
2026-10-23 13:41:22 +02:00
|
|
|
|
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
|
2026-10-23 23:35:46 +02:00
|
|
|
|
on the device beginning an attempt of its own isn't stopped, verified links or not
|
|
|
|
|
|
(D9): SimpleLogin answers without a consent page for an app already allowed.) It keeps the `sub` alone
|
2026-10-23 13:41:22 +02:00
|
|
|
|
(`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.
|
2026-10-23 23:35:46 +02:00
|
|
|
|
- A hostile app on a person's device can begin a sign-in of its own and, once the person
|
|
|
|
|
|
has allowed Halkyon at SimpleLogin, come back with a session without a consent page
|
|
|
|
|
|
(4, D9); what it gets is the account's server-side data, while private studysets,
|
|
|
|
|
|
tags and settings stay sealed under the keys (ADR 0019). Whether SimpleLogin honours
|
|
|
|
|
|
OpenID Connect's `prompt=consent`, which would show the consent page every time, is
|
|
|
|
|
|
still to be checked.
|