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>
This commit is contained in:
parent
a0842e401d
commit
42d06f7ee8
55 changed files with 3017 additions and 44 deletions
|
|
@ -75,7 +75,7 @@ A spaced-repetition learning app: studysets of flashcards, reviewed on an FSRS s
|
||||||
- **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 with a password) attach to it. Content references `AuthUser`, never a provider account. A person may sign in with GitHub and with an address and password, one of each (`account.signInWays`, which also says whether they have a password and a confirmed authenticator); the Profile tab lists their ways in, adds GitHub where the instance adds it (never where passwords are offered), and holds halkyon_app's `AccountSecuritySection` (setting up a password, the authenticators, changing it).
|
- **Identity**: `AuthUser` is the person; identity providers (anonymous, GitHub, an email address with a password) attach to it. Content references `AuthUser`, never a provider account. A person may sign in with GitHub and with an address and password, one of each (`account.signInWays`, which also says whether they have a password and a confirmed authenticator); the Profile tab lists their ways in, adds GitHub where the instance adds it (never where passwords are offered), and holds halkyon_app's `AccountSecuritySection` (setting up a password, the authenticators, changing it).
|
||||||
- **A password and an authenticator** (ADR 0017, built 2026-10-20 for build 22; halkyon_base's and halkyon_app's READMEs): the only way an identified person signs in besides GitHub, which is deprecated. 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. A reset takes effect only once the owner had 72 hours outside hold-outs (codes waiting, the address's ceiling filled: each moves it on by the time it holds the owner out); one that took effect and wasn't carried out lapses a week later, and every reset 30 days after it was asked; the Profile tab's account section shows waiting resets, called off with the password and a code (ADR 0017's full reading). 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, 30 days later (a date set wrong can be put right first), the register's pruning forgets every GitHub ID, deleting first the accounts that signed in with GitHub alone (no address), which the app warns of until then (D2; the full reading's M1). 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()`). On the device (halkyon_app): signing up shows the recovery phrase once and checks three of its words before anything reaches the instance; signing in checks SRP's M2 and opens the keys before the session is put in place; an authenticator still to set up is set up with the session held back; the unlocked keys stay in the system's keyring per instance and person, gone once another person signs in or nobody. A session alone never gets a code checked (ADR 0017's third reading, as Proton has it): every signed-in change of how the account is secured (adding an authenticator once one is confirmed, removing a confirmed one, new recovery codes, a new phrase, a new password, deleting) takes the password and a code (`OwnerProof`), each sheet asking for both; a session's wrong proofs count for it alone and the tenth in a row ends it, never making the account's codes wait. Nor does a session alone set a first password or confirm a first authenticator (the fourth reading): an account with an address proves it by a code mailed there (`firstPassword`), one that signs in with GitHub alone by a GitHub sign-in of the last 10 minutes (the app offers a password right after one, before the person's app, and asks for GitHub anew when the instance says `signInAgain`), and the first authenticator takes the password. A change of password ends the account's other sessions and every reset all of them, the authenticators' included as the sign-in carries it out, on every service of the instance (`sessions_ended_notice`). ADR 0017's "The rules as they stand" lists who changes what with which proofs.
|
- **A password and an authenticator** (ADR 0017, built 2026-10-20 for build 22; halkyon_base's and halkyon_app's READMEs): the only way an identified person signs in besides GitHub, which is deprecated. 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. A reset takes effect only once the owner had 72 hours outside hold-outs (codes waiting, the address's ceiling filled: each moves it on by the time it holds the owner out); one that took effect and wasn't carried out lapses a week later, and every reset 30 days after it was asked; the Profile tab's account section shows waiting resets, called off with the password and a code (ADR 0017's full reading). 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, 30 days later (a date set wrong can be put right first), the register's pruning forgets every GitHub ID, deleting first the accounts that signed in with GitHub alone (no address), which the app warns of until then (D2; the full reading's M1). 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()`). On the device (halkyon_app): signing up shows the recovery phrase once and checks three of its words before anything reaches the instance; signing in checks SRP's M2 and opens the keys before the session is put in place; an authenticator still to set up is set up with the session held back; the unlocked keys stay in the system's keyring per instance and person, gone once another person signs in or nobody. A session alone never gets a code checked (ADR 0017's third reading, as Proton has it): every signed-in change of how the account is secured (adding an authenticator once one is confirmed, removing a confirmed one, new recovery codes, a new phrase, a new password, deleting) takes the password and a code (`OwnerProof`), each sheet asking for both; a session's wrong proofs count for it alone and the tenth in a row ends it, never making the account's codes wait. Nor does a session alone set a first password or confirm a first authenticator (the fourth reading): an account with an address proves it by a code mailed there (`firstPassword`), one that signs in with GitHub alone by a GitHub sign-in of the last 10 minutes (the app offers a password right after one, before the person's app, and asks for GitHub anew when the instance says `signInAgain`), and the first authenticator takes the password. A change of password ends the account's other sessions and every reset all of them, the authenticators' included as the sign-in carries it out, on every service of the instance (`sessions_ended_notice`). ADR 0017's "The rules as they stand" lists who changes what with which proofs.
|
||||||
- **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. SimpleLogin's forwarding page is the register's too (ADR 0020, built 2026-10-23 ahead of the sign-in itself): `addSimpleLoginCallbackRoute` in `server.dart`, `/simplelogin-callback` on the web server, which Caddy forwards there, sending a sign-in on to the app by the marker its state begins with (halkyon_base's README); beside it `deploy/Caddyfile` answers Android's `/.well-known/assetlinks.json` for Halkyon Learn signed with the release key, on learn.halkyon.eu alone (`test/deploy/assetlinks_test.dart` holds it to `tool/release/android-certificate.sha256`).
|
- **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. The sign-in with SimpleLogin is the register's (ADR 0020, step 2 built 2026-10-23: halkyon_base's `simpleLogin` endpoint on halkyon_oidc, the instance's SimpleLogin app as `SIMPLELOGIN_CLIENT_ID` and `SIMPLELOGIN_CLIENT_SECRET` in `deploy/.env`, the ways offered as `SIGN_IN_WAYS`), and so is its forwarding page, built ahead of it: `addSimpleLoginCallbackRoute` in `server.dart`, `/simplelogin-callback` on the web server, which Caddy forwards there, sending a sign-in on to the app by the marker its state begins with (halkyon_base's README); beside it `deploy/Caddyfile` answers Android's `/.well-known/assetlinks.json` for Halkyon Learn signed with the release key, on learn.halkyon.eu alone (`test/deploy/assetlinks_test.dart` holds it to `tool/release/android-certificate.sha256`).
|
||||||
- **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.
|
||||||
- **Switching devices' person** (halkyon_app's `switchToGitHub` and `useSession`, on whether the device holds content as Learn counts it, `PersonDependencies.holdsContent`: its studysets, Recently deleted's too): swap the session directly, never through "signed out" (which would leave the device asking how to start), then delete the account left behind if it was anonymous (nobody could sign into it again), never an identified one. Someone identified is never offered a switch: an address or GitHub account that is another account's is said to be so.
|
- **Switching devices' person** (halkyon_app's `switchToGitHub` and `useSession`, on whether the device holds content as Learn counts it, `PersonDependencies.holdsContent`: its studysets, Recently deleted's too): swap the session directly, never through "signed out" (which would leave the device asking how to start), then delete the account left behind if it was anonymous (nobody could sign into it again), never an identified one. Someone identified is never offered a switch: an address or GitHub account that is another account's is said to be so.
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,10 @@ class PruningFutureCall extends FutureCall {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Forgets the sign-up attempts the rate limit is done with, with their IP
|
/// Forgets the sign-up attempts the rate limit is done with, with their IP
|
||||||
/// addresses, the email sign-in codes past their time, and the password
|
/// addresses, the email sign-in codes past their time, the password
|
||||||
/// sign-ins never finished and authenticators never confirmed (ADR 0017),
|
/// sign-ins never finished and authenticators never confirmed (ADR 0017),
|
||||||
/// and logs how many.
|
/// and the SimpleLogin sign-ins never finished (ADR 0020), and logs how
|
||||||
|
/// many.
|
||||||
Future<void> forgetSignUpAttempts(Session session) async {
|
Future<void> forgetSignUpAttempts(Session session) async {
|
||||||
final now = clock.now();
|
final now = clock.now();
|
||||||
final forgotten = await forgetOldSignUpAttempts(session, now: now);
|
final forgotten = await forgetOldSignUpAttempts(session, now: now);
|
||||||
|
|
@ -37,9 +38,11 @@ class PruningFutureCall extends FutureCall {
|
||||||
session,
|
session,
|
||||||
now: now,
|
now: now,
|
||||||
);
|
);
|
||||||
|
final simpleLogins = await forgetUnfinishedSimpleLogins(session, now: now);
|
||||||
session.log(
|
session.log(
|
||||||
'Forgot $forgotten sign-up attempts, $codes email codes and '
|
'Forgot $forgotten sign-up attempts, $codes email codes, '
|
||||||
'$unfinished unfinished sign-ins and authenticators.',
|
'$unfinished unfinished sign-ins and authenticators and '
|
||||||
|
'$simpleLogins unfinished SimpleLogin sign-ins.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -299,9 +299,10 @@ class PruningPruneAnonymousPeopleFutureCall extends _is.FutureCall
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Forgets the sign-up attempts the rate limit is done with, with their IP
|
/// Forgets the sign-up attempts the rate limit is done with, with their IP
|
||||||
/// addresses, the email sign-in codes past their time, and the password
|
/// addresses, the email sign-in codes past their time, the password
|
||||||
/// sign-ins never finished and authenticators never confirmed (ADR 0017),
|
/// sign-ins never finished and authenticators never confirmed (ADR 0017),
|
||||||
/// and logs how many.
|
/// and the SimpleLogin sign-ins never finished (ADR 0020), and logs how
|
||||||
|
/// many.
|
||||||
class PruningForgetSignUpAttemptsFutureCall extends _is.FutureCall
|
class PruningForgetSignUpAttemptsFutureCall extends _is.FutureCall
|
||||||
implements _is.InvokableFutureCall {
|
implements _is.InvokableFutureCall {
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -239,9 +239,10 @@ class PruningPruneAnonymousPeopleFutureCall extends _is.FutureCall
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Forgets the sign-up attempts the rate limit is done with, with their IP
|
/// Forgets the sign-up attempts the rate limit is done with, with their IP
|
||||||
/// addresses, the email sign-in codes past their time, and the password
|
/// addresses, the email sign-in codes past their time, the password
|
||||||
/// sign-ins never finished and authenticators never confirmed (ADR 0017),
|
/// sign-ins never finished and authenticators never confirmed (ADR 0017),
|
||||||
/// and logs how many.
|
/// and the SimpleLogin sign-ins never finished (ADR 0020), and logs how
|
||||||
|
/// many.
|
||||||
class PruningForgetSignUpAttemptsFutureCall extends _is.FutureCall
|
class PruningForgetSignUpAttemptsFutureCall extends _is.FutureCall
|
||||||
implements _is.InvokableFutureCall {
|
implements _is.InvokableFutureCall {
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,10 @@ class PruningFutureCall extends FutureCall {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Forgets the sign-up attempts the rate limit is done with, with their IP
|
/// Forgets the sign-up attempts the rate limit is done with, with their IP
|
||||||
/// addresses, the email sign-in codes past their time, and the password
|
/// addresses, the email sign-in codes past their time, the password
|
||||||
/// sign-ins never finished and authenticators never confirmed (ADR 0017),
|
/// sign-ins never finished and authenticators never confirmed (ADR 0017),
|
||||||
/// and logs how many.
|
/// and the SimpleLogin sign-ins never finished (ADR 0020), and logs how
|
||||||
|
/// many.
|
||||||
Future<void> forgetSignUpAttempts(Session session) async {
|
Future<void> forgetSignUpAttempts(Session session) async {
|
||||||
final now = clock.now();
|
final now = clock.now();
|
||||||
final forgotten = await forgetOldSignUpAttempts(session, now: now);
|
final forgotten = await forgetOldSignUpAttempts(session, now: now);
|
||||||
|
|
@ -37,9 +38,11 @@ class PruningFutureCall extends FutureCall {
|
||||||
session,
|
session,
|
||||||
now: now,
|
now: now,
|
||||||
);
|
);
|
||||||
|
final simpleLogins = await forgetUnfinishedSimpleLogins(session, now: now);
|
||||||
session.log(
|
session.log(
|
||||||
'Forgot $forgotten sign-up attempts, $codes email codes and '
|
'Forgot $forgotten sign-up attempts, $codes email codes, '
|
||||||
'$unfinished unfinished sign-ins and authenticators.',
|
'$unfinished unfinished sign-ins and authenticators and '
|
||||||
|
'$simpleLogins unfinished SimpleLogin sign-ins.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,21 @@ EMAIL_FROM=
|
||||||
TOTP_KEY=
|
TOTP_KEY=
|
||||||
RECOVERY_CODE_KEY=
|
RECOVERY_CODE_KEY=
|
||||||
SIGN_IN_DECOY_KEY=
|
SIGN_IN_DECOY_KEY=
|
||||||
|
# SimpleLogin (ADR 0020): people sign in with their SimpleLogin or Proton account, of
|
||||||
|
# which the instance keeps only SimpleLogin's ID for them (no address, no name). Off
|
||||||
|
# unless both are set: the client ID and secret of this instance's own SimpleLogin app
|
||||||
|
# (SimpleLogin → Settings → Developer → New website), registered with the callback
|
||||||
|
# /simplelogin-callback on the server's web address (on learn.halkyon.eu's server
|
||||||
|
# https://learn.halkyon.eu/simplelogin-callback), its forwarding page back to the apps.
|
||||||
|
SIMPLELOGIN_CLIENT_ID=
|
||||||
|
SIMPLELOGIN_CLIENT_SECRET=
|
||||||
|
# Another callback than this server's own page, as the app registered it (https only).
|
||||||
|
#SIMPLELOGIN_CALLBACK_URL=https://learn.example.org/simplelogin-callback
|
||||||
|
# The ways in the app offers, of those set up above: simplelogin, password, or both
|
||||||
|
# (separated by spaces or commas). Empty: every way set up. An unknown name stops the
|
||||||
|
# server as it starts.
|
||||||
|
#SIGN_IN_WAYS=simplelogin
|
||||||
|
|
||||||
# The day GitHub stops signing anyone in, three months after the release that brings
|
# The day GitHub stops signing anyone in, three months after the release that brings
|
||||||
# passwords (ADR 0017, D2): from then on its people sign in with their address and
|
# passwords (ADR 0017, D2): from then on its people sign in with their address and
|
||||||
# password only, and the daily pruning forgets every GitHub ID. Empty: GitHub goes on.
|
# password only, and the daily pruning forgets every GitHub ID. Empty: GitHub goes on.
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,14 @@ services:
|
||||||
SERVERPOD_PASSWORD_totpKey: ${TOTP_KEY:-}
|
SERVERPOD_PASSWORD_totpKey: ${TOTP_KEY:-}
|
||||||
SERVERPOD_PASSWORD_recoveryCodeKey: ${RECOVERY_CODE_KEY:-}
|
SERVERPOD_PASSWORD_recoveryCodeKey: ${RECOVERY_CODE_KEY:-}
|
||||||
SERVERPOD_PASSWORD_signInDecoyKey: ${SIGN_IN_DECOY_KEY:-}
|
SERVERPOD_PASSWORD_signInDecoyKey: ${SIGN_IN_DECOY_KEY:-}
|
||||||
|
# SimpleLogin (ADR 0020), only with the instance's SimpleLogin app; empty: off.
|
||||||
|
# Its callback; empty: this server's own page, on its web server's public address
|
||||||
|
# (https://learn.halkyon.eu/simplelogin-callback).
|
||||||
|
SERVERPOD_PASSWORD_simpleLoginClientId: ${SIMPLELOGIN_CLIENT_ID:-}
|
||||||
|
SERVERPOD_PASSWORD_simpleLoginClientSecret: ${SIMPLELOGIN_CLIENT_SECRET:-}
|
||||||
|
SIMPLELOGIN_CALLBACK_URL: ${SIMPLELOGIN_CALLBACK_URL:-}
|
||||||
|
# The ways in offered, of those set up (`simplelogin password`); empty: all.
|
||||||
|
SIGN_IN_WAYS: ${SIGN_IN_WAYS:-}
|
||||||
# Caddy, whose X-Forwarded-For names the visitor the per-network limits count
|
# Caddy, whose X-Forwarded-For names the visitor the per-network limits count
|
||||||
# (new anonymous accounts, codes by mail, password sign-ins): without it, every
|
# (new anonymous accounts, codes by mail, password sign-ins): without it, every
|
||||||
# visitor would share each limit with everyone. Its address on `edge`, below.
|
# visitor would share each limit with everyone. Its address on `edge`, below.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# ADR 0020: Signing in with SimpleLogin only
|
# ADR 0020: Signing in with SimpleLogin only
|
||||||
|
|
||||||
- **Status:** decided 23 Oct 2026, by the developer (D1–D15 below), the *Proposal*
|
- **Status:** decided 23 Oct 2026, by the developer (D1–D17 below), the *Proposal*
|
||||||
with it (D15).
|
with it (D15).
|
||||||
- **Built (23 Oct 2026), step 1:** the forwarding page (3),
|
- **Built (23 Oct 2026), step 1:** the forwarding page (3),
|
||||||
`/simplelogin-callback` on the register's web server (halkyon_base's
|
`/simplelogin-callback` on the register's web server (halkyon_base's
|
||||||
|
|
@ -16,6 +16,21 @@
|
||||||
held to `tool/release/android-certificate.sha256` by a test. The app's intent filter
|
held to `tool/release/android-certificate.sha256` by a test. The app's intent filter
|
||||||
comes with the sign-in; iOS's associated domains wait for an Apple team ID, so iPhones
|
comes with the sign-in; iOS's associated domains wait for an Apple team ID, so iPhones
|
||||||
come back through the forwarding page and the scheme until then.
|
come back through the forwarding page and the scheme until then.
|
||||||
|
- **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
|
||||||
|
`simplelogin`. Someone new gets no account yet: their identity waits (D16), with a
|
||||||
|
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.
|
||||||
- **Builds on:** ADR 0010 (email sign-in), ADR 0011 (profile pages), ADR 0016 (the
|
- **Builds on:** ADR 0010 (email sign-in), ADR 0011 (profile pages), ADR 0016 (the
|
||||||
register), ADR 0017 (a password, TOTP and the keys).
|
register), ADR 0017 (a password, TOTP and the keys).
|
||||||
- **Changes:** ADR 0017's way in on an instance that offers SimpleLogin (its keys, their
|
- **Changes:** ADR 0017's way in on an instance that offers SimpleLogin (its keys, their
|
||||||
|
|
@ -103,6 +118,18 @@ against app.simplelogin.io on 23 Oct:
|
||||||
cut-off (step 5) follows 30 days after it.
|
cut-off (step 5) follows 30 days after it.
|
||||||
- **D15 The proposal (1–12) as written** (23 Oct, evening); what building it shows
|
- **D15 The proposal (1–12) as written** (23 Oct, evening); what building it shows
|
||||||
otherwise comes back to the developer.
|
otherwise comes back to the developer.
|
||||||
|
- **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.
|
||||||
- **Also (23 Oct):** the developer's own account moves by adding SimpleLogin to it while
|
- **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
|
signed in with GitHub; the instance's SimpleLogin app goes up for approval once
|
||||||
halkyon.app/learn exists, before learn.halkyon.eu switches.
|
halkyon.app/learn exists, before learn.halkyon.eu switches.
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ abstract class SignInWays
|
||||||
SignInWays._({
|
SignInWays._({
|
||||||
required this.gitHub,
|
required this.gitHub,
|
||||||
required this.email,
|
required this.email,
|
||||||
|
bool? simpleLogin,
|
||||||
bool? password,
|
bool? password,
|
||||||
bool? authenticator,
|
bool? authenticator,
|
||||||
bool? passwordOnly,
|
bool? passwordOnly,
|
||||||
|
|
@ -25,7 +26,8 @@ abstract class SignInWays
|
||||||
this.sessionEndedForProofsAt,
|
this.sessionEndedForProofsAt,
|
||||||
this.authenticatorResetAt,
|
this.authenticatorResetAt,
|
||||||
this.accountResetAt,
|
this.accountResetAt,
|
||||||
}) : password = password ?? false,
|
}) : simpleLogin = simpleLogin ?? false,
|
||||||
|
password = password ?? false,
|
||||||
authenticator = authenticator ?? false,
|
authenticator = authenticator ?? false,
|
||||||
passwordOnly = passwordOnly ?? false,
|
passwordOnly = passwordOnly ?? false,
|
||||||
secondFactorWorks = secondFactorWorks ?? false;
|
secondFactorWorks = secondFactorWorks ?? false;
|
||||||
|
|
@ -33,6 +35,7 @@ abstract class SignInWays
|
||||||
factory SignInWays({
|
factory SignInWays({
|
||||||
required bool gitHub,
|
required bool gitHub,
|
||||||
required bool email,
|
required bool email,
|
||||||
|
bool? simpleLogin,
|
||||||
bool? password,
|
bool? password,
|
||||||
bool? authenticator,
|
bool? authenticator,
|
||||||
bool? passwordOnly,
|
bool? passwordOnly,
|
||||||
|
|
@ -46,6 +49,9 @@ abstract class SignInWays
|
||||||
return SignInWays(
|
return SignInWays(
|
||||||
gitHub: _isc.BoolJsonExtension.fromJson(jsonSerialization['gitHub']),
|
gitHub: _isc.BoolJsonExtension.fromJson(jsonSerialization['gitHub']),
|
||||||
email: _isc.BoolJsonExtension.fromJson(jsonSerialization['email']),
|
email: _isc.BoolJsonExtension.fromJson(jsonSerialization['email']),
|
||||||
|
simpleLogin: jsonSerialization['simpleLogin'] == null
|
||||||
|
? null
|
||||||
|
: _isc.BoolJsonExtension.fromJson(jsonSerialization['simpleLogin']),
|
||||||
password: jsonSerialization['password'] == null
|
password: jsonSerialization['password'] == null
|
||||||
? null
|
? null
|
||||||
: _isc.BoolJsonExtension.fromJson(jsonSerialization['password']),
|
: _isc.BoolJsonExtension.fromJson(jsonSerialization['password']),
|
||||||
|
|
@ -83,6 +89,10 @@ abstract class SignInWays
|
||||||
|
|
||||||
bool email;
|
bool email;
|
||||||
|
|
||||||
|
/// SimpleLogin (ADR 0020): the instance's register keeps its ID for the
|
||||||
|
/// instance's SimpleLogin app.
|
||||||
|
bool simpleLogin;
|
||||||
|
|
||||||
/// A password (ADR 0017), which an address needs for resets.
|
/// A password (ADR 0017), which an address needs for resets.
|
||||||
bool password;
|
bool password;
|
||||||
|
|
||||||
|
|
@ -118,6 +128,7 @@ abstract class SignInWays
|
||||||
SignInWays copyWith({
|
SignInWays copyWith({
|
||||||
bool? gitHub,
|
bool? gitHub,
|
||||||
bool? email,
|
bool? email,
|
||||||
|
bool? simpleLogin,
|
||||||
bool? password,
|
bool? password,
|
||||||
bool? authenticator,
|
bool? authenticator,
|
||||||
bool? passwordOnly,
|
bool? passwordOnly,
|
||||||
|
|
@ -132,6 +143,7 @@ abstract class SignInWays
|
||||||
'__className__': 'halkyon_base.SignInWays',
|
'__className__': 'halkyon_base.SignInWays',
|
||||||
'gitHub': gitHub,
|
'gitHub': gitHub,
|
||||||
'email': email,
|
'email': email,
|
||||||
|
'simpleLogin': simpleLogin,
|
||||||
'password': password,
|
'password': password,
|
||||||
'authenticator': authenticator,
|
'authenticator': authenticator,
|
||||||
'passwordOnly': passwordOnly,
|
'passwordOnly': passwordOnly,
|
||||||
|
|
@ -150,6 +162,7 @@ abstract class SignInWays
|
||||||
'__className__': 'halkyon_base.SignInWays',
|
'__className__': 'halkyon_base.SignInWays',
|
||||||
'gitHub': gitHub,
|
'gitHub': gitHub,
|
||||||
'email': email,
|
'email': email,
|
||||||
|
'simpleLogin': simpleLogin,
|
||||||
'password': password,
|
'password': password,
|
||||||
'authenticator': authenticator,
|
'authenticator': authenticator,
|
||||||
'passwordOnly': passwordOnly,
|
'passwordOnly': passwordOnly,
|
||||||
|
|
@ -174,6 +187,7 @@ class _SignInWaysImpl extends SignInWays {
|
||||||
_SignInWaysImpl({
|
_SignInWaysImpl({
|
||||||
required bool gitHub,
|
required bool gitHub,
|
||||||
required bool email,
|
required bool email,
|
||||||
|
bool? simpleLogin,
|
||||||
bool? password,
|
bool? password,
|
||||||
bool? authenticator,
|
bool? authenticator,
|
||||||
bool? passwordOnly,
|
bool? passwordOnly,
|
||||||
|
|
@ -184,6 +198,7 @@ class _SignInWaysImpl extends SignInWays {
|
||||||
}) : super._(
|
}) : super._(
|
||||||
gitHub: gitHub,
|
gitHub: gitHub,
|
||||||
email: email,
|
email: email,
|
||||||
|
simpleLogin: simpleLogin,
|
||||||
password: password,
|
password: password,
|
||||||
authenticator: authenticator,
|
authenticator: authenticator,
|
||||||
passwordOnly: passwordOnly,
|
passwordOnly: passwordOnly,
|
||||||
|
|
@ -200,6 +215,7 @@ class _SignInWaysImpl extends SignInWays {
|
||||||
SignInWays copyWith({
|
SignInWays copyWith({
|
||||||
bool? gitHub,
|
bool? gitHub,
|
||||||
bool? email,
|
bool? email,
|
||||||
|
bool? simpleLogin,
|
||||||
bool? password,
|
bool? password,
|
||||||
bool? authenticator,
|
bool? authenticator,
|
||||||
bool? passwordOnly,
|
bool? passwordOnly,
|
||||||
|
|
@ -211,6 +227,7 @@ class _SignInWaysImpl extends SignInWays {
|
||||||
return SignInWays(
|
return SignInWays(
|
||||||
gitHub: gitHub ?? this.gitHub,
|
gitHub: gitHub ?? this.gitHub,
|
||||||
email: email ?? this.email,
|
email: email ?? this.email,
|
||||||
|
simpleLogin: simpleLogin ?? this.simpleLogin,
|
||||||
password: password ?? this.password,
|
password: password ?? this.password,
|
||||||
authenticator: authenticator ?? this.authenticator,
|
authenticator: authenticator ?? this.authenticator,
|
||||||
passwordOnly: passwordOnly ?? this.passwordOnly,
|
passwordOnly: passwordOnly ?? this.passwordOnly,
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,11 @@ import 'package:halkyon_base_client/src/protocol/profile/profile.dart'
|
||||||
as _i8qdmv6n;
|
as _i8qdmv6n;
|
||||||
import 'package:halkyon_base_client/src/protocol/profile/profile_refusal.dart'
|
import 'package:halkyon_base_client/src/protocol/profile/profile_refusal.dart'
|
||||||
as _ih722tf9;
|
as _ih722tf9;
|
||||||
|
import 'package:halkyon_base_client/src/protocol/simplelogin/simplelogin_completed.dart'
|
||||||
|
as _ianfx7qj;
|
||||||
|
import 'package:halkyon_base_client/src/protocol/simplelogin/simplelogin_platform.dart'
|
||||||
|
as _i32mw11n;
|
||||||
|
import 'package:halkyon_oidc_client/halkyon_oidc_client.dart' as _ibkpwzgl;
|
||||||
import 'package:serverpod_auth_core_client/serverpod_auth_core_client.dart'
|
import 'package:serverpod_auth_core_client/serverpod_auth_core_client.dart'
|
||||||
as _iacc;
|
as _iacc;
|
||||||
import 'package:serverpod_auth_idp_client/serverpod_auth_idp_client.dart'
|
import 'package:serverpod_auth_idp_client/serverpod_auth_idp_client.dart'
|
||||||
|
|
@ -205,8 +210,10 @@ class EndpointInstance extends _isc.EndpointRef {
|
||||||
/// operator's OAuth app and until it ends (ADR 0017), email sign-in only
|
/// operator's OAuth app and until it ends (ADR 0017), email sign-in only
|
||||||
/// where the register can mail codes (this server, or on a member the
|
/// where the register can mail codes (this server, or on a member the
|
||||||
/// register, ADR 0016), password sign-in where the register holds its
|
/// register, ADR 0016), password sign-in where the register holds its
|
||||||
/// keys, when GitHub sign-in ends, where its source is, the oldest app
|
/// keys, SimpleLogin where the register has the instance's SimpleLogin app
|
||||||
/// build it works with, and where it keeps people's data and its backups.
|
/// (ADR 0020), each as its operator offers it, when GitHub sign-in ends,
|
||||||
|
/// where its source is, the oldest app build it works with, and where it
|
||||||
|
/// keeps people's data and its backups.
|
||||||
_ida.Future<_isnyur2i.InstanceBasics> basics() =>
|
_ida.Future<_isnyur2i.InstanceBasics> basics() =>
|
||||||
caller.callServerEndpoint<_isnyur2i.InstanceBasics>(
|
caller.callServerEndpoint<_isnyur2i.InstanceBasics>(
|
||||||
'halkyon_base.instance',
|
'halkyon_base.instance',
|
||||||
|
|
@ -729,6 +736,48 @@ class EndpointProfiles extends _isc.EndpointRef {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Signing in with SimpleLogin (ADR 0020, 3 to 5), signed out or, to add it
|
||||||
|
/// to one's account, signed in. The instance's register keeps the attempts
|
||||||
|
/// and checks SimpleLogin's codes (ADR 0016); a session is this server's.
|
||||||
|
/// Refusals are `OidcRefusedException`s.
|
||||||
|
/// {@category Endpoint}
|
||||||
|
class EndpointSimpleLogin extends _isc.EndpointRef {
|
||||||
|
EndpointSimpleLogin(_isc.EndpointCaller caller) : super(caller);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => 'halkyon_base.simpleLogin';
|
||||||
|
|
||||||
|
/// Begins a sign-in from this server's app on [platform]: the attempt,
|
||||||
|
/// whose ID the device keeps to itself, and SimpleLogin's address to open
|
||||||
|
/// in the browser, which comes back through the forwarding page with a
|
||||||
|
/// code for [complete]. Refusals as `beginSimpleLogin`'s.
|
||||||
|
_ida.Future<_ibkpwzgl.OidcStart> begin({
|
||||||
|
required _i32mw11n.SimpleLoginPlatform platform,
|
||||||
|
}) => caller.callServerEndpoint<_ibkpwzgl.OidcStart>(
|
||||||
|
'halkyon_base.simpleLogin',
|
||||||
|
'begin',
|
||||||
|
{'platform': platform},
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Ends [attempt] with SimpleLogin's [code], as the caller began it:
|
||||||
|
/// signed out, a session for the account with that identity, or for
|
||||||
|
/// someone new the handle suggested (their account comes next); signed
|
||||||
|
/// in, SimpleLogin added to the caller's account. Serverpod issues no
|
||||||
|
/// session for another person inside a signed-in call, and nobody is
|
||||||
|
/// switched to another account. Refusals as `completeSimpleLogin`'s.
|
||||||
|
_ida.Future<_ianfx7qj.SimpleLoginCompleted> complete({
|
||||||
|
required _isc.UuidValue attempt,
|
||||||
|
required String code,
|
||||||
|
}) => caller.callServerEndpoint<_ianfx7qj.SimpleLoginCompleted>(
|
||||||
|
'halkyon_base.simpleLogin',
|
||||||
|
'complete',
|
||||||
|
{
|
||||||
|
'attempt': attempt,
|
||||||
|
'code': code,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
class Caller extends _isc.ModuleEndpointCaller {
|
class Caller extends _isc.ModuleEndpointCaller {
|
||||||
Caller(_isc.ServerpodClientShared client) : super(client) {
|
Caller(_isc.ServerpodClientShared client) : super(client) {
|
||||||
account = EndpointAccount(this);
|
account = EndpointAccount(this);
|
||||||
|
|
@ -743,6 +792,7 @@ class Caller extends _isc.ModuleEndpointCaller {
|
||||||
passwordSignIn = EndpointPasswordSignIn(this);
|
passwordSignIn = EndpointPasswordSignIn(this);
|
||||||
profile = EndpointProfile(this);
|
profile = EndpointProfile(this);
|
||||||
profiles = EndpointProfiles(this);
|
profiles = EndpointProfiles(this);
|
||||||
|
simpleLogin = EndpointSimpleLogin(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
late final EndpointAccount account;
|
late final EndpointAccount account;
|
||||||
|
|
@ -769,6 +819,8 @@ class Caller extends _isc.ModuleEndpointCaller {
|
||||||
|
|
||||||
late final EndpointProfiles profiles;
|
late final EndpointProfiles profiles;
|
||||||
|
|
||||||
|
late final EndpointSimpleLogin simpleLogin;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, _isc.EndpointRef> get endpointRefLookup => {
|
Map<String, _isc.EndpointRef> get endpointRefLookup => {
|
||||||
'halkyon_base.account': account,
|
'halkyon_base.account': account,
|
||||||
|
|
@ -783,5 +835,6 @@ class Caller extends _isc.ModuleEndpointCaller {
|
||||||
'halkyon_base.passwordSignIn': passwordSignIn,
|
'halkyon_base.passwordSignIn': passwordSignIn,
|
||||||
'halkyon_base.profile': profile,
|
'halkyon_base.profile': profile,
|
||||||
'halkyon_base.profiles': profiles,
|
'halkyon_base.profiles': profiles,
|
||||||
|
'halkyon_base.simpleLogin': simpleLogin,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,10 @@ abstract class InstanceBasics
|
||||||
this.dataLocation,
|
this.dataLocation,
|
||||||
this.backupLocation,
|
this.backupLocation,
|
||||||
bool? passwordSignIn,
|
bool? passwordSignIn,
|
||||||
|
bool? simpleLogin,
|
||||||
this.gitHubSignInEnds,
|
this.gitHubSignInEnds,
|
||||||
}) : passwordSignIn = passwordSignIn ?? false;
|
}) : passwordSignIn = passwordSignIn ?? false,
|
||||||
|
simpleLogin = simpleLogin ?? false;
|
||||||
|
|
||||||
factory InstanceBasics({
|
factory InstanceBasics({
|
||||||
required String app,
|
required String app,
|
||||||
|
|
@ -39,6 +41,7 @@ abstract class InstanceBasics
|
||||||
String? dataLocation,
|
String? dataLocation,
|
||||||
String? backupLocation,
|
String? backupLocation,
|
||||||
bool? passwordSignIn,
|
bool? passwordSignIn,
|
||||||
|
bool? simpleLogin,
|
||||||
DateTime? gitHubSignInEnds,
|
DateTime? gitHubSignInEnds,
|
||||||
}) = _InstanceBasicsImpl;
|
}) = _InstanceBasicsImpl;
|
||||||
|
|
||||||
|
|
@ -58,6 +61,9 @@ abstract class InstanceBasics
|
||||||
: _isc.BoolJsonExtension.fromJson(
|
: _isc.BoolJsonExtension.fromJson(
|
||||||
jsonSerialization['passwordSignIn'],
|
jsonSerialization['passwordSignIn'],
|
||||||
),
|
),
|
||||||
|
simpleLogin: jsonSerialization['simpleLogin'] == null
|
||||||
|
? null
|
||||||
|
: _isc.BoolJsonExtension.fromJson(jsonSerialization['simpleLogin']),
|
||||||
gitHubSignInEnds: jsonSerialization['gitHubSignInEnds'] == null
|
gitHubSignInEnds: jsonSerialization['gitHubSignInEnds'] == null
|
||||||
? null
|
? null
|
||||||
: _isc.DateTimeJsonExtension.fromJson(
|
: _isc.DateTimeJsonExtension.fromJson(
|
||||||
|
|
@ -100,6 +106,10 @@ abstract class InstanceBasics
|
||||||
/// operator set the keys it needs.
|
/// operator set the keys it needs.
|
||||||
bool passwordSignIn;
|
bool passwordSignIn;
|
||||||
|
|
||||||
|
/// Whether people sign in with SimpleLogin here (ADR 0020): the
|
||||||
|
/// instance's register has a SimpleLogin app, and its operator offers it.
|
||||||
|
bool simpleLogin;
|
||||||
|
|
||||||
/// When GitHub sign-in ends here (ADR 0017: it is deprecated), as its
|
/// When GitHub sign-in ends here (ADR 0017: it is deprecated), as its
|
||||||
/// operator set it (GITHUB_SIGN_IN_ENDS), in UTC; null while no end is set,
|
/// operator set it (GITHUB_SIGN_IN_ENDS), in UTC; null while no end is set,
|
||||||
/// or once it has ended. People who sign in with GitHub add an address, a
|
/// or once it has ended. People who sign in with GitHub add an address, a
|
||||||
|
|
@ -118,6 +128,7 @@ abstract class InstanceBasics
|
||||||
String? dataLocation,
|
String? dataLocation,
|
||||||
String? backupLocation,
|
String? backupLocation,
|
||||||
bool? passwordSignIn,
|
bool? passwordSignIn,
|
||||||
|
bool? simpleLogin,
|
||||||
DateTime? gitHubSignInEnds,
|
DateTime? gitHubSignInEnds,
|
||||||
});
|
});
|
||||||
@override
|
@override
|
||||||
|
|
@ -132,6 +143,7 @@ abstract class InstanceBasics
|
||||||
if (dataLocation != null) 'dataLocation': dataLocation,
|
if (dataLocation != null) 'dataLocation': dataLocation,
|
||||||
if (backupLocation != null) 'backupLocation': backupLocation,
|
if (backupLocation != null) 'backupLocation': backupLocation,
|
||||||
'passwordSignIn': passwordSignIn,
|
'passwordSignIn': passwordSignIn,
|
||||||
|
'simpleLogin': simpleLogin,
|
||||||
if (gitHubSignInEnds != null)
|
if (gitHubSignInEnds != null)
|
||||||
'gitHubSignInEnds': gitHubSignInEnds?.toJson(),
|
'gitHubSignInEnds': gitHubSignInEnds?.toJson(),
|
||||||
};
|
};
|
||||||
|
|
@ -149,6 +161,7 @@ abstract class InstanceBasics
|
||||||
if (dataLocation != null) 'dataLocation': dataLocation,
|
if (dataLocation != null) 'dataLocation': dataLocation,
|
||||||
if (backupLocation != null) 'backupLocation': backupLocation,
|
if (backupLocation != null) 'backupLocation': backupLocation,
|
||||||
'passwordSignIn': passwordSignIn,
|
'passwordSignIn': passwordSignIn,
|
||||||
|
'simpleLogin': simpleLogin,
|
||||||
if (gitHubSignInEnds != null)
|
if (gitHubSignInEnds != null)
|
||||||
'gitHubSignInEnds': gitHubSignInEnds?.toJson(),
|
'gitHubSignInEnds': gitHubSignInEnds?.toJson(),
|
||||||
};
|
};
|
||||||
|
|
@ -172,6 +185,7 @@ class _InstanceBasicsImpl extends InstanceBasics {
|
||||||
String? dataLocation,
|
String? dataLocation,
|
||||||
String? backupLocation,
|
String? backupLocation,
|
||||||
bool? passwordSignIn,
|
bool? passwordSignIn,
|
||||||
|
bool? simpleLogin,
|
||||||
DateTime? gitHubSignInEnds,
|
DateTime? gitHubSignInEnds,
|
||||||
}) : super._(
|
}) : super._(
|
||||||
app: app,
|
app: app,
|
||||||
|
|
@ -182,6 +196,7 @@ class _InstanceBasicsImpl extends InstanceBasics {
|
||||||
dataLocation: dataLocation,
|
dataLocation: dataLocation,
|
||||||
backupLocation: backupLocation,
|
backupLocation: backupLocation,
|
||||||
passwordSignIn: passwordSignIn,
|
passwordSignIn: passwordSignIn,
|
||||||
|
simpleLogin: simpleLogin,
|
||||||
gitHubSignInEnds: gitHubSignInEnds,
|
gitHubSignInEnds: gitHubSignInEnds,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -198,6 +213,7 @@ class _InstanceBasicsImpl extends InstanceBasics {
|
||||||
Object? dataLocation = _Undefined,
|
Object? dataLocation = _Undefined,
|
||||||
Object? backupLocation = _Undefined,
|
Object? backupLocation = _Undefined,
|
||||||
bool? passwordSignIn,
|
bool? passwordSignIn,
|
||||||
|
bool? simpleLogin,
|
||||||
Object? gitHubSignInEnds = _Undefined,
|
Object? gitHubSignInEnds = _Undefined,
|
||||||
}) {
|
}) {
|
||||||
return InstanceBasics(
|
return InstanceBasics(
|
||||||
|
|
@ -213,6 +229,7 @@ class _InstanceBasicsImpl extends InstanceBasics {
|
||||||
? backupLocation
|
? backupLocation
|
||||||
: this.backupLocation,
|
: this.backupLocation,
|
||||||
passwordSignIn: passwordSignIn ?? this.passwordSignIn,
|
passwordSignIn: passwordSignIn ?? this.passwordSignIn,
|
||||||
|
simpleLogin: simpleLogin ?? this.simpleLogin,
|
||||||
gitHubSignInEnds: gitHubSignInEnds is DateTime?
|
gitHubSignInEnds: gitHubSignInEnds is DateTime?
|
||||||
? gitHubSignInEnds
|
? gitHubSignInEnds
|
||||||
: this.gitHubSignInEnds,
|
: this.gitHubSignInEnds,
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,9 @@ import 'profile/profile.dart' as _ia5j8fch;
|
||||||
import 'profile/profile_not_found_exception.dart' as _iim3hfcf;
|
import 'profile/profile_not_found_exception.dart' as _iim3hfcf;
|
||||||
import 'profile/profile_refusal.dart' as _i6j6efd1;
|
import 'profile/profile_refusal.dart' as _i6j6efd1;
|
||||||
import 'profile/profile_refused_exception.dart' as _idvc5e7p;
|
import 'profile/profile_refused_exception.dart' as _idvc5e7p;
|
||||||
|
import 'simplelogin/simplelogin_completed.dart' as _i51ry5s2;
|
||||||
|
import 'simplelogin/simplelogin_outcome.dart' as _i5ar1hzl;
|
||||||
|
import 'simplelogin/simplelogin_platform.dart' as _ivny5pkt;
|
||||||
export 'auth/email_code_purpose.dart';
|
export 'auth/email_code_purpose.dart';
|
||||||
export 'auth/email_sign_in_refusal.dart';
|
export 'auth/email_sign_in_refusal.dart';
|
||||||
export 'auth/email_sign_in_refused_exception.dart';
|
export 'auth/email_sign_in_refused_exception.dart';
|
||||||
|
|
@ -75,6 +78,9 @@ export 'profile/profile.dart';
|
||||||
export 'profile/profile_not_found_exception.dart';
|
export 'profile/profile_not_found_exception.dart';
|
||||||
export 'profile/profile_refusal.dart';
|
export 'profile/profile_refusal.dart';
|
||||||
export 'profile/profile_refused_exception.dart';
|
export 'profile/profile_refused_exception.dart';
|
||||||
|
export 'simplelogin/simplelogin_completed.dart';
|
||||||
|
export 'simplelogin/simplelogin_outcome.dart';
|
||||||
|
export 'simplelogin/simplelogin_platform.dart';
|
||||||
export 'client.dart';
|
export 'client.dart';
|
||||||
|
|
||||||
class Protocol extends _isc.SerializationManager {
|
class Protocol extends _isc.SerializationManager {
|
||||||
|
|
@ -206,6 +212,15 @@ class Protocol extends _isc.SerializationManager {
|
||||||
if (t == _idvc5e7p.ProfileRefusedException) {
|
if (t == _idvc5e7p.ProfileRefusedException) {
|
||||||
return _idvc5e7p.ProfileRefusedException.fromJson(data) as T;
|
return _idvc5e7p.ProfileRefusedException.fromJson(data) as T;
|
||||||
}
|
}
|
||||||
|
if (t == _i51ry5s2.SimpleLoginCompleted) {
|
||||||
|
return _i51ry5s2.SimpleLoginCompleted.fromJson(data) as T;
|
||||||
|
}
|
||||||
|
if (t == _i5ar1hzl.SimpleLoginOutcome) {
|
||||||
|
return _i5ar1hzl.SimpleLoginOutcome.fromJson(data) as T;
|
||||||
|
}
|
||||||
|
if (t == _ivny5pkt.SimpleLoginPlatform) {
|
||||||
|
return _ivny5pkt.SimpleLoginPlatform.fromJson(data) as T;
|
||||||
|
}
|
||||||
if (t == _isc.getType<_ifpka2on.EmailCodePurpose?>()) {
|
if (t == _isc.getType<_ifpka2on.EmailCodePurpose?>()) {
|
||||||
return (data != null ? _ifpka2on.EmailCodePurpose.fromJson(data) : null)
|
return (data != null ? _ifpka2on.EmailCodePurpose.fromJson(data) : null)
|
||||||
as T;
|
as T;
|
||||||
|
|
@ -328,6 +343,22 @@ class Protocol extends _isc.SerializationManager {
|
||||||
: null)
|
: null)
|
||||||
as T;
|
as T;
|
||||||
}
|
}
|
||||||
|
if (t == _isc.getType<_i51ry5s2.SimpleLoginCompleted?>()) {
|
||||||
|
return (data != null
|
||||||
|
? _i51ry5s2.SimpleLoginCompleted.fromJson(data)
|
||||||
|
: null)
|
||||||
|
as T;
|
||||||
|
}
|
||||||
|
if (t == _isc.getType<_i5ar1hzl.SimpleLoginOutcome?>()) {
|
||||||
|
return (data != null ? _i5ar1hzl.SimpleLoginOutcome.fromJson(data) : null)
|
||||||
|
as T;
|
||||||
|
}
|
||||||
|
if (t == _isc.getType<_ivny5pkt.SimpleLoginPlatform?>()) {
|
||||||
|
return (data != null
|
||||||
|
? _ivny5pkt.SimpleLoginPlatform.fromJson(data)
|
||||||
|
: null)
|
||||||
|
as T;
|
||||||
|
}
|
||||||
if (t == Set<String>) {
|
if (t == Set<String>) {
|
||||||
return (data as List).map((e) => deserialize<String>(e)).toSet() as T;
|
return (data as List).map((e) => deserialize<String>(e)).toSet() as T;
|
||||||
}
|
}
|
||||||
|
|
@ -391,6 +422,9 @@ class Protocol extends _isc.SerializationManager {
|
||||||
_iim3hfcf.ProfileNotFoundException => 'ProfileNotFoundException',
|
_iim3hfcf.ProfileNotFoundException => 'ProfileNotFoundException',
|
||||||
_i6j6efd1.ProfileRefusal => 'ProfileRefusal',
|
_i6j6efd1.ProfileRefusal => 'ProfileRefusal',
|
||||||
_idvc5e7p.ProfileRefusedException => 'ProfileRefusedException',
|
_idvc5e7p.ProfileRefusedException => 'ProfileRefusedException',
|
||||||
|
_i51ry5s2.SimpleLoginCompleted => 'SimpleLoginCompleted',
|
||||||
|
_i5ar1hzl.SimpleLoginOutcome => 'SimpleLoginOutcome',
|
||||||
|
_ivny5pkt.SimpleLoginPlatform => 'SimpleLoginPlatform',
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -464,6 +498,12 @@ class Protocol extends _isc.SerializationManager {
|
||||||
return 'ProfileRefusal';
|
return 'ProfileRefusal';
|
||||||
case _idvc5e7p.ProfileRefusedException():
|
case _idvc5e7p.ProfileRefusedException():
|
||||||
return 'ProfileRefusedException';
|
return 'ProfileRefusedException';
|
||||||
|
case _i51ry5s2.SimpleLoginCompleted():
|
||||||
|
return 'SimpleLoginCompleted';
|
||||||
|
case _i5ar1hzl.SimpleLoginOutcome():
|
||||||
|
return 'SimpleLoginOutcome';
|
||||||
|
case _ivny5pkt.SimpleLoginPlatform():
|
||||||
|
return 'SimpleLoginPlatform';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -558,6 +598,15 @@ class Protocol extends _isc.SerializationManager {
|
||||||
if (dataClassName == 'ProfileRefusedException') {
|
if (dataClassName == 'ProfileRefusedException') {
|
||||||
return deserialize<_idvc5e7p.ProfileRefusedException>(data['data']);
|
return deserialize<_idvc5e7p.ProfileRefusedException>(data['data']);
|
||||||
}
|
}
|
||||||
|
if (dataClassName == 'SimpleLoginCompleted') {
|
||||||
|
return deserialize<_i51ry5s2.SimpleLoginCompleted>(data['data']);
|
||||||
|
}
|
||||||
|
if (dataClassName == 'SimpleLoginOutcome') {
|
||||||
|
return deserialize<_i5ar1hzl.SimpleLoginOutcome>(data['data']);
|
||||||
|
}
|
||||||
|
if (dataClassName == 'SimpleLoginPlatform') {
|
||||||
|
return deserialize<_ivny5pkt.SimpleLoginPlatform>(data['data']);
|
||||||
|
}
|
||||||
return super.deserializeByClassName(data);
|
return super.deserializeByClassName(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||||
|
/* To generate run: "serverpod generate" */
|
||||||
|
|
||||||
|
// ignore_for_file: implementation_imports
|
||||||
|
// ignore_for_file: library_private_types_in_public_api
|
||||||
|
// ignore_for_file: non_constant_identifier_names
|
||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
// ignore_for_file: type_literal_in_constant_pattern
|
||||||
|
// ignore_for_file: use_super_parameters
|
||||||
|
// ignore_for_file: invalid_use_of_internal_member
|
||||||
|
|
||||||
|
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||||
|
import 'package:halkyon_base_client/src/protocol/protocol.dart' as _iwgbpyo5;
|
||||||
|
import 'package:serverpod_auth_core_client/serverpod_auth_core_client.dart'
|
||||||
|
as _iacc;
|
||||||
|
import 'package:serverpod_client/serverpod_client.dart' as _isc;
|
||||||
|
import '../simplelogin/simplelogin_outcome.dart' as _i2lb2chw;
|
||||||
|
|
||||||
|
/// A SimpleLogin sign-in ended (ADR 0020, 5 and 6), as the app gets it: the
|
||||||
|
/// session where it signed in; the handle suggested from the name SimpleLogin
|
||||||
|
/// shared for someone new here, which nothing keeps (null where none could
|
||||||
|
/// be made of it).
|
||||||
|
abstract class SimpleLoginCompleted
|
||||||
|
implements _isc.SerializableModel, _isc.ProtocolSerialization {
|
||||||
|
SimpleLoginCompleted._({
|
||||||
|
required this.outcome,
|
||||||
|
this.session,
|
||||||
|
this.suggestedHandle,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory SimpleLoginCompleted({
|
||||||
|
required _i2lb2chw.SimpleLoginOutcome outcome,
|
||||||
|
_iacc.AuthSuccess? session,
|
||||||
|
String? suggestedHandle,
|
||||||
|
}) = _SimpleLoginCompletedImpl;
|
||||||
|
|
||||||
|
factory SimpleLoginCompleted.fromJson(
|
||||||
|
Map<String, dynamic> jsonSerialization,
|
||||||
|
) {
|
||||||
|
return SimpleLoginCompleted(
|
||||||
|
outcome: _i2lb2chw.SimpleLoginOutcome.fromJson(
|
||||||
|
(jsonSerialization['outcome'] as String),
|
||||||
|
),
|
||||||
|
session: jsonSerialization['session'] == null
|
||||||
|
? null
|
||||||
|
: _iwgbpyo5.Protocol().deserialize<_iacc.AuthSuccess>(
|
||||||
|
jsonSerialization['session'],
|
||||||
|
),
|
||||||
|
suggestedHandle: jsonSerialization['suggestedHandle'] as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_i2lb2chw.SimpleLoginOutcome outcome;
|
||||||
|
|
||||||
|
_iacc.AuthSuccess? session;
|
||||||
|
|
||||||
|
String? suggestedHandle;
|
||||||
|
|
||||||
|
/// Returns a shallow copy of this [SimpleLoginCompleted]
|
||||||
|
/// with some or all fields replaced by the given arguments.
|
||||||
|
@_isc.useResult
|
||||||
|
SimpleLoginCompleted copyWith({
|
||||||
|
_i2lb2chw.SimpleLoginOutcome? outcome,
|
||||||
|
_iacc.AuthSuccess? session,
|
||||||
|
String? suggestedHandle,
|
||||||
|
});
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'__className__': 'halkyon_base.SimpleLoginCompleted',
|
||||||
|
'outcome': outcome.toJson(),
|
||||||
|
if (session != null) 'session': session?.toJson(),
|
||||||
|
if (suggestedHandle != null) 'suggestedHandle': suggestedHandle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJsonForProtocol() {
|
||||||
|
return {
|
||||||
|
'__className__': 'halkyon_base.SimpleLoginCompleted',
|
||||||
|
'outcome': outcome.toJson(),
|
||||||
|
if (session != null) 'session': session?.toJson(),
|
||||||
|
if (suggestedHandle != null) 'suggestedHandle': suggestedHandle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return _isc.SerializationManager.encode(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Undefined {}
|
||||||
|
|
||||||
|
class _SimpleLoginCompletedImpl extends SimpleLoginCompleted {
|
||||||
|
_SimpleLoginCompletedImpl({
|
||||||
|
required _i2lb2chw.SimpleLoginOutcome outcome,
|
||||||
|
_iacc.AuthSuccess? session,
|
||||||
|
String? suggestedHandle,
|
||||||
|
}) : super._(
|
||||||
|
outcome: outcome,
|
||||||
|
session: session,
|
||||||
|
suggestedHandle: suggestedHandle,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Returns a shallow copy of this [SimpleLoginCompleted]
|
||||||
|
/// with some or all fields replaced by the given arguments.
|
||||||
|
@_isc.useResult
|
||||||
|
@override
|
||||||
|
SimpleLoginCompleted copyWith({
|
||||||
|
_i2lb2chw.SimpleLoginOutcome? outcome,
|
||||||
|
Object? session = _Undefined,
|
||||||
|
Object? suggestedHandle = _Undefined,
|
||||||
|
}) {
|
||||||
|
return SimpleLoginCompleted(
|
||||||
|
outcome: outcome ?? this.outcome,
|
||||||
|
session: session is _iacc.AuthSuccess?
|
||||||
|
? session
|
||||||
|
: this.session?.copyWith(),
|
||||||
|
suggestedHandle: suggestedHandle is String?
|
||||||
|
? suggestedHandle
|
||||||
|
: this.suggestedHandle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||||
|
/* To generate run: "serverpod generate" */
|
||||||
|
|
||||||
|
// ignore_for_file: implementation_imports
|
||||||
|
// ignore_for_file: library_private_types_in_public_api
|
||||||
|
// ignore_for_file: non_constant_identifier_names
|
||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
// ignore_for_file: type_literal_in_constant_pattern
|
||||||
|
// ignore_for_file: use_super_parameters
|
||||||
|
// ignore_for_file: invalid_use_of_internal_member
|
||||||
|
|
||||||
|
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||||
|
import 'package:serverpod_client/serverpod_client.dart' as _isc;
|
||||||
|
|
||||||
|
/// How a SimpleLogin sign-in ended (ADR 0020, 5 and 6): signed in to the
|
||||||
|
/// account with that identity; the identity added to the account of who
|
||||||
|
/// began it signed in; or someone new here, signed out, whose identity
|
||||||
|
/// waits for them to make an account or to mend the link to theirs.
|
||||||
|
enum SimpleLoginOutcome implements _isc.SerializableModel {
|
||||||
|
signedIn,
|
||||||
|
linked,
|
||||||
|
newcomer;
|
||||||
|
|
||||||
|
static SimpleLoginOutcome fromJson(String name) {
|
||||||
|
switch (name) {
|
||||||
|
case 'signedIn':
|
||||||
|
return SimpleLoginOutcome.signedIn;
|
||||||
|
case 'linked':
|
||||||
|
return SimpleLoginOutcome.linked;
|
||||||
|
case 'newcomer':
|
||||||
|
return SimpleLoginOutcome.newcomer;
|
||||||
|
default:
|
||||||
|
throw ArgumentError(
|
||||||
|
'Value "$name" cannot be converted to "SimpleLoginOutcome"',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toJson() => name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => name;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||||
|
/* To generate run: "serverpod generate" */
|
||||||
|
|
||||||
|
// ignore_for_file: implementation_imports
|
||||||
|
// ignore_for_file: library_private_types_in_public_api
|
||||||
|
// ignore_for_file: non_constant_identifier_names
|
||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
// ignore_for_file: type_literal_in_constant_pattern
|
||||||
|
// ignore_for_file: use_super_parameters
|
||||||
|
// ignore_for_file: invalid_use_of_internal_member
|
||||||
|
|
||||||
|
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||||
|
import 'package:serverpod_client/serverpod_client.dart' as _isc;
|
||||||
|
|
||||||
|
/// Where an app begins a SimpleLogin sign-in (ADR 0020, 3), which says where
|
||||||
|
/// the forwarding page sends it back: the desktop's loopback, or the phone's
|
||||||
|
/// scheme (or, with learn.halkyon.eu's App Links, the phone's app itself).
|
||||||
|
enum SimpleLoginPlatform implements _isc.SerializableModel {
|
||||||
|
desktop,
|
||||||
|
phone;
|
||||||
|
|
||||||
|
static SimpleLoginPlatform fromJson(String name) {
|
||||||
|
switch (name) {
|
||||||
|
case 'desktop':
|
||||||
|
return SimpleLoginPlatform.desktop;
|
||||||
|
case 'phone':
|
||||||
|
return SimpleLoginPlatform.phone;
|
||||||
|
default:
|
||||||
|
throw ArgumentError(
|
||||||
|
'Value "$name" cannot be converted to "SimpleLoginPlatform"',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toJson() => name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => name;
|
||||||
|
}
|
||||||
|
|
@ -1,8 +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 (with a password and TOTP, ADR 0017,
|
generated client `halkyon_base_client`): signing in (with SimpleLogin, Halkyon Learn's
|
||||||
and the older ways), getting back into an account, the account's ways in and its
|
ADR 0020, 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
|
||||||
|
|
@ -19,11 +19,12 @@ Endpoints, under the nickname `base` in a host's client (`client.modules.base.em
|
||||||
| `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 |
|
| `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 |
|
| `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 |
|
| `accountRecovery` | `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 |
|
| `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 (their account comes with step 3); signed in, SimpleLogin added to the caller's account, who becomes `identified` | anyone |
|
||||||
|
| `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 |
|
| `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 |
|
| `emailCode` | `sendCode`: a code by mail, which proves the address and signs nobody in | anyone |
|
||||||
| `account` | `delete`, `signInWays` | signed in |
|
| `account` | `delete`, `signInWays` | signed in |
|
||||||
| `instance` | `basics`: the app it serves, GitHub's client ID if any and when GitHub sign-in ends, whether codes by mail and password sign-in are 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 and when GitHub sign-in ends, whether codes by mail, password sign-in and SimpleLogin 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 |
|
||||||
|
|
||||||
|
|
@ -43,6 +44,16 @@ 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.
|
||||||
|
- SimpleLogin (Halkyon Learn's ADR 0020) is halkyon_oidc's sign-in (ADR 0023) with its
|
||||||
|
SimpleLogin preset, on the register alone, which holds the instance's SimpleLogin app
|
||||||
|
and exchanges every code; a member asks it to begin and to complete, and issues its own
|
||||||
|
session. The attempt (ten minutes, taken once) holds a nonce and the caller, its state
|
||||||
|
the app's and platform's marker; the ID token is checked by halkyon_oidc (RS256, the
|
||||||
|
issuer, the audience, the expiry, the nonce). Only SimpleLogin's ID is kept
|
||||||
|
(`halkyon_oidc_identity`, the provider `simplelogin`): one per account, each an
|
||||||
|
account's. Someone new's identity waits 30 minutes under the attempt's ID, with a
|
||||||
|
handle suggested from the name SimpleLogin shared (`handleFromName`: `anna_mueller`,
|
||||||
|
a number added where taken), which nothing keeps.
|
||||||
- SimpleLogin's forwarding page (Halkyon Learn's ADR 0020; `addSimpleLoginCallbackRoute`,
|
- SimpleLogin's forwarding page (Halkyon Learn's ADR 0020; `addSimpleLoginCallbackRoute`,
|
||||||
on the register, on halkyon_oidc's `addOidcCallbackForwarding`, ADR 0023):
|
on the register, on halkyon_oidc's `addOidcCallbackForwarding`, ADR 0023):
|
||||||
`/simplelogin-callback`, the one address the instance's SimpleLogin
|
`/simplelogin-callback`, the one address the instance's SimpleLogin
|
||||||
|
|
@ -417,8 +428,8 @@ Built on the three parts above, on the register (members forward every step thro
|
||||||
`pruneAbandonedAnonymousPeople`, `forgetGitHubAccounts` (the register's),
|
`pruneAbandonedAnonymousPeople`, `forgetGitHubAccounts` (the register's),
|
||||||
`forgetLapsedResets` and `forgetPeopleTheRegisterForgot` (a member's), hourly
|
`forgetLapsedResets` and `forgetPeopleTheRegisterForgot` (a member's), hourly
|
||||||
`forgetOldSignUpAttempts`, `forgetExpiredEmailCodes`,
|
`forgetOldSignUpAttempts`, `forgetExpiredEmailCodes`,
|
||||||
`forgetUnconfirmedAuthenticators`, `deliverDeletionNotices` and
|
`forgetUnconfirmedAuthenticators`, `forgetUnfinishedSimpleLogins` (the
|
||||||
`deliverSessionNotices` (the register's). Each marked as one mode's does nothing in
|
register's), `deliverDeletionNotices` and `deliverSessionNotices` (the register's). Each marked as one mode's does nothing in
|
||||||
the other.
|
the other.
|
||||||
|
|
||||||
4. A host's deletion (`PersonDataDeletion`) runs inside `deletePerson`'s transaction,
|
4. A host's deletion (`PersonDataDeletion`) runs inside `deletePerson`'s transaction,
|
||||||
|
|
@ -438,13 +449,19 @@ The passwords it reads: `serverSideSessionKeyHashPepper`; for GitHub `githubClie
|
||||||
`emailSmtpPort`, `emailSmtpUser`, `emailSmtpPassword`, `emailFrom`; for passwords
|
`emailSmtpPort`, `emailSmtpUser`, `emailSmtpPassword`, `emailFrom`; for passwords
|
||||||
`totpKey`, `recoveryCodeKey` and `signInDecoyKey` (each at least 32 characters, or
|
`totpKey`, `recoveryCodeKey` and `signInDecoyKey` (each at least 32 characters, or
|
||||||
`addInstanceRoutes` stops the server; all three and email sign-in, or no password
|
`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
|
sign-in); for SimpleLogin, on the register, `simpleLoginClientId` and
|
||||||
|
`simpleLoginClientSecret` (the instance's SimpleLogin app, both or none); between the
|
||||||
|
instance's services `instanceSecret`. Never change either pepper
|
||||||
once people sign in: every session or address hash would be lost; changing `totpKey`
|
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
|
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; `GITHUB_SIGN_IN_ENDS`, the day GitHub stops signing anyone in (`2027-01-22`);
|
one; `SIMPLELOGIN_CALLBACK_URL`, the https address the SimpleLogin app registered, where
|
||||||
|
it isn't the register's own page (`/simplelogin-callback` on its web server's public
|
||||||
|
address; development names learn.halkyon.eu's); `SIGN_IN_WAYS`, the ways in the
|
||||||
|
register offers of those set up, `simplelogin`, `password` or both (unset: every one), a
|
||||||
|
name it doesn't know stopping the server (`addInstanceRoutes`); `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
|
`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;
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
/// deletion and the pruning, email sign-in's helpers, the instance's
|
/// deletion and the pruning, email sign-in's helpers, the instance's
|
||||||
/// basics, people's public profiles and their pages, the instance's register
|
/// basics, people's public profiles and their pages, the instance's register
|
||||||
/// (ADR 0016), signing in with a password and TOTP and getting back into an
|
/// (ADR 0016), signing in with a password and TOTP and getting back into an
|
||||||
/// account (ADR 0017), SimpleLogin's forwarding page (Halkyon Learn's ADR
|
/// account (ADR 0017), signing in with SimpleLogin and its forwarding page
|
||||||
/// 0020), and the module's models.
|
/// (ADR 0020), the ways in an operator offers, and the module's models.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
export 'src/auth/account.dart';
|
export 'src/auth/account.dart';
|
||||||
|
|
@ -14,6 +14,7 @@ export 'src/auth/email.dart';
|
||||||
export 'src/auth/github.dart';
|
export 'src/auth/github.dart';
|
||||||
export 'src/auth/github_sign_in_endpoint.dart' show gitHubAccountToSignIn;
|
export 'src/auth/github_sign_in_endpoint.dart' show gitHubAccountToSignIn;
|
||||||
export 'src/auth/identified.dart';
|
export 'src/auth/identified.dart';
|
||||||
|
export 'src/auth/offered_ways.dart';
|
||||||
export 'src/auth/scopes.dart';
|
export 'src/auth/scopes.dart';
|
||||||
export 'src/common/network.dart';
|
export 'src/common/network.dart';
|
||||||
export 'src/common/short_code.dart';
|
export 'src/common/short_code.dart';
|
||||||
|
|
@ -74,11 +75,15 @@ export 'src/register/member_pruning.dart';
|
||||||
export 'src/register/people.dart' show issueSession;
|
export 'src/register/people.dart' show issueSession;
|
||||||
export 'src/register/sessions_ended.dart'
|
export 'src/register/sessions_ended.dart'
|
||||||
show AccountSessionsEnded, deliverSessionNotices;
|
show AccountSessionsEnded, deliverSessionNotices;
|
||||||
|
export 'src/simplelogin/handle_suggestion.dart'
|
||||||
|
show handleFromName, handlesToTry, suggestable;
|
||||||
|
export 'src/simplelogin/simplelogin.dart' show forgetUnfinishedSimpleLogins;
|
||||||
export 'src/simplelogin/simplelogin_callback.dart'
|
export 'src/simplelogin/simplelogin_callback.dart'
|
||||||
show
|
show
|
||||||
addSimpleLoginCallbackRoute,
|
addSimpleLoginCallbackRoute,
|
||||||
simpleLoginCallbackPath,
|
simpleLoginCallbackPath,
|
||||||
simpleLoginReturns;
|
simpleLoginReturns;
|
||||||
|
export 'src/simplelogin/simplelogin_config.dart';
|
||||||
export 'src/srp/srp_group.dart';
|
export 'src/srp/srp_group.dart';
|
||||||
export 'src/srp/srp_math.dart' show numberOf, padded;
|
export 'src/srp/srp_math.dart' show numberOf, padded;
|
||||||
export 'src/srp/srp_server.dart'
|
export 'src/srp/srp_server.dart'
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:halkyon_base_server/src/auth/github.dart';
|
import 'package:halkyon_base_server/src/auth/github.dart';
|
||||||
|
import 'package:halkyon_base_server/src/simplelogin/simplelogin_config.dart';
|
||||||
import 'package:serverpod/serverpod.dart';
|
import 'package:serverpod/serverpod.dart';
|
||||||
import 'package:serverpod_auth_idp_server/core.dart';
|
import 'package:serverpod_auth_idp_server/core.dart';
|
||||||
import 'package:serverpod_auth_idp_server/providers/anonymous.dart';
|
import 'package:serverpod_auth_idp_server/providers/anonymous.dart';
|
||||||
|
|
@ -38,7 +39,12 @@ const anonymousSignUpLimit = RateLimit(
|
||||||
/// GitHub only where the operator registered a GitHub OAuth app (see
|
/// GitHub only where the operator registered a GitHub OAuth app (see
|
||||||
/// [gitHubSignInAvailable]); its helpers serve our own slim login
|
/// [gitHubSignInAvailable]); its helpers serve our own slim login
|
||||||
/// (`github.dart`), and Serverpod's stock GitHub login endpoint isn't exposed.
|
/// (`github.dart`), and Serverpod's stock GitHub login endpoint isn't exposed.
|
||||||
|
///
|
||||||
|
/// SimpleLogin (ADR 0020) on the register, where it has the instance's
|
||||||
|
/// SimpleLogin app ([simpleLoginIdpConfig]): halkyon_oidc's sign-in, whose
|
||||||
|
/// parts `simplelogin.dart` uses; its stock endpoint isn't exposed either.
|
||||||
List<IdentityProviderBuilder> identityProviderBuilders(Serverpod pod) => [
|
List<IdentityProviderBuilder> identityProviderBuilders(Serverpod pod) => [
|
||||||
const AnonymousIdpConfig(perIpAddressRateLimit: null),
|
const AnonymousIdpConfig(perIpAddressRateLimit: null),
|
||||||
if (gitHubSignInAvailable(pod)) GitHubIdpConfigFromPasswords(),
|
if (gitHubSignInAvailable(pod)) GitHubIdpConfigFromPasswords(),
|
||||||
|
?simpleLoginIdpConfig(pod),
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import 'package:halkyon_base_server/src/generated/protocol.dart';
|
||||||
import 'package:halkyon_base_server/src/password/password_config.dart';
|
import 'package:halkyon_base_server/src/password/password_config.dart';
|
||||||
import 'package:halkyon_base_server/src/register/deletion_notices.dart';
|
import 'package:halkyon_base_server/src/register/deletion_notices.dart';
|
||||||
import 'package:halkyon_base_server/src/register/people.dart';
|
import 'package:halkyon_base_server/src/register/people.dart';
|
||||||
|
import 'package:halkyon_base_server/src/simplelogin/simplelogin_config.dart';
|
||||||
import 'package:serverpod/serverpod.dart';
|
import 'package:serverpod/serverpod.dart';
|
||||||
import 'package:serverpod_auth_idp_server/core.dart';
|
import 'package:serverpod_auth_idp_server/core.dart';
|
||||||
import 'package:serverpod_auth_idp_server/providers/github.dart';
|
import 'package:serverpod_auth_idp_server/providers/github.dart';
|
||||||
|
|
@ -207,7 +208,8 @@ Future<String> verifiedGitHubUserId(
|
||||||
/// Linking the account the person already has changes nothing. Refused, with
|
/// Linking the account the person already has changes nothing. Refused, with
|
||||||
/// nothing changed, when the account belongs to someone else or the person
|
/// nothing changed, when the account belongs to someone else or the person
|
||||||
/// has another GitHub account, and wherever people sign in with a password
|
/// has another GitHub account, and wherever people sign in with a password
|
||||||
/// (ADR 0017: GitHub is deprecated, [GitHubSignInRefusal.deprecated])
|
/// or SimpleLogin (ADR 0017, ADR 0020: GitHub is deprecated,
|
||||||
|
/// [GitHubSignInRefusal.deprecated])
|
||||||
/// ([GitHubSignInRefusedException]). An account someone else has is said to
|
/// ([GitHubSignInRefusedException]). An account someone else has is said to
|
||||||
/// be so there too, as nothing would be linked: an anonymous person's app
|
/// be so there too, as nothing would be linked: an anonymous person's app
|
||||||
/// then switches to it (`gitHubSignIn.signIn`), which still signs in those
|
/// then switches to it (`gitHubSignIn.signIn`), which still signs in those
|
||||||
|
|
@ -237,7 +239,8 @@ Future<void> linkGitHubAccount(
|
||||||
reason: GitHubSignInRefusal.takenByAnotherPerson,
|
reason: GitHubSignInRefusal.takenByAnotherPerson,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (passwordSignInAvailable(session.serverpod)) {
|
if (passwordSignInAvailable(session.serverpod) ||
|
||||||
|
simpleLoginAvailable(session.serverpod)) {
|
||||||
throw GitHubSignInRefusedException(
|
throw GitHubSignInRefusedException(
|
||||||
reason: GitHubSignInRefusal.deprecated,
|
reason: GitHubSignInRefusal.deprecated,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
59
packages/halkyon_base_server/lib/src/auth/offered_ways.dart
Normal file
59
packages/halkyon_base_server/lib/src/auth/offered_ways.dart
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:serverpod/serverpod.dart';
|
||||||
|
|
||||||
|
/// A way in an operator may offer on their instance (ADR 0020, 1), by its
|
||||||
|
/// name in `SIGN_IN_WAYS`.
|
||||||
|
enum OfferedWay {
|
||||||
|
simpleLogin('simplelogin'),
|
||||||
|
password('password');
|
||||||
|
|
||||||
|
OfferedWay(this.setting);
|
||||||
|
|
||||||
|
/// Its name in `SIGN_IN_WAYS`.
|
||||||
|
final String setting;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The ways in [pod]'s operator offers (`SIGN_IN_WAYS`); null for every way
|
||||||
|
/// whose keys are there. The register's alone counts: a member asks the
|
||||||
|
/// register what it offers. Throws a [StateError] for a way it doesn't know
|
||||||
|
/// ([offeredWaysFrom]), which hosts check as they start
|
||||||
|
/// (`addInstanceRoutes`).
|
||||||
|
Set<OfferedWay>? offeredWaysOf(Serverpod pod) => switch (_waysOf[pod]) {
|
||||||
|
(:final ways) => ways,
|
||||||
|
null => offeredWaysFrom(Platform.environment['SIGN_IN_WAYS']),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The ways [value] (`SIGN_IN_WAYS`) names, by commas or spaces, whatever
|
||||||
|
/// their case: `simplelogin`, `password` or both; null for none named
|
||||||
|
/// (unset or empty, as Compose passes an unset variable), which offers every
|
||||||
|
/// way whose keys are there. Throws a [StateError] for any other name.
|
||||||
|
Set<OfferedWay>? offeredWaysFrom(String? value) {
|
||||||
|
final names = [
|
||||||
|
for (final name in (value ?? '').split(RegExp(r'[\s,]+')))
|
||||||
|
if (name.isNotEmpty) name.toLowerCase(),
|
||||||
|
];
|
||||||
|
if (names.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final byName = {for (final way in OfferedWay.values) way.setting: way};
|
||||||
|
return {
|
||||||
|
for (final name in names)
|
||||||
|
byName[name] ??
|
||||||
|
(throw StateError(
|
||||||
|
'SIGN_IN_WAYS names "$name": the ways are '
|
||||||
|
'${OfferedWay.values.map((way) => way.setting).join(' and ')}.',
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether [pod]'s operator offers [way], its keys there or not.
|
||||||
|
bool offersWay(Serverpod pod, OfferedWay way) =>
|
||||||
|
offeredWaysOf(pod)?.contains(way) ?? true;
|
||||||
|
|
||||||
|
/// Puts [ways] in place of the operator's `SIGN_IN_WAYS` on [pod], null
|
||||||
|
/// meaning every way whatever the environment says: tests do.
|
||||||
|
void useOfferedWays(Serverpod pod, Set<OfferedWay>? ways) =>
|
||||||
|
_waysOf[pod] = (ways: ways);
|
||||||
|
|
||||||
|
final _waysOf = Expando<({Set<OfferedWay>? ways})>('Offered ways');
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue