fix(auth)!: a first password takes a fresh proof of the account's way in
The fourth reading's first finding: password.set took a session alone, so whoever took one made a GitHub-only account, or an address account without a password, theirs for good (their address or password, their authenticator; GitHub then refused the owner). - An account with an address proves it anew: a code mailed to that address for this (EmailCodePurpose.firstPassword, its own mail), never the session alone, a code asked for anything else, or another address. - An account that signs in with GitHub alone does it within 10 minutes of a GitHub sign-in (the session's method and creation time, where the session lives; members tell the register), refused signInAgain before any code is used; the address it adds is proved as before. - Anonymous people still become identified with a code to the address they add. The app mails the code to the person's own address (asking for it where the device doesn't know it), offers a password right after a GitHub sign-in (D2's "at their next sign-in") before the person's app, and asks for a GitHub sign-in anew when the instance refuses an older one, keeping the code typed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
1ada2e27f6
commit
f0413531ff
1 changed files with 762 additions and 66 deletions
|
|
@ -117,6 +117,25 @@ void main() {
|
|||
);
|
||||
}
|
||||
|
||||
/// A code mailed to Anna's own address, for a password set while
|
||||
/// signed in (the fourth reading: a session alone sets none).
|
||||
Future<({UuidValue request, String code})> ownAddressCode() => mailedCode(
|
||||
'anna@example.org',
|
||||
purpose: EmailCodePurpose.firstPassword,
|
||||
);
|
||||
|
||||
/// Anna's password set anew from her phone (until an authenticator is
|
||||
/// confirmed), with a code to her own address.
|
||||
Future<void> annaSetsAgain(PasswordDevice with_) async {
|
||||
final code = await ownAddressCode();
|
||||
await endpoints.password.set(
|
||||
asAnna(),
|
||||
setup: with_.setup(),
|
||||
request: code.request,
|
||||
code: code.code,
|
||||
);
|
||||
}
|
||||
|
||||
/// Anna's password and a confirmed authenticator: its enrolment, and
|
||||
/// the recovery codes it brought.
|
||||
Future<({AuthenticatorEnrolment enrolment, List<String> recoveryCodes})>
|
||||
|
|
@ -208,14 +227,15 @@ void main() {
|
|||
expect(ways.authenticator, isFalse);
|
||||
});
|
||||
|
||||
test('needs no code from someone whose address is theirs', () async {
|
||||
test('takes, from someone whose address is theirs, a code mailed to '
|
||||
'that address for it', () async {
|
||||
await annaSetsPassword();
|
||||
await AccountPassword.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(anna),
|
||||
);
|
||||
|
||||
await endpoints.password.set(asAnna(), setup: device.setup());
|
||||
await annaSetsAgain(device);
|
||||
|
||||
expect(
|
||||
await AccountPassword.db.count(
|
||||
|
|
@ -224,6 +244,127 @@ void main() {
|
|||
),
|
||||
1,
|
||||
);
|
||||
expect(mailbox.codes.last.purpose, EmailCodePurpose.firstPassword);
|
||||
});
|
||||
|
||||
test('is refused to a session alone where the account has an '
|
||||
'address, and for a code asked for anything else or another '
|
||||
'address (the fourth reading)', () async {
|
||||
// An account with an address and no password, as codes by mail
|
||||
// left them: whoever took its session sets nothing.
|
||||
await annaSetsPassword();
|
||||
await AccountPassword.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(anna),
|
||||
);
|
||||
final thief = await PasswordDevice.make(seed: 99);
|
||||
|
||||
await expectLater(
|
||||
endpoints.password.set(asAnna(), setup: thief.setup()),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
final signUp = await mailedCode('anna@example.org');
|
||||
await expectLater(
|
||||
endpoints.password.set(
|
||||
asAnna(),
|
||||
setup: thief.setup(),
|
||||
request: signUp.request,
|
||||
code: signUp.code,
|
||||
),
|
||||
refusedAsEmail(EmailSignInRefusal.codeExpired),
|
||||
);
|
||||
final theirs = await mailedCode(
|
||||
'thief@example.org',
|
||||
purpose: EmailCodePurpose.firstPassword,
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.password.set(
|
||||
asAnna(),
|
||||
setup: thief.setup(),
|
||||
request: theirs.request,
|
||||
code: theirs.code,
|
||||
),
|
||||
refusedAsEmail(EmailSignInRefusal.otherAddressLinked),
|
||||
);
|
||||
expect(await AccountPassword.db.count(session), 0);
|
||||
expect(
|
||||
(await EmailIdentity.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(anna),
|
||||
))!.addressHash,
|
||||
addressHash(pod, 'anna@example.org'),
|
||||
);
|
||||
});
|
||||
|
||||
group('for someone who signs in with GitHub alone', () {
|
||||
setUp(() async {
|
||||
await GitHubAccount.db.insertRow(
|
||||
session,
|
||||
GitHubAccount(userIdentifier: '1001', authUserId: anna),
|
||||
);
|
||||
await grantIdentified(session, anna);
|
||||
});
|
||||
|
||||
TestSessionBuilder from(UuidValue device) => sessionBuilder.copyWith(
|
||||
authentication: AuthenticationOverride.authenticationInfo(
|
||||
anna.uuid,
|
||||
{identified},
|
||||
authId: device.uuid,
|
||||
),
|
||||
);
|
||||
|
||||
test('is refused to a session not made by a GitHub sign-in of the '
|
||||
'last ten minutes, before the code is used, so a stolen '
|
||||
'session claims nothing (the fourth reading)', () async {
|
||||
final thief = await PasswordDevice.make(seed: 99);
|
||||
final code = await mailedCode('thief@example.org');
|
||||
final stale = await atNow(
|
||||
() => signInNewDevice(session, anna, method: gitHubMethod),
|
||||
);
|
||||
now = now.add(gitHubSignInFreshFor + const Duration(seconds: 1));
|
||||
for (final device in [annasPhone, stale]) {
|
||||
await expectLater(
|
||||
atNow(
|
||||
() => endpoints.password.set(
|
||||
from(device),
|
||||
setup: thief.setup(),
|
||||
request: code.request,
|
||||
code: code.code,
|
||||
),
|
||||
),
|
||||
refused(PasswordRefusal.signInAgain),
|
||||
);
|
||||
}
|
||||
expect(await AccountPassword.db.count(session), 0);
|
||||
expect(
|
||||
await EmailIdentity.db.count(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(anna),
|
||||
),
|
||||
0,
|
||||
);
|
||||
expect(await gitHubAccountToSignIn(session, '1001'), anna);
|
||||
|
||||
// Signed in with GitHub just now, the same code still serves.
|
||||
final fresh = await atNow(
|
||||
() => signInNewDevice(session, anna, method: gitHubMethod),
|
||||
);
|
||||
await atNow(
|
||||
() => endpoints.password.set(
|
||||
from(fresh),
|
||||
setup: device.setup(),
|
||||
request: code.request,
|
||||
code: code.code,
|
||||
),
|
||||
);
|
||||
expect(
|
||||
await AccountPassword.db.count(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(anna),
|
||||
),
|
||||
1,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('is refused without an address, keeping nothing', () async {
|
||||
|
|
@ -346,7 +487,7 @@ void main() {
|
|||
'then is refused', () async {
|
||||
await annaSetsPassword();
|
||||
final other = await PasswordDevice.make(seed: 99);
|
||||
await endpoints.password.set(asAnna(), setup: other.setup());
|
||||
await annaSetsAgain(other);
|
||||
expect(
|
||||
Uint8List.sublistView(
|
||||
(await AccountPassword.db.findFirstRow(
|
||||
|
|
@ -370,7 +511,7 @@ void main() {
|
|||
});
|
||||
|
||||
await expectLater(
|
||||
endpoints.password.set(asAnna(), setup: device.setup()),
|
||||
annaSetsAgain(device),
|
||||
refused(PasswordRefusal.passwordExists),
|
||||
);
|
||||
});
|
||||
|
|
@ -1866,7 +2007,7 @@ void main() {
|
|||
|
||||
final other = await PasswordDevice.make(seed: 99);
|
||||
await expectLater(
|
||||
endpoints.password.set(asAnna(), setup: other.setup()),
|
||||
annaSetsAgain(other),
|
||||
refused(PasswordRefusal.passwordExists),
|
||||
);
|
||||
expect(
|
||||
|
|
@ -2647,10 +2788,10 @@ void main() {
|
|||
fresh.signingPublicKey,
|
||||
);
|
||||
await expectLater(finish(fresh), refused(PasswordRefusal.noReset));
|
||||
// A session alone replaces neither, before an authenticator is
|
||||
// confirmed again.
|
||||
// A session (even with a code to the address) replaces neither,
|
||||
// before an authenticator is confirmed again.
|
||||
await expectLater(
|
||||
endpoints.password.set(asAnna(), setup: device.setup()),
|
||||
annaSetsAgain(device),
|
||||
refused(PasswordRefusal.passwordExists),
|
||||
);
|
||||
expect(
|
||||
|
|
|
|||
Loading…
Reference in a new issue