feat(base)!: no anonymous accounts on the server (ADR 0020, D25)
The server's half of D25: every account has a way in, and the anonymous accounts left from before go with the daily pruning. - The module signs nobody in anonymously: `anonymousIdp` and `gitHubUpgrade` are gone, and so is linking GitHub at the register. - A password is set only by someone whose address is theirs (a `firstPassword` code) or who signs in with GitHub alone (a fresh sign-in and a `signUp` code); an anonymous account is refused `wrongCredentials` before any code is used. - SimpleLogin refuses to link onto a caller without `identified` (`wrongCaller`), the member's endpoint too, so nothing turns an anonymous account into one with a way in. - `pruneAnonymousPeople` deletes every anonymous account left, however new, never one identified. Learn and Todo schedule it as `prune-accounts` and cancel the old `prune-abandoned-anonymous-people` entry once. - Learn: someone without `identified` keeps no files; `FILE_LIMIT_ANONYMOUS_MB` is gone. `minimumAppBuild` is 23, as builds up to 22 start with an anonymous account. - Learn's sync tests make their person and session by SQL in the development database, as Serverpod stores sessions. - Docs, store listings and ADRs 0010, 0014, 0016, 0017 and 0019 amended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
5b9df73756
commit
87671208df
1 changed files with 837 additions and 1353 deletions
|
|
@ -14,14 +14,12 @@ Endpoints, under the nickname `base` in a host's client (`client.modules.base.em
|
|||
|
||||
| Endpoint | Methods | Who may call |
|
||||
|---|---|---|
|
||||
| `anonymousIdp` | `login` (Serverpod's anonymous sign-in, at most 30 new accounts per network and hour) | anyone |
|
||||
| `password` | `set`: the caller's password, with the address a mailed code proves unless they have one; `beginChange`, `change`: a new password with the old one's proof and a code, every other session ending; `setRecoveryKeys`: a new recovery phrase's wrapping, with the old password's proof and a code | signed in |
|
||||
| `passwordSignIn` | `begin`, `complete`: SRP-6a's two steps with a code from an authenticator (or a recovery code; a setup never finished, a code by mail); a session. `signUp`: an account at the address a mailed code proves, with a password; a session | without a session |
|
||||
| `authenticator` | `enrol`, `confirm` (the first brings ten recovery codes; another takes a code from one confirmed before), `list`, `remove`, `newRecoveryCodes` | signed in |
|
||||
| `accountRecovery` | `resetAuthenticator`, `beginKeyProof`, `resetPassword` (with the recovery phrase), `startAccountReset`, `completeAccountReset` | without a session |
|
||||
| `simpleLogin` | `begin(platform)`: an attempt and SimpleLogin's address to open, at most 30 an hour per network; `complete(attempt, code)`: signed out, a session for the account with that SimpleLogin ID, or for someone new the handle suggested from their name; signed in, SimpleLogin added to the caller's account, who becomes `identified`; for an account, whether it has its keys and its handle, the attempt standing ten minutes for the sign-in; `checkHandle(attempt, handle)`: whether someone new could take a handle, at most 30 an hour per attempt and 60 per network; `signUp(attempt, handle, setup)`, without a session: someone new's account, `identified`, with the handle and the keys the device made; a session; `beginMend(attempt, handle)`, `mend(attempt, proof)`, without a session: a broken link mended with the handle and the recovery phrase, the new identity in the old one's place; a session | anyone |
|
||||
| `simpleLogin` | `begin(platform)`: an attempt and SimpleLogin's address to open, at most 30 an hour per network; `complete(attempt, code)`: signed out, a session for the account with that SimpleLogin ID, or for someone new the handle suggested from their name; signed in (`identified` only, `wrongCaller` otherwise), SimpleLogin added to the caller's account; for an account, whether it has its keys and its handle, the attempt standing ten minutes for the sign-in; `checkHandle(attempt, handle)`: whether someone new could take a handle, at most 30 an hour per attempt and 60 per network; `signUp(attempt, handle, setup)`, without a session: someone new's account, `identified`, with the handle and the keys the device made; a session; `beginMend(attempt, handle)`, `mend(attempt, proof)`, without a session: a broken link mended with the handle and the recovery phrase, the new identity in the old one's place; a session | anyone |
|
||||
| `keys` | for an account that signs in with SimpleLogin (Halkyon Learn's ADR 0020, D19 and D20): `begin`, `unlock`: the caller's keys for their device, once the password is proven with its own proof key; `set`: the first keys (and handle) of an account SimpleLogin was added to, with a SimpleLogin sign-in of the last ten minutes; `changePassword`, `beginPhrase` and `resetPassword` (a lost password, with the recovery phrase), `setRecoveryKeys`: each with the password's proof (the phrase's) and such a sign-in (`KeysOwnerProof`), a new password ending every other session; the tenth wrong proof in a row ends the session, as ADR 0017's do | signed in |
|
||||
| `gitHubUpgrade` | `upgrade`: links a GitHub account to the caller, who becomes `identified`; refused where passwords or SimpleLogin are offered (deprecated) | signed in |
|
||||
| `gitHubSignIn` | `signIn`: a session for whoever has that GitHub account, unless they sign in with a password; none once `GITHUB_SIGN_IN_ENDS` has come | without a session |
|
||||
| `emailCode` | `sendCode`: a code by mail, which proves the address and signs nobody in | anyone |
|
||||
| `account` | `delete` (with the password's proof where the account has one, `keysProof` for one that signs in with SimpleLogin), `signInWays` | signed in |
|
||||
|
|
@ -93,22 +91,23 @@ Endpoints, under the nickname `base` in a host's client (`client.modules.base.em
|
|||
`simpleLogin.begin` takes the way back as a `SimpleLoginReturn`), the rest 1 to 512
|
||||
characters of base64url; anything else gets a page saying the sign-in can't go on,
|
||||
repeating nothing it was given, in English or German. It keeps and logs nothing.
|
||||
- **Per-network limits** (new anonymous accounts, codes by mail, password sign-ins,
|
||||
proofs of the recovery phrase) count by the visitor's IP address (`networkOf`): the
|
||||
- **No anonymous accounts** (Halkyon Learn's ADR 0020, D25): every account has a way
|
||||
in. An anonymous account left from before signs into nothing new and keeps no
|
||||
password or SimpleLogin; the app deletes its own as it starts (`account.delete`), and
|
||||
the pruning deletes the rest (`pruneAnonymousPeople`), whatever their age.
|
||||
- **Per-network limits** (codes by mail, password sign-ins, SimpleLogin's sign-ins,
|
||||
handle checks, proofs of the recovery phrase) count by the visitor's IP address (`networkOf`): the
|
||||
connection's, or, for a connection from a proxy the operator trusts
|
||||
(`TRUSTED_PROXIES`), the last address of `X-Forwarded-For`, the one the proxy saw
|
||||
itself. Never the `Forwarded` header, which Caddy passes on as the visitor wrote it,
|
||||
so never Serverpod's `remoteInfo`. An IPv6 visitor counts by their /64
|
||||
(`networkKeyOf`), which one line or server holds whole. Serverpod's own limit on
|
||||
anonymous accounts is off
|
||||
(it counts by the connection's address, behind a proxy the proxy's for everyone); the
|
||||
endpoint counts them itself.
|
||||
(`networkKeyOf`), which one line or server holds whole.
|
||||
- Refusals are `PasswordRefusedException`, `GitHubSignInRefusedException`,
|
||||
`EmailSignInRefusedException` and `ProfileRefusedException`, each with a reason;
|
||||
`ProfileNotFoundException` where there is no profile.
|
||||
- For the host's own code, `package:halkyon_base_server/base.dart`: the configuration,
|
||||
`tokenManagerBuilders` and `identityProviderBuilders`, the `identified` scope and
|
||||
`grantIdentified`, `deletePerson`, the pruning (`pruneAbandonedAnonymousPeople`,
|
||||
`grantIdentified`, `deletePerson`, the pruning (`pruneAnonymousPeople`,
|
||||
`forgetOldSignUpAttempts`, `forgetExpiredEmailCodes`, `forgetUnconfirmedAuthenticators`,
|
||||
`forgetGitHubAccounts`, `forgetLapsedResets`), short codes, `sourceCodeUrl`,
|
||||
`dataLocation`, `backupLocation`, people's profiles as the host's code reads them
|
||||
|
|
@ -125,9 +124,9 @@ One operator's services share one account per person: the same address, GitHub a
|
|||
and handle reach the same person in each, under one account ID (each service's
|
||||
`AuthUser` has it), and deleting the account deletes it everywhere. One service holds
|
||||
the register, its database keeping the ways in (`email_identity`, GitHub's IDs), the
|
||||
handles and profiles; the others are its members and ask it. Anonymous people stay each
|
||||
service's own until they add a way in, which makes them an account of the register under
|
||||
the same ID. Sessions stay each service's own: every app signs in itself.
|
||||
handles and profiles; the others are its members and ask it. An anonymous account left
|
||||
from before stays its service's own. Sessions stay each service's own: every app signs
|
||||
in itself.
|
||||
|
||||
- **Modes** (`AccountsMode`, `HalkyonBase.accounts`; a host reads it with
|
||||
`AccountsMode.fromEnvironment()`): `HoldsRegister` by default, also for an instance of
|
||||
|
|
@ -151,19 +150,19 @@ the same ID. Sessions stay each service's own: every app signs in itself.
|
|||
asks the register to mail a code (the device's network passed on, as the register
|
||||
counts codes per network) and to check it, or to say whose GitHub ID it is, and opens a
|
||||
session of its own for that account (`issueSession`, which makes them a person there
|
||||
under the account's ID the first time). Linking a way in makes the person identified on
|
||||
under the account's ID the first time). Adding a way in makes the person identified on
|
||||
the register and then on the member; a switch token signs in as the account the
|
||||
address is already. Every refusal is the register's, as on the register itself.
|
||||
- **Deleting:** a deletion on a member asks the register first whose account it is
|
||||
(`knownAccounts`), and is refused while it can't be reached, deleting nothing, a
|
||||
person still anonymous on the member too: the register may have linked them while
|
||||
their becoming identified there failed (the app then gives an anonymous account up
|
||||
all the same, leaving it to the pruning). An account of the register's goes there,
|
||||
their becoming identified there failed (the app then deletes an anonymous account
|
||||
left from before all the same, leaving it to the pruning). An account of the register's goes there,
|
||||
which deletes the account and
|
||||
keeps a notice for each member in the same transaction (`account_deletion_notice`),
|
||||
told at once and again hourly (`deliverDeletionNotices`, tried least first) until each
|
||||
confirms; notices to a member no longer named are dropped. An anonymous person goes
|
||||
from their service alone. Each member also deletes, daily, the identified people the
|
||||
confirms; notices to a member no longer named are dropped. An anonymous account goes
|
||||
from its service alone. Each member also deletes, daily, the identified people the
|
||||
register no longer knows (`forgetPeopleTheRegisterForgot`): it asks about everyone
|
||||
before deleting anyone, deletes nobody while the register knows none of them (a
|
||||
register misnamed or emptied), and fails, deleting nobody, while it can't reach it.
|
||||
|
|
@ -238,12 +237,12 @@ Built on the three parts above, on the register (members forward every step thro
|
|||
3–16 passes, 64 MiB–1 GiB), verifier (1 < v < N, as long as N), public keys (32 bytes
|
||||
each) and both wrappings (24-byte nonce, 112 bytes). It needs an address, and a
|
||||
session alone sets none (ADR 0017's fourth reading): an account with an address proves
|
||||
it anew by a code mailed to it for this (`firstPassword`); an anonymous person by a
|
||||
mailed code (`signUp`) for the address they add, which becomes theirs as email sign-in
|
||||
makes it (anonymous → `identified`, same ID); an account that signs in with GitHub
|
||||
alone does that within 10 minutes of a GitHub sign-in on the session asking
|
||||
it anew by a code mailed to it for this (`firstPassword`); an account that signs in
|
||||
with GitHub alone does that within 10 minutes of a GitHub sign-in on the session asking
|
||||
(`gitHubSignInFreshFor`, the session row's method and creation time, a member telling
|
||||
the register; else `signInAgain`, before any code is used). It signs in once an
|
||||
the register; else `signInAgain`, before any code is used), the address it adds proved
|
||||
by a mailed code (`signUp`). Anyone else, an anonymous account left from before among
|
||||
them, sets none (`wrongCredentials`, before any code is used). It signs in once an
|
||||
authenticator is confirmed; until then a new setup replaces it, never once one was,
|
||||
even while a reset leaves the account without one (a new password comes through the
|
||||
recovery phrase or the account's reset alone).
|
||||
|
|
@ -390,17 +389,14 @@ Built on the three parts above, on the register (members forward every step thro
|
|||
(`takesEffectAt`).
|
||||
- **Older ways:** an account that signs in with a password (it had a confirmed
|
||||
authenticator, which a reset may have taken since: `passwordOnly` in its ways in) no
|
||||
longer signs in with GitHub (`usePassword`); where passwords are offered, no new GitHub
|
||||
account is linked (`deprecated`; one someone else has is still `takenByAnotherPerson`,
|
||||
so an anonymous person's app switches to it); from `GITHUB_SIGN_IN_ENDS` on GitHub signs nobody in
|
||||
longer signs in with GitHub (`usePassword`); no account gets GitHub any more (D25),
|
||||
so it signs in only those who have it; from `GITHUB_SIGN_IN_ENDS` on GitHub signs nobody in
|
||||
(`ended`), the basics stop naming the day (`gitHubSignInEnds`, which apps show until
|
||||
then) and, 30 days later (`gitHubAccountsKeptAfterEnd`, so a date set wrong can be put
|
||||
right first), `forgetGitHubAccounts` deletes every GitHub ID on the register, first
|
||||
deleting the accounts that sign in with GitHub alone (no address, never a password),
|
||||
each on its own (one failing is tried again the next day; one deleted whose host step
|
||||
after failed counts as deleted), the members told once after all of them. `password.set` adds an
|
||||
address without GitHub only for someone anonymous where the session lives
|
||||
(`anonymousHere`, which a member tells the register).
|
||||
after failed counts as deleted), the members told once after all of them.
|
||||
|
||||
## Taking it into a host
|
||||
|
||||
|
|
@ -455,7 +451,7 @@ Built on the three parts above, on the register (members forward every step thro
|
|||
after any literal routes of its own.
|
||||
|
||||
Then schedule the pruning in a future call of the host's own: daily
|
||||
`pruneAbandonedAnonymousPeople`, `forgetGitHubAccounts` (the register's),
|
||||
`pruneAnonymousPeople`, `forgetGitHubAccounts` (the register's),
|
||||
`forgetLapsedResets` and `forgetPeopleTheRegisterForgot` (a member's), hourly
|
||||
`forgetOldSignUpAttempts`, `forgetExpiredEmailCodes`,
|
||||
`forgetUnconfirmedAuthenticators`, `forgetUnfinishedSimpleLogins` (the
|
||||
|
|
|
|||
Loading…
Reference in a new issue