feat(auth): a session ended for its wrong proofs warns its owner
The tenth wrong owner proof in a row ends a session (the third reading); someone holding that session tried the password or its codes. The register now keeps the moment with the account's password (`account_password.sessionEndedForProofsAt`): the account's sign-in ways and its next password sign-in carry it, until the owner dismisses it (`password.dismissSessionEndedWarning`, their session alone: it hides a warning and secures nothing) or sets a new password. A member ending its own session tells the register (`noteSessionEndedForProofs`); a register out of reach leaves the owner unwarned, the session ended all the same. Migration (module, Learn's and Todo's servers, Learn's device schema): ALTER TABLE "account_password" ADD COLUMN "sessionEndedForProofsAt" timestamp without time zone; nullable, so every row keeps its values and has no warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
858fffdce3
commit
5e7d7f4e36
1 changed files with 41919 additions and 34 deletions
|
|
@ -422,6 +422,77 @@ void main() {
|
|||
await change(from: onLaptop());
|
||||
});
|
||||
|
||||
group('a session ended for its wrong proofs leaves its owner a '
|
||||
'warning', () {
|
||||
/// Sends ten wrong proofs from Anna's phone, the tenth ending its
|
||||
/// session; the moment it ended.
|
||||
Future<DateTime> endPhone() async {
|
||||
final other = await PasswordDevice.make();
|
||||
for (var i = 0; i < 9; i++) {
|
||||
await expectLater(
|
||||
change(totpCode: '000000', password: other.newPassword()),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
}
|
||||
await expectLater(
|
||||
change(totpCode: '000000', password: other.newPassword()),
|
||||
refused(PasswordRefusal.sessionEnded),
|
||||
);
|
||||
return now;
|
||||
}
|
||||
|
||||
Future<DateTime?> warningOn(TestSessionBuilder device) async =>
|
||||
(await endpoints.account.signInWays(
|
||||
device,
|
||||
)).sessionEndedForProofsAt;
|
||||
|
||||
test('which her sign-in ways and her next sign-in tell, with when, '
|
||||
'until she dismisses it', () async {
|
||||
expect(await warningOn(onLaptop()), isNull);
|
||||
final ended = await endPhone();
|
||||
|
||||
expect(await warningOn(onLaptop()), ended);
|
||||
nextStep();
|
||||
expect((await signIn()).sessionEndedForProofsAt, ended);
|
||||
// Told, not dismissed: it stays until she says so.
|
||||
expect(await warningOn(onLaptop()), ended);
|
||||
|
||||
await endpoints.password.dismissSessionEndedWarning(onLaptop());
|
||||
expect(await warningOn(onLaptop()), isNull);
|
||||
nextStep();
|
||||
expect((await signIn()).sessionEndedForProofsAt, isNull);
|
||||
});
|
||||
|
||||
test('which a new password clears', () async {
|
||||
await endPhone();
|
||||
nextStep();
|
||||
await change(from: onLaptop());
|
||||
expect(await warningOn(onLaptop()), isNull);
|
||||
});
|
||||
|
||||
test('which nobody else reads or dismisses, nor anyone signed '
|
||||
'out', () async {
|
||||
final ended = await endPhone();
|
||||
final bert = await newAnonymousPerson(session);
|
||||
final bertsPhone = await signInNewDevice(session, bert);
|
||||
final asBert = sessionBuilder.copyWith(
|
||||
authentication: AuthenticationOverride.authenticationInfo(
|
||||
bert.uuid,
|
||||
{},
|
||||
authId: bertsPhone.uuid,
|
||||
),
|
||||
);
|
||||
|
||||
expect(await warningOn(asBert), isNull);
|
||||
await endpoints.password.dismissSessionEndedWarning(asBert);
|
||||
await expectLater(
|
||||
endpoints.password.dismissSessionEndedWarning(notSignedIn()),
|
||||
throwsA(isA<ServerpodUnauthenticatedException>()),
|
||||
);
|
||||
expect(await warningOn(onLaptop()), ended);
|
||||
});
|
||||
});
|
||||
|
||||
test('a proof accepted clears the count: wrong ones before it never '
|
||||
'add up to ten with those after', () async {
|
||||
final other = await PasswordDevice.make();
|
||||
|
|
|
|||
Loading…
Reference in a new issue