fix(auth): every call with a code after the password takes the address
Whoever guessed codes decided whether the owner was mailed (the re-review's fourth finding): the address a sign-in sent was optional, so a guesser left it out and no mail went, and a wait begun without a mail used up the one mail there was. Every call carrying a code after the password or the phrase proved (signing in, the phrase's reset, a change of password, a new phrase, deletion) now takes the account's address and the language of its mails, and is refused (wrongCredentials, as anything wrong after the password) before any code is checked when the address isn't the account's; every wait mails the owner, not only the first. The app sends the address the person typed, or for changes while signed in the one this device signed in with, and says so where it doesn't know it (addressUnknown). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
e8c4a3d472
commit
2a21672b43
1 changed files with 795 additions and 347 deletions
|
|
@ -48,6 +48,7 @@ void main() {
|
|||
final pending = await passwords.signIn(
|
||||
unlocked,
|
||||
totpCode: fakeTotpCode,
|
||||
language: 'en',
|
||||
);
|
||||
|
||||
expect(pending.needsAuthenticator, isFalse);
|
||||
|
|
@ -71,7 +72,7 @@ void main() {
|
|||
'and begins a new challenge for the next try', () async {
|
||||
final wrong = await passwords.unlock('anna@example.org', 'wrong');
|
||||
await expectLater(
|
||||
passwords.signIn(wrong, totpCode: fakeTotpCode),
|
||||
passwords.signIn(wrong, totpCode: fakeTotpCode, language: 'en'),
|
||||
failsWith(PasswordProblem.wrongCredentials),
|
||||
);
|
||||
final right = await passwords.unlock(
|
||||
|
|
@ -79,11 +80,11 @@ void main() {
|
|||
'correct horse',
|
||||
);
|
||||
await expectLater(
|
||||
passwords.signIn(right, totpCode: '000000'),
|
||||
passwords.signIn(right, totpCode: '000000', language: 'en'),
|
||||
failsWith(PasswordProblem.wrongCredentials),
|
||||
);
|
||||
|
||||
await passwords.signIn(right, totpCode: fakeTotpCode);
|
||||
await passwords.signIn(right, totpCode: fakeTotpCode, language: 'en');
|
||||
|
||||
expect(gateway.calls.where((call) => call == 'begin'), hasLength(3));
|
||||
expect(auth.status, const SignedOut());
|
||||
|
|
@ -98,7 +99,8 @@ void main() {
|
|||
|
||||
await withClock(
|
||||
Clock.fixed(DateTime.utc(2026, 10, 20, 12, 2)),
|
||||
() => passwords.signIn(unlocked, totpCode: fakeTotpCode),
|
||||
() =>
|
||||
passwords.signIn(unlocked, totpCode: fakeTotpCode, language: 'en'),
|
||||
);
|
||||
|
||||
expect(gateway.calls, ['begin', 'begin', 'complete']);
|
||||
|
|
@ -113,7 +115,7 @@ void main() {
|
|||
);
|
||||
|
||||
await expectLater(
|
||||
passwords.signIn(unlocked, totpCode: fakeTotpCode),
|
||||
passwords.signIn(unlocked, totpCode: fakeTotpCode, language: 'en'),
|
||||
failsWith(PasswordProblem.instanceUnproven),
|
||||
);
|
||||
});
|
||||
|
|
@ -131,7 +133,7 @@ void main() {
|
|||
'battery staple',
|
||||
);
|
||||
await expectLater(
|
||||
passwords.signIn(unlocked),
|
||||
passwords.signIn(unlocked, language: 'en'),
|
||||
failsWith(PasswordProblem.setupIncomplete),
|
||||
);
|
||||
final request = await passwords.sendCode(
|
||||
|
|
@ -143,6 +145,7 @@ void main() {
|
|||
unlocked,
|
||||
request: request,
|
||||
code: gateway.codeFor('carla@example.org'),
|
||||
language: 'en',
|
||||
);
|
||||
expect(pending.needsAuthenticator, isTrue);
|
||||
final setup = await passwords.enrol(
|
||||
|
|
@ -255,6 +258,7 @@ void main() {
|
|||
final pending = await passwords.signIn(
|
||||
await passwords.unlock('anna@example.org', 'correct horse'),
|
||||
totpCode: fakeTotpCode,
|
||||
language: 'en',
|
||||
);
|
||||
await passwords.enter(pending);
|
||||
await settle();
|
||||
|
|
@ -266,6 +270,7 @@ void main() {
|
|||
oldPassword: 'correct horse',
|
||||
newPassword: 'better horse',
|
||||
totpCode: fakeTotpCode,
|
||||
language: 'en',
|
||||
);
|
||||
|
||||
expect(anna.password, 'better horse');
|
||||
|
|
@ -276,6 +281,7 @@ void main() {
|
|||
final again = await passwords.signIn(
|
||||
unlocked,
|
||||
totpCode: fakeTotpCode,
|
||||
language: 'en',
|
||||
);
|
||||
expect(again.keys, keys.kept[anna.person]);
|
||||
});
|
||||
|
|
@ -287,6 +293,7 @@ void main() {
|
|||
oldPassword: 'wrong',
|
||||
newPassword: 'better horse',
|
||||
totpCode: fakeTotpCode,
|
||||
language: 'en',
|
||||
),
|
||||
failsWith(PasswordProblem.wrongCredentials),
|
||||
);
|
||||
|
|
@ -296,20 +303,58 @@ void main() {
|
|||
oldPassword: 'correct horse',
|
||||
newPassword: 'better horse',
|
||||
totpCode: fakeTotpCode,
|
||||
language: 'en',
|
||||
),
|
||||
failsWith(PasswordProblem.keysMissing),
|
||||
);
|
||||
expect(anna.password, 'correct horse');
|
||||
});
|
||||
|
||||
test('sends the address this device signed in with, and says when it '
|
||||
"doesn't know it, sending nothing", () async {
|
||||
await passwords.changePassword(
|
||||
oldPassword: 'correct horse',
|
||||
newPassword: 'better horse',
|
||||
totpCode: fakeTotpCode,
|
||||
language: 'en',
|
||||
);
|
||||
expect(anna.password, 'better horse');
|
||||
|
||||
addresses.addresses.clear();
|
||||
final calls = gateway.calls.length;
|
||||
await expectLater(
|
||||
passwords.changePassword(
|
||||
oldPassword: 'better horse',
|
||||
newPassword: 'best horse',
|
||||
totpCode: fakeTotpCode,
|
||||
language: 'en',
|
||||
),
|
||||
failsWith(PasswordProblem.addressUnknown),
|
||||
);
|
||||
await expectLater(
|
||||
passwords.deleteAccount(
|
||||
password: 'better horse',
|
||||
totpCode: fakeTotpCode,
|
||||
language: 'en',
|
||||
),
|
||||
failsWith(PasswordProblem.addressUnknown),
|
||||
);
|
||||
expect(gateway.calls, hasLength(calls));
|
||||
expect(anna.password, 'better horse');
|
||||
});
|
||||
|
||||
test('deletes the account with the password and a code alone, then '
|
||||
'signed out here; a wrong one or none deletes nothing', () async {
|
||||
await expectLater(
|
||||
passwords.deleteAccount(password: 'wrong', totpCode: fakeTotpCode),
|
||||
passwords.deleteAccount(
|
||||
password: 'wrong',
|
||||
totpCode: fakeTotpCode,
|
||||
language: 'en',
|
||||
),
|
||||
failsWith(PasswordProblem.wrongCredentials),
|
||||
);
|
||||
await expectLater(
|
||||
passwords.deleteAccount(password: 'correct horse'),
|
||||
passwords.deleteAccount(password: 'correct horse', language: 'en'),
|
||||
failsWith(PasswordProblem.wrongCredentials),
|
||||
);
|
||||
expect(gateway.accounts, contains('anna@example.org'));
|
||||
|
|
@ -318,6 +363,7 @@ void main() {
|
|||
await passwords.deleteAccount(
|
||||
password: 'correct horse',
|
||||
totpCode: fakeTotpCode,
|
||||
language: 'en',
|
||||
);
|
||||
|
||||
expect(gateway.accounts, isNot(contains('anna@example.org')));
|
||||
|
|
@ -333,6 +379,7 @@ void main() {
|
|||
phrase,
|
||||
password: 'correct horse',
|
||||
code: '000000',
|
||||
language: 'en',
|
||||
),
|
||||
failsWith(PasswordProblem.wrongCredentials),
|
||||
);
|
||||
|
|
@ -341,6 +388,7 @@ void main() {
|
|||
phrase,
|
||||
password: 'wrong',
|
||||
code: fakeTotpCode,
|
||||
language: 'en',
|
||||
),
|
||||
failsWith(PasswordProblem.wrongCredentials),
|
||||
);
|
||||
|
|
@ -350,6 +398,7 @@ void main() {
|
|||
phrase,
|
||||
password: 'correct horse',
|
||||
code: fakeTotpCode,
|
||||
language: 'en',
|
||||
);
|
||||
|
||||
await auth.signOut();
|
||||
|
|
@ -358,6 +407,7 @@ void main() {
|
|||
phrase: otherFakePhrase,
|
||||
newPassword: 'after the phrase',
|
||||
totpCode: fakeTotpCode,
|
||||
language: 'en',
|
||||
);
|
||||
expect(pending.grant.authUserId, anna.person);
|
||||
});
|
||||
|
|
@ -392,6 +442,7 @@ void main() {
|
|||
phrase: otherFakePhrase,
|
||||
newPassword: 'a new password',
|
||||
totpCode: fakeTotpCode,
|
||||
language: 'en',
|
||||
),
|
||||
failsWith(PasswordProblem.phraseDoesNotFit),
|
||||
);
|
||||
|
|
@ -401,6 +452,7 @@ void main() {
|
|||
phrase: 'too short',
|
||||
newPassword: 'a new password',
|
||||
totpCode: fakeTotpCode,
|
||||
language: 'en',
|
||||
),
|
||||
failsWith(PasswordProblem.notAPhrase),
|
||||
);
|
||||
|
|
@ -410,6 +462,7 @@ void main() {
|
|||
phrase: fakePhrase,
|
||||
newPassword: 'a new password',
|
||||
totpCode: fakeTotpCode,
|
||||
language: 'en',
|
||||
);
|
||||
|
||||
expect(anna.password, 'a new password');
|
||||
|
|
@ -440,7 +493,10 @@ void main() {
|
|||
|
||||
expect(at, now.add(fakeResetDelay));
|
||||
await expectLater(
|
||||
withClock(Clock.fixed(now), () => passwords.signIn(unlocked)),
|
||||
withClock(
|
||||
Clock.fixed(now),
|
||||
() => passwords.signIn(unlocked, language: 'en'),
|
||||
),
|
||||
throwsA(
|
||||
isA<PasswordFailed>()
|
||||
.having((f) => f.problem, 'problem', PasswordProblem.notDue)
|
||||
|
|
@ -449,7 +505,7 @@ void main() {
|
|||
);
|
||||
final pending = await withClock(
|
||||
Clock.fixed(at),
|
||||
() => passwords.signIn(unlocked),
|
||||
() => passwords.signIn(unlocked, language: 'en'),
|
||||
);
|
||||
expect(pending.needsAuthenticator, isTrue);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue