feat(auth): a password with TOTP on the register, and resets
The register's side of signing in with an address, a password that never leaves the device (SRP-6a) and a code from an authenticator (ADR 0017), with members forwarding every step through AccountRegister: - password.set: salt, Argon2id's costs, SRP's verifier, the public keys and the private keys wrapped twice, all made on the device; with the address a mailed code proves, an anonymous person becomes identified under their ID. It signs in once an authenticator is confirmed. - passwordSignIn.begin and complete: SRP's two steps with a TOTP or recovery code. An address nobody has is answered from a decoy key as an account would be; every wrong credential is refused alike. - authenticator: secrets sealed under totpKey (XChaCha20-Poly1305, bound to their row), each code accepted once, ten recovery codes with the first, the last one kept. - accountRecovery: a lost authenticator reset 72 hours after the password and a mailed code or the recovery phrase ask, a lost password set anew with the phrase's signature, or the account reset with new keys 72 hours after a mailed code asks; a mail at each start, a sign-in calling it off. - An account with a password no longer signs in by a mailed code or GitHub; no new GitHub account is linked where passwords are offered, and from GITHUB_SIGN_IN_ENDS GitHub signs nobody in and its IDs go. Five tables, added by the module's migration; deletePerson deletes their rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
200a2df32f
commit
a3a8ed3890
2 changed files with 22557 additions and 60 deletions
|
|
@ -24,6 +24,15 @@ final class _Mailbox implements CodeMailer {
|
|||
sent.add((to: to, code: code, language: language));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> sendResetNotice(
|
||||
Session session, {
|
||||
required String to,
|
||||
required AccountResetKind kind,
|
||||
required DateTime takesEffectAt,
|
||||
required String language,
|
||||
}) async {}
|
||||
|
||||
/// The last code mailed to [address].
|
||||
String codeFor(String address) =>
|
||||
sent.lastWhere((mail) => mail.to == address).code;
|
||||
|
|
@ -38,6 +47,15 @@ final class _Refusing implements CodeMailer {
|
|||
required String code,
|
||||
required String language,
|
||||
}) async => throw const SocketException('Connection refused');
|
||||
|
||||
@override
|
||||
Future<void> sendResetNotice(
|
||||
Session session, {
|
||||
required String to,
|
||||
required AccountResetKind kind,
|
||||
required DateTime takesEffectAt,
|
||||
required String language,
|
||||
}) async => throw const SocketException('Connection refused');
|
||||
}
|
||||
|
||||
void main() {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:halkyon_base_server/base.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../support/fixtures.dart';
|
||||
import '../support/password_device.dart';
|
||||
import '../test_tools/serverpod_test_tools.dart';
|
||||
|
||||
void main() {
|
||||
|
|
@ -169,6 +171,71 @@ void main() {
|
|||
|
||||
expect(info.gitHubClientId, isNull);
|
||||
});
|
||||
|
||||
group('deprecated (ADR 0017)', () {
|
||||
tearDown(() {
|
||||
usePasswordKeys(session.serverpod, null);
|
||||
EmailCodes.of(session.serverpod).mailer = null;
|
||||
useGitHubSignInEnds(session.serverpod, null);
|
||||
});
|
||||
|
||||
test('links no new GitHub account where people sign in with a '
|
||||
'password, and still signs in those who have one', () async {
|
||||
usePasswordKeys(session.serverpod, testPasswordKeys);
|
||||
EmailCodes.of(session.serverpod).mailer = const LogCodeMailer();
|
||||
|
||||
await expectLater(
|
||||
linkGitHubAccount(session, authUserId: anna, gitHubUserId: '2002'),
|
||||
throwsA(_refusal(GitHubSignInRefusal.deprecated)),
|
||||
);
|
||||
expect(await _isAnonymous(session, anna), isTrue);
|
||||
expect(await _gitHubAccount(session, '2002'), isNull);
|
||||
await linkGitHubAccount(session, authUserId: ben, gitHubUserId: '1001');
|
||||
expect(await gitHubAccountToSignIn(session, '1001'), ben);
|
||||
});
|
||||
|
||||
test('signs nobody in and links nothing from the day it ends, after '
|
||||
'which pruning forgets every GitHub ID', () async {
|
||||
final ends = DateTime.utc(2027, 1, 22);
|
||||
useGitHubSignInEnds(session.serverpod, ends);
|
||||
|
||||
expect(
|
||||
await forgetGitHubAccounts(
|
||||
session,
|
||||
now: ends.subtract(const Duration(seconds: 1)),
|
||||
),
|
||||
0,
|
||||
);
|
||||
await withClock(Clock.fixed(ends), () async {
|
||||
await expectLater(
|
||||
endpoints.gitHubSignIn.signIn(
|
||||
notSignedIn(),
|
||||
code: 'code',
|
||||
codeVerifier: 'verifier',
|
||||
redirectUri: _redirectUri,
|
||||
),
|
||||
throwsA(_refusal(GitHubSignInRefusal.ended)),
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.gitHubUpgrade.upgrade(
|
||||
sessionBuilder.copyWith(
|
||||
authentication: AuthenticationOverride.authenticationInfo(
|
||||
anna.uuid,
|
||||
{},
|
||||
authId: annasPhone.uuid,
|
||||
),
|
||||
),
|
||||
code: 'code',
|
||||
codeVerifier: 'verifier',
|
||||
redirectUri: _redirectUri,
|
||||
),
|
||||
throwsA(_refusal(GitHubSignInRefusal.ended)),
|
||||
);
|
||||
});
|
||||
expect(await forgetGitHubAccounts(session, now: ends), 1);
|
||||
expect(await GitHubAccount.db.count(session), 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue