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
2 changed files with 849 additions and 80 deletions

View file

@ -497,8 +497,9 @@ class EndpointPasswordSignIn extends _isc.EndpointRef {
);
/// The second step: the device's A ([clientValue]) and proof M1
/// ([clientProof]) for [challenge], and a [totpCode] or a [recoveryCode].
/// Signed in, SRP's M2 (which the device checks before trusting anything
/// ([clientProof]) for [challenge], and a [totpCode] or a [recoveryCode]
/// (or, for a setup never finished, a [code] mailed for [request] to the
/// address the sign-in began with). Signed in, SRP's M2 (which the device checks before trusting anything
/// else), the account's keys as the register keeps them, and the session.
/// Refusals as `completePasswordSignIn`'s.
_ida.Future<_ix90myvx.PasswordSignedIn> complete({
@ -507,6 +508,8 @@ class EndpointPasswordSignIn extends _isc.EndpointRef {
required _idt.ByteData clientProof,
String? totpCode,
String? recoveryCode,
_isc.UuidValue? request,
String? code,
}) => caller.callServerEndpoint<_ix90myvx.PasswordSignedIn>(
'halkyon_base.passwordSignIn',
'complete',
@ -516,6 +519,28 @@ class EndpointPasswordSignIn extends _isc.EndpointRef {
'clientProof': clientProof,
'totpCode': totpCode,
'recoveryCode': recoveryCode,
'request': request,
'code': code,
},
);
/// Signs up with the address a [code] mailed for [request]
/// (`emailCode.sendCode`) proves and the password [setup] describes (all
/// of it made on the device): a new account, or the account with that
/// address that has no password yet. Signed in, the device sets up an
/// authenticator next, before the password signs in elsewhere. Refusals as
/// `signUpWithCode`'s.
_ida.Future<_iacc.AuthSuccess> signUp({
required _isc.UuidValue request,
required String code,
required _ipn3lqh9.PasswordSetup setup,
}) => caller.callServerEndpoint<_iacc.AuthSuccess>(
'halkyon_base.passwordSignIn',
'signUp',
{
'request': request,
'code': code,
'setup': setup,
},
);
}

View file

@ -30,9 +30,8 @@ enum PasswordRefusal implements _isc.SerializableModel {
/// The step began too long ago, or was finished already: begin again.
challengeExpired,
/// The password is right, but no authenticator was confirmed yet: finish
/// setting one up where the password was set (or, while email codes still
/// sign in, signed in with a code by mail).
/// The password is right, but no authenticator was confirmed yet: sign in
/// with the password and a code mailed to the address, then set one up.
setupIncomplete,
/// What the device sent isn't a password's setup: a verifier, a key, a