fix(auth): adding an authenticator takes a code from one confirmed before
Enrolling and confirming an authenticator took only a session, so whoever held one (a stolen token) could add a second factor of their own, then use its codes to remove the owner's and make new recovery codes. While the account has a confirmed authenticator, confirming another now also takes a code from one of those (`existingCode`, each step's code once as signing in takes it, within the same 10 tries an hour), refused as `wrongExistingCode`; it is checked only once the new one's code is right, so a wrong one uses none. The first authenticator, and the first after a reset carried out, need none. Members forward it to the register. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
d9039d36ef
commit
892fba97e8
4 changed files with 161 additions and 12 deletions
|
|
@ -855,6 +855,7 @@ void main() {
|
|||
asAnna(),
|
||||
authenticator: second.authenticator,
|
||||
code: codeOf(second, at: now),
|
||||
existingCode: codeOf(complete.enrolment, at: now),
|
||||
),
|
||||
);
|
||||
expect(codes, isEmpty);
|
||||
|
|
@ -866,6 +867,71 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('take, once one is confirmed, a code from one confirmed before '
|
||||
'to add another: a session alone adds none', () async {
|
||||
final complete = await annaComplete();
|
||||
final another = await endpoints.authenticator.enrol(
|
||||
asAnna(),
|
||||
name: 'Not hers',
|
||||
label: '',
|
||||
);
|
||||
Future<List<String>> confirm({
|
||||
String? code,
|
||||
String? existingCode,
|
||||
}) => atNow(
|
||||
() => endpoints.authenticator.confirm(
|
||||
asAnna(),
|
||||
authenticator: another.authenticator,
|
||||
code: code ?? codeOf(another, at: now),
|
||||
existingCode: existingCode,
|
||||
),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
confirm(),
|
||||
refused(PasswordRefusal.wrongExistingCode),
|
||||
);
|
||||
await expectLater(
|
||||
confirm(existingCode: '000000'),
|
||||
refused(PasswordRefusal.wrongExistingCode),
|
||||
);
|
||||
// Its own code is no code from one confirmed before.
|
||||
await expectLater(
|
||||
confirm(existingCode: codeOf(another, at: now)),
|
||||
refused(PasswordRefusal.wrongExistingCode),
|
||||
);
|
||||
// A wrong code from the new one takes nothing of the other's.
|
||||
await expectLater(
|
||||
confirm(
|
||||
code: '000000',
|
||||
existingCode: codeOf(complete.enrolment, at: now),
|
||||
),
|
||||
refused(PasswordRefusal.wrongCode),
|
||||
);
|
||||
expect(
|
||||
(await endpoints.authenticator.list(asAnna())).map(
|
||||
(authenticator) => authenticator.confirmed,
|
||||
),
|
||||
[true, false],
|
||||
);
|
||||
|
||||
expect(
|
||||
await confirm(existingCode: codeOf(complete.enrolment, at: now)),
|
||||
isEmpty,
|
||||
);
|
||||
expect(
|
||||
(await endpoints.authenticator.list(asAnna())).map(
|
||||
(authenticator) => authenticator.confirmed,
|
||||
),
|
||||
[true, true],
|
||||
);
|
||||
// The code it took is used.
|
||||
await expectLater(
|
||||
signIn(totpCode: codeOf(complete.enrolment, at: now)),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
});
|
||||
|
||||
test("are another person's to nobody: unknown", () async {
|
||||
final complete = await annaComplete();
|
||||
final ben = await newAnonymousPerson(session);
|
||||
|
|
@ -930,6 +996,7 @@ void main() {
|
|||
asAnna(),
|
||||
authenticator: second.authenticator,
|
||||
code: codeOf(second, at: now),
|
||||
existingCode: codeOf(complete.enrolment, at: now),
|
||||
),
|
||||
);
|
||||
nextStep();
|
||||
|
|
|
|||
|
|
@ -289,6 +289,7 @@ final class _Register implements AccountRegister {
|
|||
required UuidValue person,
|
||||
required UuidValue authenticator,
|
||||
required String code,
|
||||
String? existingCode,
|
||||
}) => _answer('confirmAuthenticator', () {
|
||||
authenticated.add(person);
|
||||
return const ['K7QX-M2PD'];
|
||||
|
|
|
|||
|
|
@ -531,10 +531,32 @@ void main() {
|
|||
name: 'Spare',
|
||||
label: '',
|
||||
);
|
||||
await expectLater(
|
||||
remote.confirmAuthenticator(
|
||||
session,
|
||||
person: annaOnTodo,
|
||||
authenticator: spare.authenticator,
|
||||
code: codeOf(spare),
|
||||
),
|
||||
refused(PasswordRefusal.wrongExistingCode),
|
||||
);
|
||||
now = now.add(totpPeriod);
|
||||
expect(
|
||||
await remote.confirmAuthenticator(
|
||||
session,
|
||||
person: annaOnTodo,
|
||||
authenticator: spare.authenticator,
|
||||
code: codeOf(spare),
|
||||
existingCode: codeOf(enrolment),
|
||||
),
|
||||
isEmpty,
|
||||
);
|
||||
now = now.add(totpPeriod);
|
||||
await remote.removeAuthenticator(
|
||||
session,
|
||||
person: annaOnTodo,
|
||||
authenticator: spare.authenticator,
|
||||
code: codeOf(enrolment),
|
||||
);
|
||||
final ways = await remote.signInWaysOf(session, annaOnTodo);
|
||||
expect(ways.password && ways.authenticator, isTrue);
|
||||
|
|
|
|||
|
|
@ -931,6 +931,7 @@ class _AuthenticatorEndpoint {
|
|||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required _is.UuidValue authenticator,
|
||||
required String code,
|
||||
String? existingCode,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
|
|
@ -946,6 +947,7 @@ class _AuthenticatorEndpoint {
|
|||
parameters: _ist.testObjectToJson({
|
||||
'authenticator': authenticator,
|
||||
'code': code,
|
||||
'existingCode': existingCode,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue