fix(auth): a proof the register couldn't answer counts; the sign-up mail says it gives a password

Two of the fourth reading's lows:

- A signed-in proof whose call to the register failed
  (RegisterUnavailableException, a member's call timing out after the
  register checked it) stays counted against the session, rather than
  taken back: a guess never goes uncounted.
- A sign-up code also gives an existing account with that address its
  first password, which its mail now says in the subject too ("create an
  account or give it a password").

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-21 11:46:01 +02:00
commit f154b7ea7c
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 50 additions and 10 deletions

View file

@ -97,7 +97,7 @@ final class LogCodeMailer implements CodeMailer {
// A compound with the name, hyphenated through: Halkyon-Learn-Code.
subject:
'$code: dein ${app.replaceAll(' ', '-')}-Code, ${switch (purpose) {
EmailCodePurpose.signUp => 'um ein Konto anzulegen',
EmailCodePurpose.signUp => 'um ein Konto anzulegen oder ihm ein Passwort zu geben',
EmailCodePurpose.finishSetup => 'um dein Konto fertig einzurichten',
EmailCodePurpose.authenticatorReset => 'um deine Authenticator-App abzuschalten',
EmailCodePurpose.accountReset => 'um dein Konto zurückzusetzen',
@ -133,7 +133,7 @@ final class LogCodeMailer implements CodeMailer {
_ => (
subject:
'$code: your $app code to ${switch (purpose) {
EmailCodePurpose.signUp => 'create an account',
EmailCodePurpose.signUp => 'create an account or give it a password',
EmailCodePurpose.finishSetup => 'finish setting up your account',
EmailCodePurpose.authenticatorReset => 'switch off your authenticator',
EmailCodePurpose.accountReset => 'reset your account',

View file

@ -1,5 +1,6 @@
import 'package:clock/clock.dart';
import 'package:halkyon_base_server/src/generated/protocol.dart';
import 'package:halkyon_base_server/src/register/register_protocol.dart';
import 'package:serverpod/serverpod.dart';
import 'package:serverpod_auth_idp_server/core.dart';
@ -24,7 +25,8 @@ const ownerProofTriesBeforeSignOut = 10;
/// The refusal that says a proof (the password, or the code with it) was
/// wrong: it counts. Any other outcome (a refusal of something else, an
/// error) checked no proof or found it right, and is taken back.
/// error) checked no proof or found it right, and is taken back, but for a
/// register out of reach, which may have checked it.
const _wrongProofs = {PasswordRefusal.wrongCredentials};
/// [check]'s outcome, with [proof] sent from [session]'s signed-in session
@ -78,6 +80,10 @@ Future<T> asOwnerProofTry<T>(
throw PasswordRefusedException(reason: PasswordRefusal.sessionEnded);
}
rethrow;
} on RegisterUnavailableException {
// The register may have checked it before the answer was lost (a
// member's call timing out): it stays counted (the fourth reading).
rethrow;
} on Object {
await _takeBack(session, sessionId);
rethrow;