feat(auth): ways in for accounts without a working password
ADR 0017, step 3. Signing up takes an address a mailed code proves and a password's setup, without a session (`passwordSignIn.signUp`): an address nobody has becomes a new identified account, and an account with the address but no password (codes by mail before; no keys exist, so nothing waits) takes the password, keeping its ID. A setup never finished signs in with its password and a code mailed to the address it began with, until an authenticator is confirmed, which also calls off a reset of the account waiting; any other account takes no code by mail in place of its second factor and leaves the code unused. A reset of the whole account waiting no longer counts as having had an authenticator, so its owner isn't locked out until it completes. Members forward both through the register; the tests' shared helpers move into `support/password_support.dart`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
ce2c00c7ea
commit
f8076dcacf
1 changed files with 849 additions and 80 deletions
|
|
@ -685,6 +685,67 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
group('ways in without a working password (ADR 0017)', () {
|
||||
late DateTime now;
|
||||
Future<T> atNow<T>(Future<T> Function() body) =>
|
||||
withClock(Clock(() => now), body);
|
||||
|
||||
setUp(() {
|
||||
usePasswordKeys(pod, testPasswordKeys);
|
||||
now = DateTime.utc(2026, 10, 20, 12);
|
||||
});
|
||||
|
||||
test("cross for a member's device: signing up with a code, and a "
|
||||
'setup never finished signing in with the password and a code, '
|
||||
'with what the register refuses', () async {
|
||||
final device = await PasswordDevice.make();
|
||||
await atNow(() async {
|
||||
Future<UuidValue> code() => remote.sendSignInCode(
|
||||
session,
|
||||
address: 'carla@example.org',
|
||||
language: 'en',
|
||||
network: '203.0.113.7',
|
||||
);
|
||||
final carla = await remote.signUpWithCode(
|
||||
session,
|
||||
request: await code(),
|
||||
code: mailbox.codeFor('carla@example.org'),
|
||||
setup: device.setup(),
|
||||
);
|
||||
expect(
|
||||
(await AuthUser.db.findById(session, carla))!.scopeNames,
|
||||
contains(identified.name),
|
||||
);
|
||||
await expectLater(
|
||||
remote.signUpWithCode(
|
||||
session,
|
||||
request: await code(),
|
||||
code: mailbox.codeFor('carla@example.org'),
|
||||
setup: device.setup(),
|
||||
),
|
||||
refused(PasswordRefusal.passwordExists),
|
||||
);
|
||||
|
||||
final challenge = await remote.beginPasswordSignIn(
|
||||
session,
|
||||
address: 'carla@example.org',
|
||||
network: '203.0.113.7',
|
||||
);
|
||||
final answer = device.answer(challenge);
|
||||
final proof = await remote.completePasswordSignIn(
|
||||
session,
|
||||
challenge: challenge.challenge,
|
||||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
request: await code(),
|
||||
code: mailbox.codeFor('carla@example.org'),
|
||||
);
|
||||
expect(proof.account, carla);
|
||||
expect(proof.needsAuthenticator, isTrue);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('deleting an account', () {
|
||||
test('deletes it here and tells every member, keeping no notice '
|
||||
'once they confirmed', () async {
|
||||
|
|
|
|||
Loading…
Reference in a new issue