feat(base)!: no anonymous accounts on the server (ADR 0020, D25)
The server's half of D25: every account has a way in, and the anonymous accounts left from before go with the daily pruning. - The module signs nobody in anonymously: `anonymousIdp` and `gitHubUpgrade` are gone, and so is linking GitHub at the register. - A password is set only by someone whose address is theirs (a `firstPassword` code) or who signs in with GitHub alone (a fresh sign-in and a `signUp` code); an anonymous account is refused `wrongCredentials` before any code is used. - SimpleLogin refuses to link onto a caller without `identified` (`wrongCaller`), the member's endpoint too, so nothing turns an anonymous account into one with a way in. - `pruneAnonymousPeople` deletes every anonymous account left, however new, never one identified. Learn and Todo schedule it as `prune-accounts` and cancel the old `prune-abandoned-anonymous-people` entry once. - Learn: someone without `identified` keeps no files; `FILE_LIMIT_ANONYMOUS_MB` is gone. `minimumAppBuild` is 23, as builds up to 22 start with an anonymous account. - Learn's sync tests make their person and session by SQL in the development database, as Serverpod stores sessions. - Docs, store listings and ADRs 0010, 0014, 0016, 0017 and 0019 amended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
5b9df73756
commit
87671208df
37 changed files with 837 additions and 1353 deletions
|
|
@ -14,14 +14,12 @@ Endpoints, under the nickname `base` in a host's client (`client.modules.base.em
|
|||
|
||||
| Endpoint | Methods | Who may call |
|
||||
|---|---|---|
|
||||
| `anonymousIdp` | `login` (Serverpod's anonymous sign-in, at most 30 new accounts per network and hour) | anyone |
|
||||
| `password` | `set`: the caller's password, with the address a mailed code proves unless they have one; `beginChange`, `change`: a new password with the old one's proof and a code, every other session ending; `setRecoveryKeys`: a new recovery phrase's wrapping, with the old password's proof and a code | signed in |
|
||||
| `passwordSignIn` | `begin`, `complete`: SRP-6a's two steps with a code from an authenticator (or a recovery code; a setup never finished, a code by mail); a session. `signUp`: an account at the address a mailed code proves, with a password; a session | without a session |
|
||||
| `authenticator` | `enrol`, `confirm` (the first brings ten recovery codes; another takes a code from one confirmed before), `list`, `remove`, `newRecoveryCodes` | signed in |
|
||||
| `accountRecovery` | `resetAuthenticator`, `beginKeyProof`, `resetPassword` (with the recovery phrase), `startAccountReset`, `completeAccountReset` | without a session |
|
||||
| `simpleLogin` | `begin(platform)`: an attempt and SimpleLogin's address to open, at most 30 an hour per network; `complete(attempt, code)`: signed out, a session for the account with that SimpleLogin ID, or for someone new the handle suggested from their name; signed in, SimpleLogin added to the caller's account, who becomes `identified`; for an account, whether it has its keys and its handle, the attempt standing ten minutes for the sign-in; `checkHandle(attempt, handle)`: whether someone new could take a handle, at most 30 an hour per attempt and 60 per network; `signUp(attempt, handle, setup)`, without a session: someone new's account, `identified`, with the handle and the keys the device made; a session; `beginMend(attempt, handle)`, `mend(attempt, proof)`, without a session: a broken link mended with the handle and the recovery phrase, the new identity in the old one's place; a session | anyone |
|
||||
| `simpleLogin` | `begin(platform)`: an attempt and SimpleLogin's address to open, at most 30 an hour per network; `complete(attempt, code)`: signed out, a session for the account with that SimpleLogin ID, or for someone new the handle suggested from their name; signed in (`identified` only, `wrongCaller` otherwise), SimpleLogin added to the caller's account; for an account, whether it has its keys and its handle, the attempt standing ten minutes for the sign-in; `checkHandle(attempt, handle)`: whether someone new could take a handle, at most 30 an hour per attempt and 60 per network; `signUp(attempt, handle, setup)`, without a session: someone new's account, `identified`, with the handle and the keys the device made; a session; `beginMend(attempt, handle)`, `mend(attempt, proof)`, without a session: a broken link mended with the handle and the recovery phrase, the new identity in the old one's place; a session | anyone |
|
||||
| `keys` | for an account that signs in with SimpleLogin (Halkyon Learn's ADR 0020, D19 and D20): `begin`, `unlock`: the caller's keys for their device, once the password is proven with its own proof key; `set`: the first keys (and handle) of an account SimpleLogin was added to, with a SimpleLogin sign-in of the last ten minutes; `changePassword`, `beginPhrase` and `resetPassword` (a lost password, with the recovery phrase), `setRecoveryKeys`: each with the password's proof (the phrase's) and such a sign-in (`KeysOwnerProof`), a new password ending every other session; the tenth wrong proof in a row ends the session, as ADR 0017's do | signed in |
|
||||
| `gitHubUpgrade` | `upgrade`: links a GitHub account to the caller, who becomes `identified`; refused where passwords or SimpleLogin are offered (deprecated) | signed in |
|
||||
| `gitHubSignIn` | `signIn`: a session for whoever has that GitHub account, unless they sign in with a password; none once `GITHUB_SIGN_IN_ENDS` has come | without a session |
|
||||
| `emailCode` | `sendCode`: a code by mail, which proves the address and signs nobody in | anyone |
|
||||
| `account` | `delete` (with the password's proof where the account has one, `keysProof` for one that signs in with SimpleLogin), `signInWays` | signed in |
|
||||
|
|
@ -93,22 +91,23 @@ Endpoints, under the nickname `base` in a host's client (`client.modules.base.em
|
|||
`simpleLogin.begin` takes the way back as a `SimpleLoginReturn`), the rest 1 to 512
|
||||
characters of base64url; anything else gets a page saying the sign-in can't go on,
|
||||
repeating nothing it was given, in English or German. It keeps and logs nothing.
|
||||
- **Per-network limits** (new anonymous accounts, codes by mail, password sign-ins,
|
||||
proofs of the recovery phrase) count by the visitor's IP address (`networkOf`): the
|
||||
- **No anonymous accounts** (Halkyon Learn's ADR 0020, D25): every account has a way
|
||||
in. An anonymous account left from before signs into nothing new and keeps no
|
||||
password or SimpleLogin; the app deletes its own as it starts (`account.delete`), and
|
||||
the pruning deletes the rest (`pruneAnonymousPeople`), whatever their age.
|
||||
- **Per-network limits** (codes by mail, password sign-ins, SimpleLogin's sign-ins,
|
||||
handle checks, proofs of the recovery phrase) count by the visitor's IP address (`networkOf`): the
|
||||
connection's, or, for a connection from a proxy the operator trusts
|
||||
(`TRUSTED_PROXIES`), the last address of `X-Forwarded-For`, the one the proxy saw
|
||||
itself. Never the `Forwarded` header, which Caddy passes on as the visitor wrote it,
|
||||
so never Serverpod's `remoteInfo`. An IPv6 visitor counts by their /64
|
||||
(`networkKeyOf`), which one line or server holds whole. Serverpod's own limit on
|
||||
anonymous accounts is off
|
||||
(it counts by the connection's address, behind a proxy the proxy's for everyone); the
|
||||
endpoint counts them itself.
|
||||
(`networkKeyOf`), which one line or server holds whole.
|
||||
- Refusals are `PasswordRefusedException`, `GitHubSignInRefusedException`,
|
||||
`EmailSignInRefusedException` and `ProfileRefusedException`, each with a reason;
|
||||
`ProfileNotFoundException` where there is no profile.
|
||||
- For the host's own code, `package:halkyon_base_server/base.dart`: the configuration,
|
||||
`tokenManagerBuilders` and `identityProviderBuilders`, the `identified` scope and
|
||||
`grantIdentified`, `deletePerson`, the pruning (`pruneAbandonedAnonymousPeople`,
|
||||
`grantIdentified`, `deletePerson`, the pruning (`pruneAnonymousPeople`,
|
||||
`forgetOldSignUpAttempts`, `forgetExpiredEmailCodes`, `forgetUnconfirmedAuthenticators`,
|
||||
`forgetGitHubAccounts`, `forgetLapsedResets`), short codes, `sourceCodeUrl`,
|
||||
`dataLocation`, `backupLocation`, people's profiles as the host's code reads them
|
||||
|
|
@ -125,9 +124,9 @@ One operator's services share one account per person: the same address, GitHub a
|
|||
and handle reach the same person in each, under one account ID (each service's
|
||||
`AuthUser` has it), and deleting the account deletes it everywhere. One service holds
|
||||
the register, its database keeping the ways in (`email_identity`, GitHub's IDs), the
|
||||
handles and profiles; the others are its members and ask it. Anonymous people stay each
|
||||
service's own until they add a way in, which makes them an account of the register under
|
||||
the same ID. Sessions stay each service's own: every app signs in itself.
|
||||
handles and profiles; the others are its members and ask it. An anonymous account left
|
||||
from before stays its service's own. Sessions stay each service's own: every app signs
|
||||
in itself.
|
||||
|
||||
- **Modes** (`AccountsMode`, `HalkyonBase.accounts`; a host reads it with
|
||||
`AccountsMode.fromEnvironment()`): `HoldsRegister` by default, also for an instance of
|
||||
|
|
@ -151,19 +150,19 @@ the same ID. Sessions stay each service's own: every app signs in itself.
|
|||
asks the register to mail a code (the device's network passed on, as the register
|
||||
counts codes per network) and to check it, or to say whose GitHub ID it is, and opens a
|
||||
session of its own for that account (`issueSession`, which makes them a person there
|
||||
under the account's ID the first time). Linking a way in makes the person identified on
|
||||
under the account's ID the first time). Adding a way in makes the person identified on
|
||||
the register and then on the member; a switch token signs in as the account the
|
||||
address is already. Every refusal is the register's, as on the register itself.
|
||||
- **Deleting:** a deletion on a member asks the register first whose account it is
|
||||
(`knownAccounts`), and is refused while it can't be reached, deleting nothing, a
|
||||
person still anonymous on the member too: the register may have linked them while
|
||||
their becoming identified there failed (the app then gives an anonymous account up
|
||||
all the same, leaving it to the pruning). An account of the register's goes there,
|
||||
their becoming identified there failed (the app then deletes an anonymous account
|
||||
left from before all the same, leaving it to the pruning). An account of the register's goes there,
|
||||
which deletes the account and
|
||||
keeps a notice for each member in the same transaction (`account_deletion_notice`),
|
||||
told at once and again hourly (`deliverDeletionNotices`, tried least first) until each
|
||||
confirms; notices to a member no longer named are dropped. An anonymous person goes
|
||||
from their service alone. Each member also deletes, daily, the identified people the
|
||||
confirms; notices to a member no longer named are dropped. An anonymous account goes
|
||||
from its service alone. Each member also deletes, daily, the identified people the
|
||||
register no longer knows (`forgetPeopleTheRegisterForgot`): it asks about everyone
|
||||
before deleting anyone, deletes nobody while the register knows none of them (a
|
||||
register misnamed or emptied), and fails, deleting nobody, while it can't reach it.
|
||||
|
|
@ -238,12 +237,12 @@ Built on the three parts above, on the register (members forward every step thro
|
|||
3–16 passes, 64 MiB–1 GiB), verifier (1 < v < N, as long as N), public keys (32 bytes
|
||||
each) and both wrappings (24-byte nonce, 112 bytes). It needs an address, and a
|
||||
session alone sets none (ADR 0017's fourth reading): an account with an address proves
|
||||
it anew by a code mailed to it for this (`firstPassword`); an anonymous person by a
|
||||
mailed code (`signUp`) for the address they add, which becomes theirs as email sign-in
|
||||
makes it (anonymous → `identified`, same ID); an account that signs in with GitHub
|
||||
alone does that within 10 minutes of a GitHub sign-in on the session asking
|
||||
it anew by a code mailed to it for this (`firstPassword`); an account that signs in
|
||||
with GitHub alone does that within 10 minutes of a GitHub sign-in on the session asking
|
||||
(`gitHubSignInFreshFor`, the session row's method and creation time, a member telling
|
||||
the register; else `signInAgain`, before any code is used). It signs in once an
|
||||
the register; else `signInAgain`, before any code is used), the address it adds proved
|
||||
by a mailed code (`signUp`). Anyone else, an anonymous account left from before among
|
||||
them, sets none (`wrongCredentials`, before any code is used). It signs in once an
|
||||
authenticator is confirmed; until then a new setup replaces it, never once one was,
|
||||
even while a reset leaves the account without one (a new password comes through the
|
||||
recovery phrase or the account's reset alone).
|
||||
|
|
@ -390,17 +389,14 @@ Built on the three parts above, on the register (members forward every step thro
|
|||
(`takesEffectAt`).
|
||||
- **Older ways:** an account that signs in with a password (it had a confirmed
|
||||
authenticator, which a reset may have taken since: `passwordOnly` in its ways in) no
|
||||
longer signs in with GitHub (`usePassword`); where passwords are offered, no new GitHub
|
||||
account is linked (`deprecated`; one someone else has is still `takenByAnotherPerson`,
|
||||
so an anonymous person's app switches to it); from `GITHUB_SIGN_IN_ENDS` on GitHub signs nobody in
|
||||
longer signs in with GitHub (`usePassword`); no account gets GitHub any more (D25),
|
||||
so it signs in only those who have it; from `GITHUB_SIGN_IN_ENDS` on GitHub signs nobody in
|
||||
(`ended`), the basics stop naming the day (`gitHubSignInEnds`, which apps show until
|
||||
then) and, 30 days later (`gitHubAccountsKeptAfterEnd`, so a date set wrong can be put
|
||||
right first), `forgetGitHubAccounts` deletes every GitHub ID on the register, first
|
||||
deleting the accounts that sign in with GitHub alone (no address, never a password),
|
||||
each on its own (one failing is tried again the next day; one deleted whose host step
|
||||
after failed counts as deleted), the members told once after all of them. `password.set` adds an
|
||||
address without GitHub only for someone anonymous where the session lives
|
||||
(`anonymousHere`, which a member tells the register).
|
||||
after failed counts as deleted), the members told once after all of them.
|
||||
|
||||
## Taking it into a host
|
||||
|
||||
|
|
@ -455,7 +451,7 @@ Built on the three parts above, on the register (members forward every step thro
|
|||
after any literal routes of its own.
|
||||
|
||||
Then schedule the pruning in a future call of the host's own: daily
|
||||
`pruneAbandonedAnonymousPeople`, `forgetGitHubAccounts` (the register's),
|
||||
`pruneAnonymousPeople`, `forgetGitHubAccounts` (the register's),
|
||||
`forgetLapsedResets` and `forgetPeopleTheRegisterForgot` (a member's), hourly
|
||||
`forgetOldSignUpAttempts`, `forgetExpiredEmailCodes`,
|
||||
`forgetUnconfirmedAuthenticators`, `forgetUnfinishedSimpleLogins` (the
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:halkyon_base_server/src/auth/auth_config.dart';
|
||||
import 'package:halkyon_base_server/src/auth/scopes.dart';
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart';
|
||||
import 'package:halkyon_base_server/src/halkyon_base.dart';
|
||||
|
|
@ -128,7 +127,8 @@ Future<void> deletePerson(
|
|||
where: (t) => t.authUserId.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
// Their GitHub id and anonymous account go with them (ON DELETE CASCADE).
|
||||
// Their GitHub ID and SimpleLogin identity go with them, and an
|
||||
// anonymous account left from before (ON DELETE CASCADE).
|
||||
// Gone already on a member the register told first (ADR 0016).
|
||||
// DELETE FROM "serverpod_auth_core_user" WHERE "id" = $1
|
||||
if (person != null) {
|
||||
|
|
@ -148,50 +148,48 @@ Future<void> deletePerson(
|
|||
}
|
||||
}
|
||||
|
||||
/// Deletes the anonymous people nobody can return to, at most [limit] per
|
||||
/// call, and returns how many: created before and unused since 90 days
|
||||
/// before [now]. Their sessions have expired by then (the inactivity
|
||||
/// timeout, `auth_config.dart`), and an anonymous account can't sign in
|
||||
/// again. Identified people are never pruned. Each goes through
|
||||
/// [deletePerson], so the host's data of theirs goes too.
|
||||
Future<int> pruneAbandonedAnonymousPeople(
|
||||
Session session, {
|
||||
required DateTime now,
|
||||
int limit = 500,
|
||||
}) async {
|
||||
// The columns hold UTC as timestamp without time zone.
|
||||
final cutoff = now
|
||||
.toUtc()
|
||||
.subtract(const Duration(days: 90))
|
||||
.toIso8601String()
|
||||
.replaceFirst('Z', '');
|
||||
final abandoned = await session.db.unsafeQuery(
|
||||
/// Deletes the anonymous accounts left from before every account had a way
|
||||
/// in (ADR 0020, D25), at most [limit] per call, and returns how many: none
|
||||
/// is made any more, an app deletes its own as it starts, and none can sign
|
||||
/// in again once its session is gone, so the rest are a device's that never
|
||||
/// started a newer app. Whatever their age: the app no longer serves them
|
||||
/// (`minimumAppBuild`). Someone `identified` with an anonymous account's
|
||||
/// row left is never pruned. Each goes through [deletePerson], so the
|
||||
/// host's data of theirs goes too.
|
||||
Future<int> pruneAnonymousPeople(Session session, {int limit = 500}) async {
|
||||
// "scopeNames" holds the person's scopes as a JSON array of names.
|
||||
final anonymous = await session.db.unsafeQuery(
|
||||
'SELECT a."authUserId" FROM "serverpod_auth_idp_anonymous_account" a '
|
||||
'WHERE a."createdAt" < @cutoff::timestamp '
|
||||
'AND NOT EXISTS (SELECT 1 FROM "serverpod_auth_core_session" s '
|
||||
'WHERE s."authUserId" = a."authUserId" '
|
||||
'AND s."lastUsedAt" >= @cutoff::timestamp) '
|
||||
'JOIN "serverpod_auth_core_user" u ON u."id" = a."authUserId" '
|
||||
'WHERE NOT EXISTS (SELECT 1 FROM json_array_elements_text(u."scopeNames") '
|
||||
'AS scope WHERE scope = @identified) '
|
||||
'ORDER BY a."createdAt" LIMIT @limit',
|
||||
parameters: QueryParameters.named({'cutoff': cutoff, 'limit': limit}),
|
||||
parameters: QueryParameters.named({
|
||||
'identified': identified.name,
|
||||
'limit': limit,
|
||||
}),
|
||||
);
|
||||
for (final row in abandoned) {
|
||||
for (final row in anonymous) {
|
||||
await deletePerson(session, UuidValue.withValidation(row.first.toString()));
|
||||
}
|
||||
return abandoned.length;
|
||||
return anonymous.length;
|
||||
}
|
||||
|
||||
/// Deletes Serverpod's notes of sign-up attempts that are older than the
|
||||
/// rate limit looks back ([anonymousSignUpLimit]) at [now], and returns how
|
||||
/// many: each holds the IP address it came from, which the instance keeps no
|
||||
/// longer than the limit needs it. Serverpod itself deletes them only for its
|
||||
/// own email sign-in, which Halkyon servers don't use. Email sign-in's and
|
||||
/// password sign-in's notes go too: none of their limits looks back
|
||||
/// further.
|
||||
/// How far back the limits counting in Serverpod's notes of attempts look
|
||||
/// (`RateLimitedRequestAttempt`): email sign-in's, password sign-in's,
|
||||
/// SimpleLogin's and the handle checks' all count an hour.
|
||||
const _attemptsLookBack = Duration(hours: 1);
|
||||
|
||||
/// Deletes Serverpod's notes of attempts that are older than the limits
|
||||
/// look back ([_attemptsLookBack]) at [now], and returns how many: each
|
||||
/// holds the IP address it came from, which the instance keeps no longer
|
||||
/// than the limit needs it. Serverpod itself deletes them only for its own
|
||||
/// email sign-in, which Halkyon servers don't use.
|
||||
Future<int> forgetOldSignUpAttempts(
|
||||
Session session, {
|
||||
required DateTime now,
|
||||
}) async {
|
||||
final cutoff = now.toUtc().subtract(anonymousSignUpLimit.timeframe);
|
||||
final cutoff = now.toUtc().subtract(_attemptsLookBack);
|
||||
// DELETE FROM "serverpod_auth_idp_rate_limited_request_attempt"
|
||||
// WHERE "attemptedAt" < $1
|
||||
final forgotten = await RateLimitedRequestAttempt.db.deleteWhere(
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ class AccountEndpoint extends Endpoint {
|
|||
bool get requireLogin => true;
|
||||
|
||||
/// Deletes the caller with everything they have (see [deletePerson]). The
|
||||
/// app also calls it when an anonymous account is given up (decided
|
||||
/// 2026-09-30). Their other devices learn at once that their sessions
|
||||
/// app also calls it for an anonymous account left from before, as it
|
||||
/// starts (ADR 0020, D25). Their other devices learn at once that their sessions
|
||||
/// ended. An account deletes everywhere (ADR 0016): on a member, the
|
||||
/// register deletes it and tells every member, this one deleting its own
|
||||
/// data at once; an anonymous person is this service's alone. An account
|
||||
|
|
@ -83,7 +83,7 @@ class AccountEndpoint extends Endpoint {
|
|||
/// ([RegisterUnavailableException]), a person still anonymous here
|
||||
/// neither (the short reading's L2): the register may have linked them
|
||||
/// while their becoming identified here failed. An anonymous account
|
||||
/// given up meanwhile stays to the pruning (90 days unused).
|
||||
/// given up meanwhile stays to the nightly pruning.
|
||||
static Future<bool> _registersAccount(
|
||||
Session session,
|
||||
UuidValue person,
|
||||
|
|
@ -91,8 +91,8 @@ class AccountEndpoint extends Endpoint {
|
|||
session.serverpod,
|
||||
).knownAccounts(session, {person})).contains(person);
|
||||
|
||||
/// How the caller signs in besides their sessions: with GitHub, with an
|
||||
/// email address, both, or neither (anonymous).
|
||||
/// How the caller signs in besides their sessions ([SignInWays]): with
|
||||
/// SimpleLogin, a password, an email address or GitHub.
|
||||
Future<SignInWays> signInWays(Session session) => AccountRegister.of(
|
||||
session.serverpod,
|
||||
).signInWaysOf(session, session.authenticated!.authUserId);
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
import 'package:halkyon_base_server/src/auth/auth_config.dart';
|
||||
import 'package:halkyon_base_server/src/common/network.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
import 'package:serverpod_auth_idp_server/providers/anonymous.dart';
|
||||
|
||||
/// Signs people in anonymously; the app calls it on first launch.
|
||||
class AnonymousIdpEndpoint extends AnonymousIdpBaseEndpoint {
|
||||
/// Creates a new anonymous account and returns its session: at most 30 an
|
||||
/// hour from one network (`anonymousSignUpLimit`), counted by the
|
||||
/// visitor's address (`networkOf`) rather than as Serverpod's own limit
|
||||
/// counts them, by the connection's, which behind a proxy is the proxy's
|
||||
/// for everyone.
|
||||
@override
|
||||
Future<AuthSuccess> login(Session session, {String? token}) async {
|
||||
if (!await _accountsPerNetwork.tryRecordAttempt(
|
||||
session,
|
||||
key: networkOf(session),
|
||||
)) {
|
||||
throw AnonymousAccountBlockedException(
|
||||
reason: AnonymousAccountBlockedExceptionReason.tooManyAttempts,
|
||||
);
|
||||
}
|
||||
return await super.login(session, token: token);
|
||||
}
|
||||
}
|
||||
|
||||
final _accountsPerNetwork = DatabaseRateLimiter(
|
||||
RateLimiterConfig(
|
||||
domain: 'anonymous',
|
||||
source: 'accounts_per_network',
|
||||
maxAttempts: anonymousSignUpLimit.maxAttempts,
|
||||
timeframe: anonymousSignUpLimit.timeframe,
|
||||
),
|
||||
);
|
||||
|
|
@ -2,39 +2,22 @@ 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_auth_idp_server/core.dart';
|
||||
import 'package:serverpod_auth_idp_server/providers/anonymous.dart';
|
||||
import 'package:serverpod_auth_idp_server/providers/github.dart';
|
||||
|
||||
// How people authenticate. The server and its tests use this same
|
||||
// configuration, so the tests check what production runs.
|
||||
|
||||
/// Server-side sessions: revocable immediately, scopes live on the session
|
||||
/// row. No absolute lifetime (an anonymous session cannot be restored). The
|
||||
/// inactivity timeout matches the pruning policy for abandoned accounts.
|
||||
/// row. No absolute lifetime, only 90 days unused.
|
||||
final List<TokenManagerBuilder> tokenManagerBuilders = [
|
||||
ServerSideSessionsConfigFromPasswords(
|
||||
defaultSessionInactivityTimeout: const Duration(days: 90),
|
||||
),
|
||||
];
|
||||
|
||||
/// How many new anonymous accounts one network (the visitor's IP address,
|
||||
/// `networkOf`) may make, and in what time: counted by the anonymous
|
||||
/// endpoint itself (`AnonymousIdpEndpoint`), as Serverpod's own limit counts
|
||||
/// by the connection's address, behind a proxy the proxy's. Each attempt is
|
||||
/// noted with that address, which the instance forgets once the time is up
|
||||
/// (`forgetOldSignUpAttempts`).
|
||||
const anonymousSignUpLimit = RateLimit(
|
||||
maxAttempts: 30,
|
||||
timeframe: Duration(hours: 1),
|
||||
);
|
||||
|
||||
/// The ways people sign in on [pod].
|
||||
///
|
||||
/// Anonymous accounts are the trial mode for solo use. At most 30 new ones per
|
||||
/// IP address and hour (Serverpod's default is 100): a school class behind one
|
||||
/// address can still start together, while one abuser gets at most 720 a day.
|
||||
/// [anonymousSignUpLimit] counts them; Serverpod's own limit is off, as it
|
||||
/// would count every visitor behind a proxy as one.
|
||||
/// The ways people sign in on [pod], besides a password and codes by mail,
|
||||
/// which are the module's own (`passwords.dart`, `email.dart`). No anonymous
|
||||
/// accounts (ADR 0020, D25): every account has a way in.
|
||||
///
|
||||
/// GitHub only where the operator registered a GitHub OAuth app (see
|
||||
/// [gitHubSignInAvailable]); its helpers serve our own slim login
|
||||
|
|
@ -44,7 +27,6 @@ const anonymousSignUpLimit = RateLimit(
|
|||
/// 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) => [
|
||||
const AnonymousIdpConfig(perIpAddressRateLimit: null),
|
||||
if (gitHubSignInAvailable(pod)) GitHubIdpConfigFromPasswords(),
|
||||
?simpleLoginIdpConfig(pod),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -2,13 +2,9 @@ import 'dart:io';
|
|||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:halkyon_base_server/src/auth/account.dart';
|
||||
import 'package:halkyon_base_server/src/auth/identified.dart';
|
||||
import 'package:halkyon_base_server/src/auth/membership.dart';
|
||||
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/register/deletion_notices.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_auth_idp_server/core.dart';
|
||||
import 'package:serverpod_auth_idp_server/providers/github.dart';
|
||||
|
|
@ -198,82 +194,11 @@ Future<String> verifiedGitHubUserId(
|
|||
return details.userIdentifier;
|
||||
}
|
||||
|
||||
/// Links the GitHub account [gitHubUserId] to the person [authUserId], in one
|
||||
/// transaction: they become `identified` (on their existing sessions too) and
|
||||
/// lose their anonymous identity, since they now sign in with GitHub. The
|
||||
/// person stays the same, so everything they have stays where it is; a
|
||||
/// member's person becomes an account of the register under their ID
|
||||
/// (ADR 0016).
|
||||
///
|
||||
/// Linking the account the person already has changes nothing. Refused, with
|
||||
/// nothing changed, when the account belongs to someone else or the person
|
||||
/// has another GitHub account, and wherever people sign in with a password
|
||||
/// or SimpleLogin (ADR 0017, ADR 0020: GitHub is deprecated,
|
||||
/// [GitHubSignInRefusal.deprecated])
|
||||
/// ([GitHubSignInRefusedException]). An account someone else has is said to
|
||||
/// 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
|
||||
/// who have one.
|
||||
Future<void> linkGitHubAccount(
|
||||
Session session, {
|
||||
required UuidValue authUserId,
|
||||
required String gitHubUserId,
|
||||
}) => session.db.transaction((transaction) async {
|
||||
// SELECT * FROM "serverpod_auth_idp_github_account"
|
||||
// WHERE "userIdentifier" = $1 OR "authUserId" = $2
|
||||
final accounts = await GitHubAccount.db.find(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.userIdentifier.equals(gitHubUserId) | t.authUserId.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
if (accounts.any(
|
||||
(account) =>
|
||||
account.userIdentifier == gitHubUserId &&
|
||||
account.authUserId == authUserId,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
if (accounts.any((account) => account.userIdentifier == gitHubUserId)) {
|
||||
throw GitHubSignInRefusedException(
|
||||
reason: GitHubSignInRefusal.takenByAnotherPerson,
|
||||
);
|
||||
}
|
||||
if (passwordSignInAvailable(session.serverpod) ||
|
||||
simpleLoginAvailable(session.serverpod)) {
|
||||
throw GitHubSignInRefusedException(
|
||||
reason: GitHubSignInRefusal.deprecated,
|
||||
);
|
||||
}
|
||||
if (accounts.isNotEmpty) {
|
||||
throw GitHubSignInRefusedException(
|
||||
reason: GitHubSignInRefusal.otherGitHubAccountLinked,
|
||||
);
|
||||
}
|
||||
|
||||
await ensurePerson(session, authUserId, transaction: transaction);
|
||||
// INSERT INTO "serverpod_auth_idp_github_account"
|
||||
// ("userIdentifier", "authUserId", "created") VALUES ($1, $2, $3)
|
||||
await GitHubAccount.db.insertRow(
|
||||
session,
|
||||
GitHubAccount(userIdentifier: gitHubUserId, authUserId: authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
await grantIdentified(session, authUserId, transaction: transaction);
|
||||
// DELETE FROM "serverpod_auth_idp_anonymous_account" WHERE "authUserId" = $1
|
||||
await AnonymousAccount.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
});
|
||||
|
||||
/// The person who has the GitHub account [gitHubUserId]; null for nobody.
|
||||
/// A session for them is the caller's to issue (`issueSession`), when it
|
||||
/// switches to them on another device.
|
||||
///
|
||||
/// Nobody is created here: people start anonymous and link GitHub later
|
||||
/// ([linkGitHubAccount]).
|
||||
/// Nobody is created here: no account gets GitHub any more (ADR 0017, D2).
|
||||
Future<UuidValue?> accountWithGitHub(
|
||||
Session session, {
|
||||
required String gitHubUserId,
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
import 'package:halkyon_base_server/src/auth/github.dart';
|
||||
import 'package:halkyon_base_server/src/auth/membership.dart';
|
||||
import 'package:halkyon_base_server/src/register/account_register.dart';
|
||||
import 'package:halkyon_base_server/src/register/people.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
|
||||
/// Links a GitHub account to the signed-in person: the lossless upgrade from
|
||||
/// an anonymous account. The person and everything they have stay
|
||||
/// where they are; only the way they sign in changes. The instance's
|
||||
/// register keeps the GitHub ID (ADR 0016).
|
||||
class GitHubUpgradeEndpoint extends Endpoint {
|
||||
@override
|
||||
bool get requireLogin => true;
|
||||
|
||||
/// Links the GitHub account that [code] proves to the caller; refusals as in
|
||||
/// [linkGitHubAccount].
|
||||
Future<void> upgrade(
|
||||
Session session, {
|
||||
required String code,
|
||||
required String codeVerifier,
|
||||
required String redirectUri,
|
||||
}) async {
|
||||
final gitHubUserId = await verifiedGitHubUserId(
|
||||
session,
|
||||
code: code,
|
||||
codeVerifier: codeVerifier,
|
||||
redirectUri: redirectUri,
|
||||
);
|
||||
final person = session.authenticated!.authUserId;
|
||||
await AccountRegister.of(
|
||||
session.serverpod,
|
||||
).linkGitHub(session, person: person, gitHubUserId: gitHubUserId);
|
||||
if (isMember(session.serverpod)) {
|
||||
await becomeIdentified(session, person);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import 'package:serverpod/serverpod.dart';
|
||||
|
||||
/// People with an identity, such as GitHub, rather than only an anonymous
|
||||
/// account. Granted when they link one (see `grantIdentified`).
|
||||
/// People with a way in, every account since anonymous ones went (ADR 0020,
|
||||
/// D25), unlike an anonymous account left from before. Granted as a way in
|
||||
/// becomes theirs (see `grantIdentified`).
|
||||
///
|
||||
/// Endpoints that involve other people or several devices require it through
|
||||
/// `requiredScopes`, never through checks of their own.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
### The ways a person signs in, besides an anonymous account's session.
|
||||
### The ways a person signs in, besides their sessions.
|
||||
class: SignInWays
|
||||
fields:
|
||||
gitHub: bool
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import 'dart:typed_data';
|
|||
import 'package:serverpod/serverpod.dart';
|
||||
|
||||
// The network a request came from, which the per-network rate limits count
|
||||
// by (new anonymous accounts, codes by mail, password sign-ins and proofs of
|
||||
// the recovery phrase). Behind a reverse proxy every connection comes from
|
||||
// by (codes by mail, password sign-ins, SimpleLogin's sign-ins, handle
|
||||
// checks and proofs of the recovery phrase). Behind a reverse proxy every connection comes from
|
||||
// the proxy, which would make each of those limits the whole instance's.
|
||||
// So for a connection from a proxy the operator trusts (`TRUSTED_PROXIES`;
|
||||
// deploy/compose.yaml names Caddy), the visitor is the last address of
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
/// The ways a person signs in, besides an anonymous account's session.
|
||||
/// The ways a person signs in, besides their sessions.
|
||||
abstract class SignInWays
|
||||
implements _is.SerializableModel, _is.ProtocolSerialization {
|
||||
SignInWays._({
|
||||
|
|
|
|||
|
|
@ -40,10 +40,8 @@ import 'package:serverpod_auth_core_server/serverpod_auth_core_server.dart'
|
|||
import 'package:serverpod_auth_idp_server/serverpod_auth_idp_server.dart'
|
||||
as _iais;
|
||||
import '../auth/account_endpoint.dart' as _ii6ycf6v;
|
||||
import '../auth/anonymous_idp_endpoint.dart' as _in0zita6;
|
||||
import '../auth/email_code_endpoint.dart' as _in3cimlf;
|
||||
import '../auth/github_sign_in_endpoint.dart' as _iasw79m5;
|
||||
import '../auth/github_upgrade_endpoint.dart' as _ivnugbf2;
|
||||
import '../instance/instance_endpoint.dart' as _i3g8bfc2;
|
||||
import '../keys/keys_endpoint.dart' as _icowoccn;
|
||||
import '../password/account_recovery_endpoint.dart' as _ivnbjhab;
|
||||
|
|
@ -63,12 +61,6 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
'account',
|
||||
'halkyon_base',
|
||||
),
|
||||
'anonymousIdp': _in0zita6.AnonymousIdpEndpoint()
|
||||
..initialize(
|
||||
server,
|
||||
'anonymousIdp',
|
||||
'halkyon_base',
|
||||
),
|
||||
'emailCode': _in3cimlf.EmailCodeEndpoint()
|
||||
..initialize(
|
||||
server,
|
||||
|
|
@ -81,12 +73,6 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
'gitHubSignIn',
|
||||
'halkyon_base',
|
||||
),
|
||||
'gitHubUpgrade': _ivnugbf2.GitHubUpgradeEndpoint()
|
||||
..initialize(
|
||||
server,
|
||||
'gitHubUpgrade',
|
||||
'halkyon_base',
|
||||
),
|
||||
'instance': _i3g8bfc2.InstanceEndpoint()
|
||||
..initialize(
|
||||
server,
|
||||
|
|
@ -183,32 +169,6 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
),
|
||||
},
|
||||
);
|
||||
connectors['anonymousIdp'] = _is.EndpointConnector(
|
||||
name: 'anonymousIdp',
|
||||
endpoint: endpoints['anonymousIdp']!,
|
||||
methodConnectors: {
|
||||
'login': _is.MethodConnector(
|
||||
name: 'login',
|
||||
params: {
|
||||
'token': _is.ParameterDescription(
|
||||
name: 'token',
|
||||
type: _is.getType<String?>(),
|
||||
nullable: true,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['anonymousIdp'] as _in0zita6.AnonymousIdpEndpoint)
|
||||
.login(
|
||||
session,
|
||||
token: params['token'],
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
connectors['emailCode'] = _is.EndpointConnector(
|
||||
name: 'emailCode',
|
||||
endpoint: endpoints['emailCode']!,
|
||||
|
|
@ -284,45 +244,6 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
),
|
||||
},
|
||||
);
|
||||
connectors['gitHubUpgrade'] = _is.EndpointConnector(
|
||||
name: 'gitHubUpgrade',
|
||||
endpoint: endpoints['gitHubUpgrade']!,
|
||||
methodConnectors: {
|
||||
'upgrade': _is.MethodConnector(
|
||||
name: 'upgrade',
|
||||
params: {
|
||||
'code': _is.ParameterDescription(
|
||||
name: 'code',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'codeVerifier': _is.ParameterDescription(
|
||||
name: 'codeVerifier',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'redirectUri': _is.ParameterDescription(
|
||||
name: 'redirectUri',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['gitHubUpgrade']
|
||||
as _ivnugbf2.GitHubUpgradeEndpoint)
|
||||
.upgrade(
|
||||
session,
|
||||
code: params['code'],
|
||||
codeVerifier: params['codeVerifier'],
|
||||
redirectUri: params['redirectUri'],
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
connectors['instance'] = _is.EndpointConnector(
|
||||
name: 'instance',
|
||||
endpoint: endpoints['instance']!,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,10 @@
|
|||
account:
|
||||
- delete:
|
||||
- signInWays:
|
||||
anonymousIdp:
|
||||
- login:
|
||||
emailCode:
|
||||
- sendCode:
|
||||
gitHubSignIn:
|
||||
- signIn:
|
||||
gitHubUpgrade:
|
||||
- upgrade:
|
||||
instance:
|
||||
- basics:
|
||||
keys:
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ Future<DeviceKeys> unlockKeys(
|
|||
}
|
||||
|
||||
/// Gives [person], an account without keys that SimpleLogin was added to
|
||||
/// (one of before: anonymous, or signing in with GitHub or a code), the
|
||||
/// (one of before that signs in with GitHub or a code), the
|
||||
/// keys [setup] describes, and [handle] where it has none (ADR 0020, 6 and
|
||||
/// D5), with their SimpleLogin sign-in under [attempt] of the last ten
|
||||
/// minutes, taken (D20). Nothing else proves the owner: the account has no
|
||||
|
|
|
|||
|
|
@ -12,9 +12,8 @@ import 'package:halkyon_base_server/src/register/people.dart';
|
|||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
|
||||
/// Giving the signed-in person a password (ADR 0017): the way an anonymous
|
||||
/// person becomes identified, keeping everything they have, and the way
|
||||
/// someone who signs in with a code by mail or GitHub moves to a password.
|
||||
/// Giving the signed-in person a password (ADR 0017): the way someone who
|
||||
/// signs in with a code by mail or GitHub moves to a password.
|
||||
/// The instance's register keeps it (ADR 0016).
|
||||
class PasswordEndpoint extends Endpoint {
|
||||
@override
|
||||
|
|
@ -42,14 +41,6 @@ class PasswordEndpoint extends Endpoint {
|
|||
request: request,
|
||||
code: code,
|
||||
freshGitHubSignIn: await _signedInWithGitHubJustNow(session),
|
||||
// SELECT count(*) FROM "serverpod_auth_idp_anonymous_account"
|
||||
// WHERE "authUserId" = $1
|
||||
anonymousHere:
|
||||
await AnonymousAccount.db.count(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(person),
|
||||
) >
|
||||
0,
|
||||
keepSession: switch (session.authenticated!.authId) {
|
||||
final id when Uuid.isValidUUID(fromString: id) =>
|
||||
UuidValue.withValidation(id),
|
||||
|
|
|
|||
|
|
@ -412,9 +412,8 @@ const gitHubSignInFreshFor = Duration(minutes: 10);
|
|||
|
||||
/// Gives [person] a password ([setup], ADR 0017). A password needs an
|
||||
/// address, for resets: the person's own, or the one [code] proves for
|
||||
/// [request] (mailed with `sendSignInCode`), which becomes theirs as email
|
||||
/// sign-in makes it (they become `identified` and stop being anonymous,
|
||||
/// keeping their ID and everything they have) in the same transaction. The
|
||||
/// [request] (mailed with `sendSignInCode`), which becomes theirs in the
|
||||
/// same transaction. The
|
||||
/// account signs in with the password only once an authenticator is
|
||||
/// confirmed; until then a new setup replaces it, but never once one was
|
||||
/// ([hadConfirmedAuthenticator]: after a reset, a new password comes through
|
||||
|
|
@ -426,12 +425,11 @@ const gitHubSignInFreshFor = Duration(minutes: 10);
|
|||
/// ([EmailCodePurpose.firstPassword]); one that signs in with GitHub alone
|
||||
/// by a GitHub sign-in of the last [gitHubSignInFreshFor]
|
||||
/// ([freshGitHubSignIn], which the server holding the session tells), with
|
||||
/// the address it adds proved as an anonymous person proves theirs
|
||||
/// ([EmailCodePurpose.signUp]). Only someone anonymous where the session
|
||||
/// lives ([anonymousHere], which that server tells) adds an address so
|
||||
/// without either: anyone else has no way in left to prove (a GitHub ID
|
||||
/// forgotten at its end, the full reading's M1), and a session of theirs
|
||||
/// alone would make the account a thief's.
|
||||
/// the address it adds proved as a sign-up proves one
|
||||
/// ([EmailCodePurpose.signUp]). Anyone else has no way in left to prove (a
|
||||
/// GitHub ID forgotten at its end, the full reading's M1, or an anonymous
|
||||
/// account left from before, ADR 0020, D25), and a session of theirs alone
|
||||
/// would make the account a thief's.
|
||||
///
|
||||
/// Refused ([PasswordRefusedException]) where the instance has no password
|
||||
/// sign-in, for a setup out of range, without an address, for an address
|
||||
|
|
@ -439,8 +437,8 @@ const gitHubSignInFreshFor = Duration(minutes: 10);
|
|||
/// confirmed authenticator ([PasswordRefusal.passwordExists]), without the
|
||||
/// code its own address takes ([PasswordRefusal.wrongCredentials]), and
|
||||
/// for GitHub without a recent sign-in ([PasswordRefusal.signInAgain],
|
||||
/// before any code is used), and for someone neither anonymous nor with a
|
||||
/// way in ([PasswordRefusal.wrongCredentials], before any code is used); a
|
||||
/// before any code is used), and for someone without a way in
|
||||
/// ([PasswordRefusal.wrongCredentials], before any code is used); a
|
||||
/// code's refusals as `proveAddressCode`'s, and
|
||||
/// another address than the account's as email sign-in refuses it
|
||||
/// ([EmailSignInRefusedException]). Nothing changes then.
|
||||
|
|
@ -451,7 +449,6 @@ Future<void> setPassword(
|
|||
UuidValue? request,
|
||||
String? code,
|
||||
bool freshGitHubSignIn = false,
|
||||
bool anonymousHere = false,
|
||||
UuidValue? keepSession,
|
||||
}) async {
|
||||
requirePasswordKeys(session.serverpod);
|
||||
|
|
@ -491,7 +488,7 @@ Future<void> setPassword(
|
|||
if (gitHub && !freshGitHubSignIn) {
|
||||
throw refusedFor(PasswordRefusal.signInAgain);
|
||||
}
|
||||
if (!gitHub && !anonymousHere) {
|
||||
if (!gitHub) {
|
||||
throw refusedFor(PasswordRefusal.wrongCredentials);
|
||||
}
|
||||
proven = request != null && code != null
|
||||
|
|
@ -543,13 +540,6 @@ Future<void> setPassword(
|
|||
transaction: transaction,
|
||||
);
|
||||
await grantIdentified(session, person, transaction: transaction);
|
||||
// DELETE FROM "serverpod_auth_idp_anonymous_account"
|
||||
// WHERE "authUserId" = $1
|
||||
await AnonymousAccount.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(person),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// No address proved: none at all, or one that came meanwhile, whose
|
||||
|
|
@ -683,16 +673,9 @@ Future<UuidValue> signUpWithCode(
|
|||
0) {
|
||||
throw refusedFor(PasswordRefusal.passwordExists);
|
||||
}
|
||||
// An address's person is identified already, and no longer
|
||||
// anonymous; as email sign-in left them.
|
||||
// An address's person is identified already, as email sign-in
|
||||
// left them.
|
||||
await grantIdentified(session, person, transaction: transaction);
|
||||
// DELETE FROM "serverpod_auth_idp_anonymous_account"
|
||||
// WHERE "authUserId" = $1
|
||||
await AnonymousAccount.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(person),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
// INSERT INTO "account_password" (…) VALUES (…)
|
||||
await AccountPassword.db.insertRow(
|
||||
|
|
|
|||
|
|
@ -126,14 +126,6 @@ abstract interface class AccountRegister {
|
|||
required String network,
|
||||
});
|
||||
|
||||
/// Links the GitHub account [gitHubUserId] to [person] (an account of the
|
||||
/// register, under their ID). Refusals as `linkGitHubAccount`'s.
|
||||
Future<void> linkGitHub(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required String gitHubUserId,
|
||||
});
|
||||
|
||||
/// The account that signs in with the GitHub account [gitHubUserId]; null
|
||||
/// for none.
|
||||
Future<UuidValue?> accountWithGitHub(Session session, String gitHubUserId);
|
||||
|
|
@ -298,9 +290,7 @@ abstract interface class AccountRegister {
|
|||
/// Gives [person] the password [setup] describes, with the address [code]
|
||||
/// proves for [request] (their own where they have one), and for one who
|
||||
/// signs in with GitHub alone a GitHub sign-in of theirs made recently
|
||||
/// ([freshGitHubSignIn], of the session asking); someone with neither
|
||||
/// only while anonymous where the session lives ([anonymousHere]).
|
||||
/// Replacing a setup never finished ends every other session of the
|
||||
/// ([freshGitHubSignIn], of the session asking). Replacing a setup never finished ends every other session of the
|
||||
/// account but [keepSession]. Refusals as `setPassword`'s.
|
||||
Future<void> setPassword(
|
||||
Session session, {
|
||||
|
|
@ -309,7 +299,6 @@ abstract interface class AccountRegister {
|
|||
UuidValue? request,
|
||||
String? code,
|
||||
bool freshGitHubSignIn = false,
|
||||
bool anonymousHere = false,
|
||||
UuidValue? keepSession,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -182,13 +182,6 @@ Future<Map<String, Object?>?> _answer(
|
|||
network: arguments.string('network'),
|
||||
);
|
||||
return {'request': request.uuid};
|
||||
case RegisterOperation.linkGitHub:
|
||||
await register.linkGitHub(
|
||||
session,
|
||||
person: arguments.id('person'),
|
||||
gitHubUserId: arguments.string('gitHubUserId'),
|
||||
);
|
||||
return const {};
|
||||
case RegisterOperation.accountWithGitHub:
|
||||
final account = await register.accountWithGitHub(
|
||||
session,
|
||||
|
|
@ -347,11 +340,8 @@ Future<Map<String, Object?>?> _answer(
|
|||
setup: arguments.model('setup', PasswordSetup.fromJson),
|
||||
request: arguments.optionalId('request'),
|
||||
code: arguments.optionalString('code'),
|
||||
// The member tells it of its own session (the fourth reading) and
|
||||
// person (the full reading's M1): the register keeps no anonymous
|
||||
// person of a member's.
|
||||
// The member tells it of its own session (the fourth reading).
|
||||
freshGitHubSignIn: arguments.flag('freshGitHubSignIn'),
|
||||
anonymousHere: arguments.flag('anonymousHere'),
|
||||
keepSession: arguments.optionalId('keepSession'),
|
||||
);
|
||||
return const {};
|
||||
|
|
|
|||
|
|
@ -366,17 +366,6 @@ final class LocalRegister implements AccountRegister {
|
|||
network: network,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> linkGitHub(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required String gitHubUserId,
|
||||
}) => github.linkGitHubAccount(
|
||||
session,
|
||||
authUserId: person,
|
||||
gitHubUserId: gitHubUserId,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<UuidValue?> accountWithGitHub(Session session, String gitHubUserId) =>
|
||||
github.accountWithGitHub(session, gitHubUserId: gitHubUserId);
|
||||
|
|
@ -615,7 +604,6 @@ final class LocalRegister implements AccountRegister {
|
|||
UuidValue? request,
|
||||
String? code,
|
||||
bool freshGitHubSignIn = false,
|
||||
bool anonymousHere = false,
|
||||
UuidValue? keepSession,
|
||||
}) => passwords.setPassword(
|
||||
session,
|
||||
|
|
@ -624,7 +612,6 @@ final class LocalRegister implements AccountRegister {
|
|||
request: request,
|
||||
code: code,
|
||||
freshGitHubSignIn: freshGitHubSignIn,
|
||||
anonymousHere: anonymousHere,
|
||||
keepSession: keepSession,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import 'package:serverpod_auth_idp_server/core.dart';
|
|||
|
||||
/// Makes [account] a person of this server's database, under that ID, unless
|
||||
/// it is one already, with [scopes] (ADR 0016): on the register, a member's
|
||||
/// anonymous person becoming identified keeps their ID; on a member, the
|
||||
/// register's account signs in here. Serverpod makes people with an ID of its
|
||||
/// person adding a way in keeps their ID; on a member, the register's
|
||||
/// account signs in here. Serverpod makes people with an ID of its
|
||||
/// own choosing, so the row is written here with the one given.
|
||||
Future<void> ensurePerson(
|
||||
Session session,
|
||||
|
|
@ -104,16 +104,6 @@ Future<AuthSuccess> issueSessionAfter(
|
|||
|
||||
/// What [person] becomes here once a way in is theirs, as linking makes them
|
||||
/// on the register in the same transaction: `identified` (on their sessions
|
||||
/// too), and no longer anonymous. A member does it after the register linked
|
||||
/// the way in.
|
||||
/// too). A member does it after the register linked the way in.
|
||||
Future<void> becomeIdentified(Session session, UuidValue person) =>
|
||||
session.db.transaction((transaction) async {
|
||||
await grantIdentified(session, person, transaction: transaction);
|
||||
// DELETE FROM "serverpod_auth_idp_anonymous_account"
|
||||
// WHERE "authUserId" = $1
|
||||
await AnonymousAccount.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(person),
|
||||
transaction: transaction,
|
||||
);
|
||||
});
|
||||
grantIdentified(session, person);
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue