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:
Tizian 「ティツィアーン」 2026-10-20 14:41:16 +02:00
commit f8076dcacf
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -269,8 +269,23 @@ final class _Register implements AccountRegister {
required ByteData clientProof,
String? totpCode,
String? recoveryCode,
UuidValue? request,
String? code,
}) => _answer('completePasswordSignIn', _proven);
@override
Future<UuidValue> signUpWithCode(
Session session, {
required UuidValue request,
required String code,
required PasswordSetup setup,
}) => _answer('signUpWithCode', () {
final account = const Uuid().v7obj();
add(account);
passwords.add(account);
return account;
});
@override
Future<AuthenticatorEnrolment> enrolAuthenticator(
Session session, {
@ -708,6 +723,29 @@ void main() {
);
});
test('sign up at the register, the new account signed in here, '
'identified', () async {
final device = await PasswordDevice.make();
final signedUp = await endpoints.passwordSignIn.signUp(
notSignedIn(),
request: const Uuid().v7obj(),
code: 'K7QX-M2PD',
setup: device.setup(),
);
expect(register.asked, ['signUpWithCode']);
expect(register.passwords, {signedUp.authUserId});
expect(await scopesOf(signedUp.authUserId), {identified.name});
expect(
(await ServerSideSession.db.find(
session,
where: (t) => t.authUserId.equals(signedUp.authUserId),
)).single.method,
passwordMethod,
);
});
test('refuse as the register refuses, signing nobody in', () async {
final people = await AuthUser.db.count(session);