docs(auth): what building the register's side settled
ADR 0017 records the decisions taken in building (SRP's identity, the decoys, the 72-hour account reset proposed for confirmation, GitHub's deprecation) and what step 3 must do; the module's README and the AGENTS files say how password sign-in works. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
5f646a4e6b
commit
c67b492bfd
4 changed files with 139 additions and 18 deletions
|
|
@ -9,7 +9,7 @@ Halkyon's apps and the packages they share, in one repository and one pub worksp
|
||||||
- **`apps/<app>/`**: each app's packages, as `serverpod create` lays them out (`halkyon_<app>_server`, `_client`, `_flutter`), plus what is the app's alone (Learn's curator, the scripts writing Learn's assets in `apps/learn/tool/`, run from `apps/learn`). Learn is the app that runs; Todo is still Serverpod's scaffold; Train and Meditate are to come.
|
- **`apps/<app>/`**: each app's packages, as `serverpod create` lays them out (`halkyon_<app>_server`, `_client`, `_flutter`), plus what is the app's alone (Learn's curator, the scripts writing Learn's assets in `apps/learn/tool/`, run from `apps/learn`). Learn is the app that runs; Todo is still Serverpod's scaffold; Train and Meditate are to come.
|
||||||
- **`packages/`**: what the apps share, each with its README and licence, which the apps take by path (`path: ../../../packages/<package>`), so a change to a shared package and to the apps using it is one commit:
|
- **`packages/`**: what the apps share, each with its README and licence, which the apps take by path (`path: ../../../packages/<package>`), so a change to a shared package and to the apps using it is one commit:
|
||||||
- `halkyon_ui`: Halkyon's look, the theme around cobalt and copper, Noto Sans bundled, the window-size classes and the widgets every screen is built of, with texts of its own (English, German).
|
- `halkyon_ui`: Halkyon's look, the theme around cobalt and copper, Noto Sans bundled, the window-size classes and the widgets every screen is built of, with texts of its own (English, German).
|
||||||
- `halkyon_base_server`, `halkyon_base_client`: the Serverpod module every Halkyon server includes (ADR 0014): signing in (anonymous, GitHub, email codes), an account's ways in and its deletion with a hook for each app's own data, the pruning, the instance's basics, people's handles and public profiles, and the instance's register (ADR 0016): one account per person across an operator's services, held by one service (Learn's server on learn.halkyon.eu) and asked by the others, its members (`ACCOUNT_REGISTER_URL`), all sharing a secret (`INSTANCE_SECRET`); its README says how.
|
- `halkyon_base_server`, `halkyon_base_client`: the Serverpod module every Halkyon server includes (ADR 0014): signing in (anonymous, a password with TOTP and the resets of ADR 0017, email codes, GitHub until it ends), an account's ways in and its deletion with a hook for each app's own data, the pruning, the instance's basics, people's handles and public profiles, and the instance's register (ADR 0016): one account per person across an operator's services, held by one service (Learn's server on learn.halkyon.eu) and asked by the others, its members (`ACCOUNT_REGISTER_URL`), all sharing a secret (`INSTANCE_SECRET`); its README says how.
|
||||||
- `halkyon_app`: every app's shell on the module's client (ADR 0014): the instances a device keeps accounts on and the first start, signing in and switching person, the gates building an app's dependencies per instance and per person, the sync service and where it stands, the account section of an app's settings (`AccountTiles`), About with the instance's source link (`showAppAbout`), the language every app picks (`resolveAppLocale`) and countries' and subdivisions' names (Unicode CLDR's, bundled), with texts that name the app.
|
- `halkyon_app`: every app's shell on the module's client (ADR 0014): the instances a device keeps accounts on and the first start, signing in and switching person, the gates building an app's dependencies per instance and per person, the sync service and where it stands, the account section of an app's settings (`AccountTiles`), About with the instance's source link (`showAppAbout`), the language every app picks (`resolveAppLocale`) and countries' and subdivisions' names (Unicode CLDR's, bundled), with texts that name the app.
|
||||||
- `halkyon_core` (low-level primitives in pure Dart: ids, a logger, `SerialQueue`, `CoalescingRunner`) and `halkyon_interop` (contracts between apps, pure Dart).
|
- `halkyon_core` (low-level primitives in pure Dart: ids, a logger, `SerialQueue`, `CoalescingRunner`) and `halkyon_interop` (contracts between apps, pure Dart).
|
||||||
- `halkyon_keys` (pure Dart on libsodium, ADR 0017): an account's keys on the device, the password's Argon2id and SRP's x, the key pairs and their wrapping, the recovery phrase, data keys, content sealed and signed. SRP-6a's two halves are the module's (`halkyon_base_server`'s `base.dart`, `halkyon_base_client`'s `srp.dart`), as are TOTP and the recovery codes. `sodium`'s build hook compiles libsodium, so every machine that builds or tests needs a C compiler and `make`.
|
- `halkyon_keys` (pure Dart on libsodium, ADR 0017): an account's keys on the device, the password's Argon2id and SRP's x, the key pairs and their wrapping, the recovery phrase, data keys, content sealed and signed. SRP-6a's two halves are the module's (`halkyon_base_server`'s `base.dart`, `halkyon_base_client`'s `srp.dart`), as are TOTP and the recovery codes. `sodium`'s build hook compiles libsodium, so every machine that builds or tests needs a C compiler and `make`.
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,8 @@ A spaced-repetition learning app: studysets of flashcards, reviewed on an FSRS s
|
||||||
## Rules the code relies on
|
## Rules the code relies on
|
||||||
|
|
||||||
- **Authentication**: server-side sessions (`ServerSideSessionsConfigFromPasswords`), not JWT, for immediate revocation. Scopes are copied onto the session row, so changing a person's scopes means updating their session rows too.
|
- **Authentication**: server-side sessions (`ServerSideSessionsConfigFromPasswords`), not JWT, for immediate revocation. Scopes are copied onto the session row, so changing a person's scopes means updating their session rows too.
|
||||||
- **Identity**: `AuthUser` is the person; identity providers (anonymous, GitHub, an email address) attach to it. Content references `AuthUser`, never a provider account. A person may sign in with GitHub and with an address, one of each (`account.signInWays`); the Profile tab lists their ways in and adds the missing one.
|
- **Identity**: `AuthUser` is the person; identity providers (anonymous, GitHub, an email address, a password) attach to it. Content references `AuthUser`, never a provider account. A person may sign in with GitHub and with an address, one of each (`account.signInWays`, which also says whether they have a password and a confirmed authenticator); the Profile tab lists their ways in and adds the missing one.
|
||||||
|
- **A password and an authenticator** (ADR 0017; the server's side built 2026-10-20, the app's screens to come; halkyon_base's README): the address, a password that never leaves the device (SRP-6a, `passwordSignIn.begin` and `complete`, the verifier and the account's wrapped keys on the register) and a code from an authenticator app (TOTP, mandatory) or a recovery code. An address nobody has is answered as an account's would be (decoys from `signInDecoyKey`), and every wrong credential is refused alike (`wrongCredentials`). Lost authenticator: the password and a mailed code (or the recovery phrase) turn it off 72 hours later, a mail at the start, a sign-in with a code calling it off; lost password: the recovery phrase's signature and a code set a new one, keys kept, or without the phrase a mailed code resets the whole account (new keys) after the same 72 hours. An account with a password and a confirmed authenticator no longer signs in by a mailed code or GitHub (`usePassword`); where passwords are offered, no new GitHub account is linked (`deprecated`), and from `GITHUB_SIGN_IN_ENDS` on GitHub signs nobody in and pruning forgets every GitHub ID (D2). Password sign-in is on only where the passwords hold `totpKey`, `recoveryCodeKey` and `signInDecoyKey` (`TOTP_KEY`, `RECOVERY_CODE_KEY`, `SIGN_IN_DECOY_KEY` in `deploy/.env`) and email sign-in is set up (`passwordSignIn` in `instance.basics()`).
|
||||||
- **One account across the instance** (ADR 0016, decided 2026-10-19; halkyon_base's README): Learn's server holds the instance's register, so its tables keep every service's ways in, handles and profiles, and an account's ID is the `AuthUser` ID in every service. The instance's other services (Todo) are its members: they ask it over its web server (`/halkyon-base/register/…`, `addInstanceRoutes` in `server.dart`, never forwarded by Caddy) with the secret the services share (`instanceSecret`, `INSTANCE_SECRET` in `deploy/.env`), and a member's anonymous person adding a way in becomes an account here under the same ID. Deleting an account (here, or a member passing it on) keeps a notice per member in the same transaction (`account_deletion_notice`), told at once and hourly until each confirms (`tellMembersOfDeletions`). Learn's endpoints and code go through the module as on any register; nothing changes for Learn's apps.
|
- **One account across the instance** (ADR 0016, decided 2026-10-19; halkyon_base's README): Learn's server holds the instance's register, so its tables keep every service's ways in, handles and profiles, and an account's ID is the `AuthUser` ID in every service. The instance's other services (Todo) are its members: they ask it over its web server (`/halkyon-base/register/…`, `addInstanceRoutes` in `server.dart`, never forwarded by Caddy) with the secret the services share (`instanceSecret`, `INSTANCE_SECRET` in `deploy/.env`), and a member's anonymous person adding a way in becomes an account here under the same ID. Deleting an account (here, or a member passing it on) keeps a notice per member in the same transaction (`account_deletion_notice`), told at once and hourly until each confirms (`tellMembersOfDeletions`). Learn's endpoints and code go through the module as on any register; nothing changes for Learn's apps.
|
||||||
- **GitHub keeps only the numeric GitHub ID**: our own slim login (halkyon_base's `github.dart`) on Serverpod's GitHub helpers; Serverpod's stock GitHub login endpoint, which stores name, email and avatar, is never exposed. GitHub sign-in is on only where the passwords hold `githubClientId` and `githubClientSecret` (in production `GITHUB_CLIENT_ID`/`GITHUB_CLIENT_SECRET` in `deploy/.env`); the app asks halkyon_base's `instance.basics()`.
|
- **GitHub keeps only the numeric GitHub ID**: our own slim login (halkyon_base's `github.dart`) on Serverpod's GitHub helpers; Serverpod's stock GitHub login endpoint, which stores name, email and avatar, is never exposed. GitHub sign-in is on only where the passwords hold `githubClientId` and `githubClientSecret` (in production `GITHUB_CLIENT_ID`/`GITHUB_CLIENT_SECRET` in `deploy/.env`); the app asks halkyon_base's `instance.basics()`.
|
||||||
- **Linking keeps the person** (`GitHubUpgradeEndpoint`, `requireLogin`): the GitHub ID joins the signed-in anonymous person, who becomes identified and loses the anonymous identity, in one transaction. **Signing in** (`GitHubSignInEndpoint`) only reaches people who have a GitHub account already, and only without a session: Serverpod refuses to issue another person's session inside a signed-in call. Refusals are `GitHubSignInRefusedException` with a reason.
|
- **Linking keeps the person** (`GitHubUpgradeEndpoint`, `requireLogin`): the GitHub ID joins the signed-in anonymous person, who becomes identified and loses the anonymous identity, in one transaction. **Signing in** (`GitHubSignInEndpoint`) only reaches people who have a GitHub account already, and only without a session: Serverpod refuses to issue another person's session inside a signed-in call. Refusals are `GitHubSignInRefusedException` with a reason.
|
||||||
|
|
@ -118,7 +119,7 @@ A spaced-repetition learning app: studysets of flashcards, reviewed on an FSRS s
|
||||||
- **Coming back with a field focused brings its keyboard back** (`KeyboardBackOnResume` around the app): Flutter (v3.47.5) gives the window back the keyboard's height after the app was left while the keyboard moved, with no keyboard showing, which left screens pushed up ([flutter#191156](https://github.com/flutter/flutter/issues/191156), open). Remove it once that is fixed.
|
- **Coming back with a field focused brings its keyboard back** (`KeyboardBackOnResume` around the app): Flutter (v3.47.5) gives the window back the keyboard's height after the app was left while the keyboard moved, with no keyboard showing, which left screens pushed up ([flutter#191156](https://github.com/flutter/flutter/issues/191156), open). Remove it once that is fixed.
|
||||||
- **One person's data per device**: `PersonFolders` wipes every other person's folder when a person's data opens: their sessions ended on this device. Opening waits for the previous person's database to close.
|
- **One person's data per device**: `PersonFolders` wipes every other person's folder when a person's data opens: their sessions ended on this device. Opening waits for the previous person's database to close.
|
||||||
- **Session logs are not persisted** (`persistentEnabled: false` in every run mode) for privacy. Don't re-enable them.
|
- **Session logs are not persisted** (`persistentEnabled: false` in every run mode) for privacy. Don't re-enable them.
|
||||||
- **Secrets** live in `apps/learn/halkyon_learn_server/config/passwords.yaml` (git-ignored). Never change `serverSideSessionKeyHashPepper`: it would sign everyone out, and anonymous accounts can't sign back in. Never change `emailSignInPepper` either: every address's hash would be lost, and with it those people's way in.
|
- **Secrets** live in `apps/learn/halkyon_learn_server/config/passwords.yaml` (git-ignored). Never change `serverSideSessionKeyHashPepper`: it would sign everyone out, and anonymous accounts can't sign back in. Never change `emailSignInPepper` either: every address's hash would be lost, and with it those people's way in. Never change `totpKey` (every authenticator stops working) or `recoveryCodeKey` (every recovery code), nor `signInDecoyKey` without need (it shows which addresses were unused to whoever watched).
|
||||||
- **Checks on a device** live in `apps/learn/halkyon_learn_flutter/integration_test/` and run by hand, never in `tool/check.dart` or CI: `flutter test integration_test/<file> -d <device>`. `pdf_engine_test.dart` checks the real PDF engine (on the Linux desktop too), `youtube_player_test.dart` the embedded YouTube player (Android), `jellyfin_player_test.dart` the video player streaming from a Jellyfin server (`--dart-define`s for the server and the person, in the file), `japanese_analyser_test.dart` MeCab as its build hook compiles it, with the curator's IPADIC pack and an instance's JMdict export (`--dart-define`s for both). Never on the emulator from an agent session: it takes the editor down with it.
|
- **Checks on a device** live in `apps/learn/halkyon_learn_flutter/integration_test/` and run by hand, never in `tool/check.dart` or CI: `flutter test integration_test/<file> -d <device>`. `pdf_engine_test.dart` checks the real PDF engine (on the Linux desktop too), `youtube_player_test.dart` the embedded YouTube player (Android), `jellyfin_player_test.dart` the video player streaming from a Jellyfin server (`--dart-define`s for the server and the person, in the file), `japanese_analyser_test.dart` MeCab as its build hook compiles it, with the curator's IPADIC pack and an instance's JMdict export (`--dart-define`s for both). Never on the emulator from an agent session: it takes the editor down with it.
|
||||||
- **Every build needs a C++ compiler**, `flutter test` too: `mecab_for_dart`'s build hook compiles MeCab for the platform (ADR 0012; on Linux g++ or clang, on Android the NDK's, on Windows Visual Studio's, on macOS and iOS Xcode's). The snap's build installs clang; CI's runner needs one too.
|
- **Every build needs a C++ compiler**, `flutter test` too: `mecab_for_dart`'s build hook compiles MeCab for the platform (ADR 0012; on Linux g++ or clang, on Android the NDK's, on Windows Visual Studio's, on macOS and iOS Xcode's). The snap's build installs clang; CI's runner needs one too.
|
||||||
- **Windows builds need Developer Mode**: building makes symbolic links (Flutter links each plugin's native code into the build, and pdfrx's build does too), which Windows lets ordinary users make only in Developer Mode. PDFium itself comes with the build as a Dart native asset.
|
- **Windows builds need Developer Mode**: building makes symbolic links (Flutter links each plugin's native code into the build, and pdfrx's build does too), which Windows lets ordinary users make only in Developer Mode. PDFium itself comes with the build as a Dart native asset.
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,66 @@ that the instance doesn't also know, so nothing could be encrypted for the perso
|
||||||
key (5); passwords taken as RFC 8265's OpaqueString (other spaces to a plain space, NFC,
|
key (5); passwords taken as RFC 8265's OpaqueString (other spaces to a plain space, NFC,
|
||||||
control characters refused) before Argon2id, so a password unlocks alike on every system.
|
control characters refused) before Argon2id, so a password unlocks alike on every system.
|
||||||
|
|
||||||
|
## Settled in building the register's side (20 Oct 2026)
|
||||||
|
|
||||||
|
In `halkyon_base_server` (its README has the details); members forward every step
|
||||||
|
through `AccountRegister`, the session being theirs.
|
||||||
|
|
||||||
|
- **Sign-up** (`password.set`) needs an address: the person's own, or one a mailed code
|
||||||
|
proves in the same call, which makes an anonymous person identified under their ID.
|
||||||
|
The password signs in only once an authenticator is confirmed; until then a new setup
|
||||||
|
replaces it. The register checks what it can: salt 16 bytes, Argon2id `argon2id13` at
|
||||||
|
3–16 passes and 64 MiB–1 GiB, the verifier 1 < v < N padded to N, public keys 32 bytes,
|
||||||
|
each wrapping a 24-byte nonce and 112 bytes.
|
||||||
|
- **SRP's identity I is the sign-in's challenge ID**, not the account's: x comes from
|
||||||
|
Argon2id over the password and salt, so I only binds the proof to this sign-in, and
|
||||||
|
an account ID handed to whoever types an address would tell who has one.
|
||||||
|
- **Addresses nobody has** (or whose account has no password) get a salt and verifier
|
||||||
|
from a third instance key, `signInDecoyKey`, and the address's hash, the same queries
|
||||||
|
and modular arithmetic (both are computed for every sign-in), a challenge row of their
|
||||||
|
own, and in the second step the same refusal. One refusal, `wrongCredentials`, for a
|
||||||
|
wrong password, an unknown address, and a wrong, used or missing code;
|
||||||
|
`setupIncomplete` (no confirmed authenticator) and `notDue` (a reset waiting) only
|
||||||
|
after a right password. Begun sign-ins are limited to 10 an hour per address and 30
|
||||||
|
per network; challenges last two minutes and are taken once.
|
||||||
|
- **TOTP secrets** are sealed with XChaCha20-Poly1305 (pub.dev's `cryptography`, pure
|
||||||
|
Dart, pinned) under a key derived from `totpKey`, bound to the row's account and ID;
|
||||||
|
each step's code passes once (a conditional update of the last step). At most 10
|
||||||
|
authenticators; one not confirmed within a day is pruned; the last confirmed stays;
|
||||||
|
removing one, or new recovery codes, takes a code (10 tries an hour per account).
|
||||||
|
- **Resets** (D3): an authenticator reset takes effect 72 hours after the password and
|
||||||
|
a mailed code or the recovery phrase ask, with a mail to the address at once (none
|
||||||
|
sent, none started); a sign-in with a code calls off every reset waiting. Once due,
|
||||||
|
the first sign-in with the password alone carries it out (authenticators and recovery
|
||||||
|
codes go), and the password alone signs in until a new authenticator is confirmed.
|
||||||
|
The recovery phrase is proved by the account's Ed25519 signature over
|
||||||
|
`halkyon key proof v1|`, the challenge's ID and a nonce. A password set with the
|
||||||
|
phrase, or an account reset, ends the account's sessions on the register's server.
|
||||||
|
- **The account reset without the phrase waits 72 hours too, with a mail at the start**
|
||||||
|
(a mailed code asks, a new one carries it out once due; a sign-in with the password
|
||||||
|
calls it off): a proposal, for the developer to confirm. Carried out: a new password
|
||||||
|
and new keys, no authenticators.
|
||||||
|
- **Keys:** without all three keys (or without email sign-in) the instance offers no
|
||||||
|
password sign-in (`passwordSignIn` in its basics), so an instance can take these
|
||||||
|
releases before its operator makes keys; a key set but shorter than 32 characters
|
||||||
|
stops the server.
|
||||||
|
- **GitHub** (D2): an account with a password and a confirmed authenticator is refused
|
||||||
|
(`usePassword`), one without signs in as before and the app leads to setting up an
|
||||||
|
address, a password and an authenticator (its ways in say what is missing): a refusal
|
||||||
|
there would lock out GitHub's people on a new device, as nothing else signs them in.
|
||||||
|
Where passwords are offered, no new GitHub account is linked (`deprecated`). From
|
||||||
|
`GITHUB_SIGN_IN_ENDS` (a day the operator sets) GitHub signs nobody in (`ended`), the
|
||||||
|
basics stop offering it, and the daily pruning forgets every GitHub ID.
|
||||||
|
- **Email codes** still sign in (until step 3), but not into an account with a password
|
||||||
|
and a confirmed authenticator (`usePassword`), which a code alone would bypass.
|
||||||
|
|
||||||
|
Step 3 (email codes go): sign-up keeps `password.set` with its code; `emailCode.verifyCode`
|
||||||
|
and `emailSignIn.signIn` go, and with them the switch token; accounts with an address but
|
||||||
|
no password, and accounts whose setup never finished, need a way in (a mailed code and a
|
||||||
|
new password, as the account reset, or a password plus a mailed code until an
|
||||||
|
authenticator is confirmed); a member's sessions should end too when the register resets
|
||||||
|
an account; and a signed-in change of password (with the keys at hand) is still missing.
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
- People keep a password, an authenticator app and a recovery phrase: more to keep than a
|
- People keep a password, an authenticator app and a recovery phrase: more to keep than a
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
# halkyon_base
|
# halkyon_base
|
||||||
|
|
||||||
What every Halkyon server has, as a Serverpod module (`halkyon_base_server`, with its
|
What every Halkyon server has, as a Serverpod module (`halkyon_base_server`, with its
|
||||||
generated client `halkyon_base_client`): signing in, the account's ways in and its
|
generated client `halkyon_base_client`): signing in (with a password and TOTP, ADR 0017,
|
||||||
|
and the older ways), getting back into an account, the account's ways in and its
|
||||||
deletion, the pruning of what nobody can return to, what an instance tells its apps
|
deletion, the pruning of what nobody can return to, what an instance tells its apps
|
||||||
before anyone signs in, and people's public profiles with their pages on the web, which
|
before anyone signs in, and people's public profiles with their pages on the web, which
|
||||||
the instance's register keeps (ADR 0016). It comes from Halkyon Learn's server, whose
|
the instance's register keeps (ADR 0016). It comes from Halkyon Learn's server, whose
|
||||||
|
|
@ -14,12 +15,16 @@ Endpoints, under the nickname `base` in a host's client (`client.modules.base.em
|
||||||
| Endpoint | Methods | Who may call |
|
| Endpoint | Methods | Who may call |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `anonymousIdp` | `login` (Serverpod's anonymous sign-in, at most 30 new accounts per IP address and hour) | anyone |
|
| `anonymousIdp` | `login` (Serverpod's anonymous sign-in, at most 30 new accounts per IP address and hour) | anyone |
|
||||||
| `gitHubUpgrade` | `upgrade`: links a GitHub account to the caller, who becomes `identified` | signed in |
|
| `password` | `set`: the caller's password, with the address a mailed code proves unless they have one | signed in |
|
||||||
| `gitHubSignIn` | `signIn`: a session for whoever has that GitHub account | without a session |
|
| `passwordSignIn` | `begin`, `complete`: SRP-6a's two steps with a code from an authenticator (or a recovery code); a session | without a session |
|
||||||
|
| `authenticator` | `enrol`, `confirm` (the first brings ten recovery codes), `list`, `remove`, `newRecoveryCodes` | signed in |
|
||||||
|
| `accountRecovery` | `sendCode`, `resetAuthenticator`, `beginKeyProof`, `resetPassword` (with the recovery phrase), `startAccountReset`, `completeAccountReset` | without a session |
|
||||||
|
| `gitHubUpgrade` | `upgrade`: links a GitHub account to the caller, who becomes `identified`; refused where passwords 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`, `verifyCode`: a code by mail; a new address becomes the caller's, another person's gives a switch token | signed in |
|
| `emailCode` | `sendCode`, `verifyCode`: a code by mail; a new address becomes the caller's, another person's gives a switch token | signed in |
|
||||||
| `emailSignIn` | `signIn`: a session for the person a switch token names | without a session |
|
| `emailSignIn` | `signIn`: a session for the person a switch token names | without a session |
|
||||||
| `account` | `delete`, `signInWays` | signed in |
|
| `account` | `delete`, `signInWays` | signed in |
|
||||||
| `instance` | `basics`: the app it serves, GitHub's client ID if any, whether email sign-in is on, the source link, the oldest app build, where data and backups are | anyone |
|
| `instance` | `basics`: the app it serves, GitHub's client ID if any, whether email and password sign-in are on, the source link, the oldest app build, where data and backups are | anyone |
|
||||||
| `profile` | `mine`, `save`, `checkHandle`, `checkLinks` (at most 10 an hour), `setAvatar`, `removeAvatar`, `remove`: the caller's own public profile | `identified` |
|
| `profile` | `mine`, `save`, `checkHandle`, `checkLinks` (at most 10 an hour), `setAvatar`, `removeAvatar`, `remove`: the caller's own public profile | `identified` |
|
||||||
| `profiles` | `byHandle`: anyone's public profile, whatever the handle's case | signed in |
|
| `profiles` | `byHandle`: anyone's public profile, whatever the handle's case | signed in |
|
||||||
|
|
||||||
|
|
@ -36,13 +41,14 @@ Endpoints, under the nickname `base` in a host's client (`client.modules.base.em
|
||||||
`https://<account domain>/@handle` (`ACCOUNT_DOMAIN`, else the server's public host),
|
`https://<account domain>/@handle` (`ACCOUNT_DOMAIN`, else the server's public host),
|
||||||
served by `addProfilePageRoute`, in English or German, its foot what the host's
|
served by `addProfilePageRoute`, in English or German, its foot what the host's
|
||||||
`profilePageNote` says.
|
`profilePageNote` says.
|
||||||
- Refusals are `GitHubSignInRefusedException`, `EmailSignInRefusedException` and
|
- Refusals are `PasswordRefusedException`, `GitHubSignInRefusedException`,
|
||||||
`ProfileRefusedException`, each with a reason; `ProfileNotFoundException` where there is
|
`EmailSignInRefusedException` and `ProfileRefusedException`, each with a reason;
|
||||||
no profile.
|
`ProfileNotFoundException` where there is no profile.
|
||||||
- For the host's own code, `package:halkyon_base_server/base.dart`: the configuration,
|
- For the host's own code, `package:halkyon_base_server/base.dart`: the configuration,
|
||||||
`tokenManagerBuilders` and `identityProviderBuilders`, the `identified` scope and
|
`tokenManagerBuilders` and `identityProviderBuilders`, the `identified` scope and
|
||||||
`grantIdentified`, `deletePerson`, the pruning (`pruneAbandonedAnonymousPeople`,
|
`grantIdentified`, `deletePerson`, the pruning (`pruneAbandonedAnonymousPeople`,
|
||||||
`forgetOldSignUpAttempts`, `forgetExpiredEmailCodes`), short codes, `sourceCodeUrl`,
|
`forgetOldSignUpAttempts`, `forgetExpiredEmailCodes`, `forgetUnconfirmedAuthenticators`,
|
||||||
|
`forgetGitHubAccounts`), short codes, `sourceCodeUrl`,
|
||||||
`dataLocation`, `backupLocation`, people's profiles as the host's code reads them
|
`dataLocation`, `backupLocation`, people's profiles as the host's code reads them
|
||||||
(`profileOf`, `profilesOf`, `personWithHandle`, `fixHandle`; `saveProfile` for its
|
(`profileOf`, `profilesOf`, `personWithHandle`, `fixHandle`; `saveProfile` for its
|
||||||
tests), `accountDomain`, `profilePageUrl`, `addProfilePageRoute`, the register
|
tests), `accountDomain`, `profilePageUrl`, `addProfilePageRoute`, the register
|
||||||
|
|
@ -147,6 +153,53 @@ The second factor, in `package:halkyon_base_server/base.dart`:
|
||||||
hashes, key)` compares every hash in constant time and gives the hashes left
|
hashes, key)` compares every hash in constant time and gives the hashes left
|
||||||
(`RecoveryCodeAccepted.remaining`), so each code works once.
|
(`RecoveryCodeAccepted.remaining`), so each code works once.
|
||||||
|
|
||||||
|
## Signing in with a password (ADR 0017)
|
||||||
|
|
||||||
|
Built on the three parts above, on the register (members forward every step through
|
||||||
|
`AccountRegister`, `RemoteRegister` included; the session is the member's own).
|
||||||
|
|
||||||
|
- **Tables** (`serverOnly`, each row deleted with its person by `deletePerson`):
|
||||||
|
`account_password` (one per account: Argon2id's salt and costs, SRP's verifier padded
|
||||||
|
to N, the public keys, the private keys wrapped under the password and under the
|
||||||
|
recovery phrase), `totp_authenticator` (the secret sealed with XChaCha20-Poly1305 under
|
||||||
|
a key derived from `totpKey`, bound to the row's account and ID; `confirmedAt`;
|
||||||
|
`lastStep`), `recovery_code` (keyed hashes), `sign_in_challenge` (a step begun: SRP's b
|
||||||
|
and B, or a nonce to sign; two minutes, taken once by a delete), `account_reset` (one
|
||||||
|
of each kind per account, with when it takes effect).
|
||||||
|
- **Setting a password** (`password.set`): the device sends salt, costs (`argon2id13`,
|
||||||
|
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: the
|
||||||
|
caller's, or the one a mailed code proves, which becomes theirs as email sign-in makes
|
||||||
|
it (anonymous → `identified`, same ID). It signs in once an authenticator is
|
||||||
|
confirmed; until then a new setup replaces it.
|
||||||
|
- **Signing in:** `begin(address)` answers salt, costs and B; `complete(challenge, A, M1,
|
||||||
|
totpCode | recoveryCode)` answers M2, the public keys, the password's wrapping and a
|
||||||
|
session. SRP's identity I is the challenge's ID. An address nobody has (or whose
|
||||||
|
account has no password) gets a salt and verifier from `signInDecoyKey` and the
|
||||||
|
address's hash, the same queries and arithmetic, and the same refusal: a wrong
|
||||||
|
password, an unknown address and a wrong, used or missing code are all
|
||||||
|
`wrongCredentials`; `setupIncomplete` and `notDue` come only after a right password.
|
||||||
|
At most 10 sign-ins begun per address and 30 per network an hour.
|
||||||
|
- **Authenticators:** a secret made on the register, shown once as an `otpauth://` URI
|
||||||
|
(issuer: the account domain); confirmed by a code; the first confirmed brings ten
|
||||||
|
recovery codes; at most 10 per account; the last confirmed one stays; removing a
|
||||||
|
confirmed one, or new recovery codes, takes a code (10 tries an hour per account).
|
||||||
|
Each step's code is accepted once, by a conditional update of `lastStep`.
|
||||||
|
- **Resets:** a lost authenticator: SRP's proof plus a mailed code or the recovery
|
||||||
|
phrase's signature asks for it; it takes effect 72 hours later, a mail tells the
|
||||||
|
address at once, and signing in with a code calls it off; then the password alone
|
||||||
|
signs in until a new authenticator is confirmed. A lost password with the phrase: the
|
||||||
|
account's Ed25519 signature over a nonce (`keyProofMessage`) and a code set a new
|
||||||
|
password, the keys staying. A lost password without it: a mailed code asks, 72 hours
|
||||||
|
pass (a mail at the start, a sign-in calls it off), a new code carries it out: new
|
||||||
|
password, new keys, no authenticators. Either change of password ends the account's
|
||||||
|
sessions on the register's server.
|
||||||
|
- **Older ways:** an account that signs in with a password (and a confirmed
|
||||||
|
authenticator) no longer signs in by a mailed code (`usePassword`) or GitHub
|
||||||
|
(`usePassword`); where passwords are offered, no new GitHub account is linked
|
||||||
|
(`deprecated`); from `GITHUB_SIGN_IN_ENDS` on GitHub signs nobody in (`ended`) and
|
||||||
|
`forgetGitHubAccounts` deletes every GitHub ID.
|
||||||
|
|
||||||
## Taking it into a host
|
## Taking it into a host
|
||||||
|
|
||||||
1. Depend on both packages by path, within the repository's workspace (an app's
|
1. Depend on both packages by path, within the repository's workspace (an app's
|
||||||
|
|
@ -199,8 +252,9 @@ The second factor, in `package:halkyon_base_server/base.dart`:
|
||||||
literal routes of its own.
|
literal routes of its own.
|
||||||
|
|
||||||
Then schedule the pruning in a future call of the host's own: daily
|
Then schedule the pruning in a future call of the host's own: daily
|
||||||
`pruneAbandonedAnonymousPeople` and `forgetPeopleTheRegisterForgot` (a member's),
|
`pruneAbandonedAnonymousPeople`, `forgetGitHubAccounts` and
|
||||||
hourly `forgetOldSignUpAttempts`, `forgetExpiredEmailCodes` and
|
`forgetPeopleTheRegisterForgot` (a member's), hourly `forgetOldSignUpAttempts`,
|
||||||
|
`forgetExpiredEmailCodes`, `forgetUnconfirmedAuthenticators` and
|
||||||
`deliverDeletionNotices` (the register's). Each does nothing in the other mode.
|
`deliverDeletionNotices` (the register's). Each does nothing in the other mode.
|
||||||
|
|
||||||
4. A host's deletion (`PersonDataDeletion`) runs inside `deletePerson`'s transaction,
|
4. A host's deletion (`PersonDataDeletion`) runs inside `deletePerson`'s transaction,
|
||||||
|
|
@ -217,13 +271,17 @@ The second factor, in `package:halkyon_base_server/base.dart`:
|
||||||
|
|
||||||
The passwords it reads: `serverSideSessionKeyHashPepper`; for GitHub `githubClientId` and
|
The passwords it reads: `serverSideSessionKeyHashPepper`; for GitHub `githubClientId` and
|
||||||
`githubClientSecret`; for email `emailSignInPepper` and `emailSmtpHost`,
|
`githubClientSecret`; for email `emailSignInPepper` and `emailSmtpHost`,
|
||||||
`emailSmtpPort`, `emailSmtpUser`, `emailSmtpPassword`, `emailFrom`; between the
|
`emailSmtpPort`, `emailSmtpUser`, `emailSmtpPassword`, `emailFrom`; for passwords
|
||||||
instance's services `instanceSecret`. Never change either pepper once people sign in:
|
`totpKey`, `recoveryCodeKey` and `signInDecoyKey` (each at least 32 characters, or
|
||||||
every session or address hash would be lost. A member needs no email settings of its
|
`addInstanceRoutes` stops the server; all three and email sign-in, or no password
|
||||||
|
sign-in); between the instance's services `instanceSecret`. Never change either pepper
|
||||||
|
once people sign in: every session or address hash would be lost; changing `totpKey`
|
||||||
|
voids every authenticator, `recoveryCodeKey` every recovery code. A member needs no email settings of its
|
||||||
own (the register mails the codes), but GitHub's (its own OAuth app's, whose callback is
|
own (the register mails the codes), but GitHub's (its own OAuth app's, whose callback is
|
||||||
its app's). The environment: `ACCOUNT_REGISTER_URL` and `INSTANCE_MEMBERS` (above);
|
its app's). The environment: `ACCOUNT_REGISTER_URL` and `INSTANCE_MEMBERS` (above);
|
||||||
`ACCOUNT_DOMAIN`, the domain handles are on, which can never change once anyone follows
|
`ACCOUNT_DOMAIN`, the domain handles are on, which can never change once anyone follows
|
||||||
one; `SOURCE_URL`, `DATA_LOCATION`, `BACKUP_LOCATION`: where the instance keeps people's data
|
one; `GITHUB_SIGN_IN_ENDS`, the day GitHub stops signing anyone in (`2027-01-22`);
|
||||||
|
`SOURCE_URL`, `DATA_LOCATION`, `BACKUP_LOCATION`: where the instance keeps people's data
|
||||||
and its backups, each `EU`, a country's code (`DE`) or a subdivision's (ISO 3166-2,
|
and its backups, each `EU`, a country's code (`DE`) or a subdivision's (ISO 3166-2,
|
||||||
`US-CA`, `DE-BY`), the backups also `none`; where the disks are, not which law governs.
|
`US-CA`, `DE-BY`), the backups also `none`; where the disks are, not which law governs.
|
||||||
|
|
||||||
|
|
@ -233,7 +291,9 @@ and its backups, each `EU`, a country's code (`DE`) or a subdivision's (ISO 3166
|
||||||
`serverpod --no-analytics create-migration` after changing a table. Tests need no Docker:
|
`serverpod --no-analytics create-migration` after changing a table. Tests need no Docker:
|
||||||
`dart test` here starts Serverpod's embedded PostgreSQL. They read `config/passwords.yaml`
|
`dart test` here starts Serverpod's embedded PostgreSQL. They read `config/passwords.yaml`
|
||||||
(git-ignored) with throwaway values under `test:` for `database`, `serviceSecret`,
|
(git-ignored) with throwaway values under `test:` for `database`, `serviceSecret`,
|
||||||
`serverSideSessionKeyHashPepper` and `emailSignInPepper`.
|
`serverSideSessionKeyHashPepper` and `emailSignInPepper`. Password sign-in's keys may be
|
||||||
|
there too (CI's are), but the tests set their own (`usePasswordKeys`) and switch them
|
||||||
|
off otherwise.
|
||||||
|
|
||||||
## Licences
|
## Licences
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue