fix(auth): a reset waits while its owner is held out, and devices show it
The full reading's H1: an owner could be kept from calling a reset off, and the reset then went through. Whoever knew the password and had the mailbox asked an authenticators' reset and kept the codes waiting, so the owner's sign-ins were refused until it took effect; whoever had the mailbox filled the address's proofs ceiling from three networks and completed an account reset. Now every hold-out of the owner after a reset was asked (a wait that begins, an attempt a wait refuses, a sign-in or proof refused by the address's ceiling) moves the account's resets not carried out to take effect no sooner than 72 hours after it ends (pushOutResets). A reset carried out (the password alone signing in until an authenticator is confirmed) is never pushed, nor a lapsed one brought back. Signed-in devices are told too: SignInWays carries when a waiting reset of each kind takes effect, the account section shows them with Call it off, which takes the owner's proof (password and code; password. callOffResets, forwarded by members) and calls every reset off, as no wait holds a signed-in owner's proof back. Texts in English and German. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
4d1cd2045c
commit
9d0d037c86
15 changed files with 1022 additions and 18 deletions
|
|
@ -21,7 +21,7 @@ export 'src/halkyon_base.dart';
|
|||
export 'src/instance/data_location.dart';
|
||||
export 'src/instance/source_code.dart';
|
||||
export 'src/password/account_resets.dart'
|
||||
show forgetLapsedResets, resetLapsesAfter;
|
||||
show forgetLapsedResets, resetDelay, resetLapsesAfter;
|
||||
export 'src/password/authenticators.dart'
|
||||
show
|
||||
forgetUnconfirmedAuthenticators,
|
||||
|
|
@ -36,7 +36,6 @@ export 'src/password/passwords.dart'
|
|||
newPasswordCosts,
|
||||
passwordGroup,
|
||||
passwordMethod,
|
||||
resetDelay,
|
||||
signInChallengeLifetime,
|
||||
signsInWithPassword,
|
||||
wrappedKeysLength;
|
||||
|
|
|
|||
|
|
@ -19,3 +19,9 @@ fields:
|
|||
### a row, the owner's warning until they dismiss it or set a new
|
||||
### password (ADR 0017, the fourth reading); null for none.
|
||||
sessionEndedForProofsAt: DateTime?
|
||||
### When a reset of the authenticators waiting (or due, not carried out)
|
||||
### takes effect, so a signed-in device shows it and its owner calls it
|
||||
### off with their proof (ADR 0017, the full reading's H1); null for none.
|
||||
authenticatorResetAt: DateTime?
|
||||
### When a reset of the account waiting (or due) takes effect, alike.
|
||||
accountResetAt: DateTime?
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ abstract class SignInWays
|
|||
bool? passwordOnly,
|
||||
bool? secondFactorWorks,
|
||||
this.sessionEndedForProofsAt,
|
||||
this.authenticatorResetAt,
|
||||
this.accountResetAt,
|
||||
}) : password = password ?? false,
|
||||
authenticator = authenticator ?? false,
|
||||
passwordOnly = passwordOnly ?? false,
|
||||
|
|
@ -36,6 +38,8 @@ abstract class SignInWays
|
|||
bool? passwordOnly,
|
||||
bool? secondFactorWorks,
|
||||
DateTime? sessionEndedForProofsAt,
|
||||
DateTime? authenticatorResetAt,
|
||||
DateTime? accountResetAt,
|
||||
}) = _SignInWaysImpl;
|
||||
|
||||
factory SignInWays.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
|
|
@ -62,6 +66,16 @@ abstract class SignInWays
|
|||
: _is.DateTimeJsonExtension.fromJson(
|
||||
jsonSerialization['sessionEndedForProofsAt'],
|
||||
),
|
||||
authenticatorResetAt: jsonSerialization['authenticatorResetAt'] == null
|
||||
? null
|
||||
: _is.DateTimeJsonExtension.fromJson(
|
||||
jsonSerialization['authenticatorResetAt'],
|
||||
),
|
||||
accountResetAt: jsonSerialization['accountResetAt'] == null
|
||||
? null
|
||||
: _is.DateTimeJsonExtension.fromJson(
|
||||
jsonSerialization['accountResetAt'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -90,6 +104,14 @@ abstract class SignInWays
|
|||
/// password (ADR 0017, the fourth reading); null for none.
|
||||
DateTime? sessionEndedForProofsAt;
|
||||
|
||||
/// When a reset of the authenticators waiting (or due, not carried out)
|
||||
/// takes effect, so a signed-in device shows it and its owner calls it
|
||||
/// off with their proof (ADR 0017, the full reading's H1); null for none.
|
||||
DateTime? authenticatorResetAt;
|
||||
|
||||
/// When a reset of the account waiting (or due) takes effect, alike.
|
||||
DateTime? accountResetAt;
|
||||
|
||||
/// Returns a shallow copy of this [SignInWays]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
|
|
@ -101,6 +123,8 @@ abstract class SignInWays
|
|||
bool? passwordOnly,
|
||||
bool? secondFactorWorks,
|
||||
DateTime? sessionEndedForProofsAt,
|
||||
DateTime? authenticatorResetAt,
|
||||
DateTime? accountResetAt,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
|
|
@ -114,6 +138,9 @@ abstract class SignInWays
|
|||
'secondFactorWorks': secondFactorWorks,
|
||||
if (sessionEndedForProofsAt != null)
|
||||
'sessionEndedForProofsAt': sessionEndedForProofsAt?.toJson(),
|
||||
if (authenticatorResetAt != null)
|
||||
'authenticatorResetAt': authenticatorResetAt?.toJson(),
|
||||
if (accountResetAt != null) 'accountResetAt': accountResetAt?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -129,6 +156,9 @@ abstract class SignInWays
|
|||
'secondFactorWorks': secondFactorWorks,
|
||||
if (sessionEndedForProofsAt != null)
|
||||
'sessionEndedForProofsAt': sessionEndedForProofsAt?.toJson(),
|
||||
if (authenticatorResetAt != null)
|
||||
'authenticatorResetAt': authenticatorResetAt?.toJson(),
|
||||
if (accountResetAt != null) 'accountResetAt': accountResetAt?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -149,6 +179,8 @@ class _SignInWaysImpl extends SignInWays {
|
|||
bool? passwordOnly,
|
||||
bool? secondFactorWorks,
|
||||
DateTime? sessionEndedForProofsAt,
|
||||
DateTime? authenticatorResetAt,
|
||||
DateTime? accountResetAt,
|
||||
}) : super._(
|
||||
gitHub: gitHub,
|
||||
email: email,
|
||||
|
|
@ -157,6 +189,8 @@ class _SignInWaysImpl extends SignInWays {
|
|||
passwordOnly: passwordOnly,
|
||||
secondFactorWorks: secondFactorWorks,
|
||||
sessionEndedForProofsAt: sessionEndedForProofsAt,
|
||||
authenticatorResetAt: authenticatorResetAt,
|
||||
accountResetAt: accountResetAt,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [SignInWays]
|
||||
|
|
@ -171,6 +205,8 @@ class _SignInWaysImpl extends SignInWays {
|
|||
bool? passwordOnly,
|
||||
bool? secondFactorWorks,
|
||||
Object? sessionEndedForProofsAt = _Undefined,
|
||||
Object? authenticatorResetAt = _Undefined,
|
||||
Object? accountResetAt = _Undefined,
|
||||
}) {
|
||||
return SignInWays(
|
||||
gitHub: gitHub ?? this.gitHub,
|
||||
|
|
@ -182,6 +218,12 @@ class _SignInWaysImpl extends SignInWays {
|
|||
sessionEndedForProofsAt: sessionEndedForProofsAt is DateTime?
|
||||
? sessionEndedForProofsAt
|
||||
: this.sessionEndedForProofsAt,
|
||||
authenticatorResetAt: authenticatorResetAt is DateTime?
|
||||
? authenticatorResetAt
|
||||
: this.authenticatorResetAt,
|
||||
accountResetAt: accountResetAt is DateTime?
|
||||
? accountResetAt
|
||||
: this.accountResetAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -747,6 +747,25 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
password: params['password'],
|
||||
),
|
||||
),
|
||||
'callOffResets': _is.MethodConnector(
|
||||
name: 'callOffResets',
|
||||
params: {
|
||||
'proof': _is.ParameterDescription(
|
||||
name: 'proof',
|
||||
type: _is.getType<_ifhod73s.OwnerProof>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async => (endpoints['password'] as _ig7fyg1b.PasswordEndpoint)
|
||||
.callOffResets(
|
||||
session,
|
||||
proof: params['proof'],
|
||||
),
|
||||
),
|
||||
'setRecoveryKeys': _is.MethodConnector(
|
||||
name: 'setRecoveryKeys',
|
||||
params: {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ password:
|
|||
- set:
|
||||
- beginChange:
|
||||
- change:
|
||||
- callOffResets:
|
||||
- setRecoveryKeys:
|
||||
- dismissSessionEndedWarning:
|
||||
passwordSignIn:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
|
||||
|
|
@ -9,6 +10,21 @@ import 'package:serverpod/serverpod.dart';
|
|||
// it waits anew, with a new mail. A reset of the authenticators carried out
|
||||
// never lapses: its row says the password alone signs in until an
|
||||
// authenticator is confirmed, which removes it.
|
||||
//
|
||||
// Nor does a reset take effect while its owner is held out of calling it
|
||||
// off (the full reading's H1): whoever knows the password made the codes
|
||||
// wait, or whoever knows the address filled its proofs' ceiling, and the
|
||||
// owner's sign-in, which calls every reset off, was refused. Each such
|
||||
// hold-out moves the account's resets not carried out to take effect no
|
||||
// sooner than [resetDelay] after it ends ([pushOutResets]), so the owner
|
||||
// always has that long, once let in, to call them off; and a signed-in
|
||||
// device shows them (`SignInWays`), whose owner calls them off with their
|
||||
// proof, which no wait holds back.
|
||||
|
||||
/// How long a reset waits before it takes effect (ADR 0017, D3): long
|
||||
/// enough for the account's owner, told by mail, to call it off by signing
|
||||
/// in.
|
||||
const resetDelay = Duration(hours: 72);
|
||||
|
||||
/// How long a reset that took effect waits to be carried out before it
|
||||
/// lapses.
|
||||
|
|
@ -69,3 +85,80 @@ Future<int> forgetLapsedResets(
|
|||
}
|
||||
return accounts.length + lapsed.length;
|
||||
}
|
||||
|
||||
/// Moves [account]'s resets not carried out and not lapsed to take effect
|
||||
/// no sooner than [resetDelay] after [heldOutUntil], when its owner, held
|
||||
/// out of signing in meanwhile, can call them off again (the full reading's
|
||||
/// H1). A reset of the authenticators carried out (none confirmed any more)
|
||||
/// stays as it is: its owner signs in with the password alone meanwhile.
|
||||
Future<void> pushOutResets(
|
||||
Session session,
|
||||
UuidValue account,
|
||||
DateTime heldOutUntil, {
|
||||
Transaction? transaction,
|
||||
}) async {
|
||||
final takesEffectAt = heldOutUntil.toUtc().add(resetDelay);
|
||||
final notLapsedSince = clock.now().toUtc().subtract(resetLapsesAfter);
|
||||
// SELECT count(*) FROM "totp_authenticator"
|
||||
// WHERE "authUserId" = $1 AND "confirmedAt" IS NOT NULL
|
||||
final notCarriedOut =
|
||||
await TotpAuthenticator.db.count(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.authUserId.equals(account) & t.confirmedAt.notEquals(null),
|
||||
transaction: transaction,
|
||||
) >
|
||||
0;
|
||||
// UPDATE "account_reset" SET "takesEffectAt" = $1
|
||||
// WHERE "authUserId" = $2 AND "takesEffectAt" < $1
|
||||
// AND "takesEffectAt" > $3 [AND "kind" = 'account']
|
||||
await AccountReset.db.updateWhere(
|
||||
session,
|
||||
columnValues: (t) => [t.takesEffectAt(takesEffectAt)],
|
||||
where: (t) {
|
||||
final waiting =
|
||||
t.authUserId.equals(account) &
|
||||
(t.takesEffectAt < takesEffectAt) &
|
||||
(t.takesEffectAt > notLapsedSince);
|
||||
return notCarriedOut
|
||||
? waiting
|
||||
: waiting & t.kind.equals(AccountResetKind.account);
|
||||
},
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// When [account]'s resets waiting (or due, not carried out and not lapsed)
|
||||
/// take effect, each kind's; null for none.
|
||||
Future<({DateTime? authenticator, DateTime? account})> waitingResets(
|
||||
Session session,
|
||||
UuidValue account,
|
||||
) async {
|
||||
final now = clock.now().toUtc();
|
||||
// SELECT * FROM "account_reset" WHERE "authUserId" = $1
|
||||
final rows = await AccountReset.db.find(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(account),
|
||||
);
|
||||
DateTime? whenOf(AccountResetKind kind) => rows
|
||||
.where((reset) => reset.kind == kind && !resetLapsed(reset, now))
|
||||
.firstOrNull
|
||||
?.takesEffectAt;
|
||||
final authenticatorAt = whenOf(AccountResetKind.authenticator);
|
||||
return (
|
||||
authenticator:
|
||||
authenticatorAt != null &&
|
||||
// SELECT count(*) FROM "totp_authenticator"
|
||||
// WHERE "authUserId" = $1 AND "confirmedAt" IS NOT NULL
|
||||
await TotpAuthenticator.db.count(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.authUserId.equals(account) &
|
||||
t.confirmedAt.notEquals(null),
|
||||
) >
|
||||
0
|
||||
? authenticatorAt
|
||||
: null,
|
||||
account: whenOf(AccountResetKind.account),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,21 @@ Future<void> changePassword(
|
|||
await announceSessionsEnded(session, person);
|
||||
}
|
||||
|
||||
/// Calls off every reset of [person]'s account waiting, with the owner's
|
||||
/// [proof] (the full reading's H1): a signed-in device shows the resets
|
||||
/// (`SignInWays`), and its owner, held out of signing in by codes made to
|
||||
/// wait, calls them off all the same, as no wait holds a signed-in owner's
|
||||
/// proof back. Refusals as [proveOwner]'s.
|
||||
Future<void> callOffResets(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required OwnerProof proof,
|
||||
}) async {
|
||||
final keys = requirePasswordKeys(session.serverpod);
|
||||
// The owner's code, accepted, calls every reset off (`ownersCode`).
|
||||
await proveOwner(session, keys, person: person, proof: proof);
|
||||
}
|
||||
|
||||
/// Replaces the wrapping of [person]'s keys under their recovery phrase
|
||||
/// with [recoveryKeys] (a new phrase, made on the device, which holds the
|
||||
/// keys) and the phrase's proof key with [recoveryProofKey] (its public
|
||||
|
|
|
|||
|
|
@ -100,6 +100,29 @@ class PasswordEndpoint extends Endpoint {
|
|||
);
|
||||
}
|
||||
|
||||
/// Calls off every reset of the caller's account waiting, with the
|
||||
/// owner's [proof] (ADR 0017, the full reading's H1: a signed-in device
|
||||
/// shows the resets, `SignInWays`), even while the account's codes wait;
|
||||
/// counted against this session as every owner's proof is. Refusals as
|
||||
/// `callOffResets`' ([PasswordRefusedException]).
|
||||
Future<void> callOffResets(
|
||||
Session session, {
|
||||
required OwnerProof proof,
|
||||
}) {
|
||||
final bound = proof.copyWith(
|
||||
host: deviceHostOn(session.serverpod, proof.host),
|
||||
);
|
||||
return asOwnerProofTry(
|
||||
session,
|
||||
bound,
|
||||
() => AccountRegister.of(session.serverpod).callOffResets(
|
||||
session,
|
||||
person: session.authenticated!.authUserId,
|
||||
proof: bound,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// A new recovery phrase: [recoveryKeys], the caller's keys wrapped under
|
||||
/// it on the device, replace the old wrapping, and [recoveryProofKey], the
|
||||
/// public half of its proof key, the old one, with the owner's [proof], as
|
||||
|
|
|
|||
|
|
@ -42,11 +42,6 @@ const passwordMethod = 'password';
|
|||
/// second step; it is taken once, either way.
|
||||
const signInChallengeLifetime = Duration(minutes: 2);
|
||||
|
||||
/// How long a reset waits before it takes effect (ADR 0017, D3): long
|
||||
/// enough for the account's owner, told by mail, to call it off by signing
|
||||
/// in.
|
||||
const resetDelay = Duration(hours: 72);
|
||||
|
||||
/// The group Halkyon signs in with.
|
||||
final passwordGroup = SrpGroup.rfc5054Group3072;
|
||||
|
||||
|
|
@ -119,10 +114,13 @@ final _proofsPerAddress = DatabaseRateLimiter(
|
|||
domain: 'password_sign_in',
|
||||
source: 'proofs_per_address',
|
||||
maxAttempts: 30,
|
||||
timeframe: const Duration(hours: 1),
|
||||
timeframe: _proofsWindow,
|
||||
),
|
||||
);
|
||||
|
||||
/// [_proofsPerAddress]' window: how long its ceiling holds an owner out.
|
||||
const _proofsWindow = Duration(hours: 1);
|
||||
|
||||
/// How many sign-ins and proofs of the recovery phrase one network (IP
|
||||
/// address) begins in an hour.
|
||||
final _beginsPerNetwork = DatabaseRateLimiter(
|
||||
|
|
@ -152,15 +150,34 @@ final _keyProofsPerAddressAndNetwork = DatabaseRateLimiter(
|
|||
Future<void> _refuseAddressProvingTooOften(Session session, String hash) async {
|
||||
if (await _proofsPerAddress.countAttempts(session, key: hash) >=
|
||||
_proofsPerAddress.config.maxAttempts) {
|
||||
throw refusedFor(PasswordRefusal.tooManyAttempts);
|
||||
await _heldOut(session, hash);
|
||||
}
|
||||
}
|
||||
|
||||
/// Refuses ([PasswordRefusal.tooManyAttempts]) a proof or sign-in for
|
||||
/// [hash]'s address past [_proofsPerAddress]' ceiling, the owner held out
|
||||
/// for its hour: the account's resets are pushed out past it (the full
|
||||
/// reading's H1), so whoever filled the ceiling can't carry one out
|
||||
/// meanwhile. The same queries for an address nobody has.
|
||||
Future<Never> _heldOut(Session session, String hash) async {
|
||||
// SELECT * FROM "email_identity" WHERE "addressHash" = $1
|
||||
final identity = await EmailIdentity.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.addressHash.equals(hash),
|
||||
);
|
||||
await pushOutResets(
|
||||
session,
|
||||
identity?.authUserId ?? UuidValue.fromString(_nobody),
|
||||
clock.now().toUtc().add(_proofsWindow),
|
||||
);
|
||||
throw refusedFor(PasswordRefusal.tooManyAttempts);
|
||||
}
|
||||
|
||||
/// Counts a proof for [hash]'s address about to be checked; refused
|
||||
/// ([PasswordRefusal.tooManyAttempts]) past [_proofsPerAddress]' ceiling.
|
||||
Future<void> _countProof(Session session, String hash) async {
|
||||
if (!await _proofsPerAddress.tryRecordAttempt(session, key: hash)) {
|
||||
throw refusedFor(PasswordRefusal.tooManyAttempts);
|
||||
await _heldOut(session, hash);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'dart:math';
|
|||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart';
|
||||
import 'package:halkyon_base_server/src/password/account_resets.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
|
||||
// Codes guessed with the password known (ADR 0017; the second reading's M4):
|
||||
|
|
@ -52,11 +53,36 @@ const longestCodeWait = Duration(days: 1);
|
|||
/// One attempt at a time per account (a transaction holding an advisory
|
||||
/// lock on the account's key), so of codes sent at once at most the ten
|
||||
/// before a wait are checked.
|
||||
///
|
||||
/// While codes wait, the account's owner can't sign in to call a reset
|
||||
/// off: a wait that begins, and an attempt a wait refuses, push the
|
||||
/// account's resets out past it (`pushOutResets`, the full reading's H1).
|
||||
Future<DateTime?> takeCodeAttempt(
|
||||
Session session,
|
||||
UuidValue account, {
|
||||
SecondFactorPath path = SecondFactorPath.password,
|
||||
}) {
|
||||
}) async {
|
||||
final taken = await _takeCodeAttempt(session, account, path);
|
||||
final heldOutUntil = taken.refusedUntil ?? taken.began;
|
||||
if (heldOutUntil != null) {
|
||||
await pushOutResets(session, account, heldOutUntil);
|
||||
}
|
||||
if (taken.refusedUntil case final waiting?) {
|
||||
throw PasswordRefusedException(
|
||||
reason: PasswordRefusal.codesWait,
|
||||
takesEffectAt: waiting,
|
||||
);
|
||||
}
|
||||
return taken.began;
|
||||
}
|
||||
|
||||
/// [takeCodeAttempt]'s count: the wait that refused the attempt, or the
|
||||
/// wait it began.
|
||||
Future<({DateTime? refusedUntil, DateTime? began})> _takeCodeAttempt(
|
||||
Session session,
|
||||
UuidValue account,
|
||||
SecondFactorPath path,
|
||||
) {
|
||||
final now = clock.now().toUtc();
|
||||
return session.db.transaction((transaction) async {
|
||||
// SELECT pg_advisory_xact_lock($1)
|
||||
|
|
@ -76,10 +102,7 @@ Future<DateTime?> takeCodeAttempt(
|
|||
);
|
||||
final waiting = row?.waitUntil;
|
||||
if (waiting != null && waiting.isAfter(now)) {
|
||||
throw PasswordRefusedException(
|
||||
reason: PasswordRefusal.codesWait,
|
||||
takesEffectAt: waiting,
|
||||
);
|
||||
return (refusedUntil: waiting, began: null);
|
||||
}
|
||||
final failures = (row?.failures ?? 0) + 1;
|
||||
final waits = row?.waits ?? 0;
|
||||
|
|
@ -109,7 +132,7 @@ Future<DateTime?> takeCodeAttempt(
|
|||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
return until;
|
||||
return (refusedUntil: null, began: until);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -318,6 +318,15 @@ abstract interface class AccountRegister {
|
|||
UuidValue? keepSession,
|
||||
});
|
||||
|
||||
/// Calls off every reset of [person]'s account waiting, with the owner's
|
||||
/// [proof] (ADR 0017, the full reading's H1). Refusals as
|
||||
/// `callOffResets`'.
|
||||
Future<void> callOffResets(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required OwnerProof proof,
|
||||
});
|
||||
|
||||
/// Replaces the wrapping of [person]'s keys under their recovery phrase
|
||||
/// and the phrase's proof key, with the owner's [proof]. Refusals as
|
||||
/// `setRecoveryKeys`'.
|
||||
|
|
|
|||
|
|
@ -381,6 +381,13 @@ Future<Map<String, Object?>?> _answer(
|
|||
keepSession: arguments.optionalId('keepSession'),
|
||||
);
|
||||
return const {};
|
||||
case RegisterOperation.callOffResets:
|
||||
await register.callOffResets(
|
||||
session,
|
||||
person: arguments.id('person'),
|
||||
proof: arguments.model('proof', OwnerProof.fromJson),
|
||||
);
|
||||
return const {};
|
||||
case RegisterOperation.setRecoveryKeys:
|
||||
await register.setRecoveryKeys(
|
||||
session,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import 'package:halkyon_base_server/src/generated/protocol.dart';
|
|||
import 'package:halkyon_base_server/src/halkyon_base.dart';
|
||||
import 'package:halkyon_base_server/src/password/account_recovery.dart'
|
||||
as recovery;
|
||||
import 'package:halkyon_base_server/src/password/account_resets.dart'
|
||||
as account_resets;
|
||||
import 'package:halkyon_base_server/src/password/authenticators.dart'
|
||||
as authenticators;
|
||||
import 'package:halkyon_base_server/src/password/owner_proof_tries.dart'
|
||||
|
|
@ -369,6 +371,7 @@ final class LocalRegister implements AccountRegister {
|
|||
session,
|
||||
where: (t) => t.authUserId.equals(person),
|
||||
);
|
||||
final resets = await account_resets.waitingResets(session, person);
|
||||
return SignInWays(
|
||||
// SELECT count(*) FROM "serverpod_auth_idp_github_account"
|
||||
// WHERE "authUserId" = $1
|
||||
|
|
@ -393,6 +396,8 @@ final class LocalRegister implements AccountRegister {
|
|||
passwordOnly: await passwords.signsInWithPassword(session, person),
|
||||
secondFactorWorks: await passwords.secondFactorWorks(session, person),
|
||||
sessionEndedForProofsAt: password?.sessionEndedForProofsAt,
|
||||
authenticatorResetAt: resets.authenticator,
|
||||
accountResetAt: resets.account,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -640,6 +645,13 @@ final class LocalRegister implements AccountRegister {
|
|||
keepSession: keepSession,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> callOffResets(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required OwnerProof proof,
|
||||
}) => change.callOffResets(session, person: person, proof: proof);
|
||||
|
||||
@override
|
||||
Future<void> setRecoveryKeys(
|
||||
Session session, {
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ abstract final class RegisterOperation {
|
|||
static const beginPasswordChange = 'begin-password-change';
|
||||
static const changePassword = 'change-password';
|
||||
static const setRecoveryKeys = 'set-recovery-keys';
|
||||
static const callOffResets = 'call-off-resets';
|
||||
}
|
||||
|
||||
/// A refusal the register answered, as its JSON: the exception the module's
|
||||
|
|
|
|||
|
|
@ -604,6 +604,16 @@ final class RemoteRegister implements AccountRegister {
|
|||
'keepSession': keepSession?.uuid,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<void> callOffResets(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required OwnerProof proof,
|
||||
}) => _ask(RegisterOperation.callOffResets, {
|
||||
'person': person.uuid,
|
||||
'proof': proof.toJson(),
|
||||
});
|
||||
|
||||
@override
|
||||
Future<void> setRecoveryKeys(
|
||||
Session session, {
|
||||
|
|
|
|||
Loading…
Reference in a new issue