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
10 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,15 @@ final class _NoMail implements CodeMailer {
|
|||
required String code,
|
||||
required String language,
|
||||
}) async {}
|
||||
|
||||
@override
|
||||
Future<void> sendResetNotice(
|
||||
Session session, {
|
||||
required String to,
|
||||
required AccountResetKind kind,
|
||||
required DateTime takesEffectAt,
|
||||
required String language,
|
||||
}) async {}
|
||||
}
|
||||
|
||||
void main() {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -11,6 +11,7 @@ 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';
|
||||
|
||||
const _secret = 'a shared secret only the instance knows';
|
||||
|
|
@ -35,6 +36,16 @@ final class _Register implements AccountRegister {
|
|||
final askedAbout = <Set<UuidValue>>[];
|
||||
bool offered = true;
|
||||
|
||||
/// Whether it offers password sign-in.
|
||||
bool passwordOffered = true;
|
||||
|
||||
/// The accounts with a password, and those with an authenticator too.
|
||||
final passwords = <UuidValue>{};
|
||||
final authenticated = <UuidValue>{};
|
||||
|
||||
/// What a password sign-in proves; null refuses it.
|
||||
PasswordProof? proof;
|
||||
|
||||
/// Thrown by every call while set: the register refusing, or down.
|
||||
Exception? failing;
|
||||
|
||||
|
|
@ -206,9 +217,168 @@ final class _Register implements AccountRegister {
|
|||
() => SignInWays(
|
||||
gitHub: gitHub.containsValue(person),
|
||||
email: addresses.containsValue(person),
|
||||
password: passwords.contains(person),
|
||||
authenticator: authenticated.contains(person),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<bool> passwordSignInOffered(Session session) =>
|
||||
_answer('passwordSignInOffered', () => passwordOffered);
|
||||
|
||||
@override
|
||||
Future<void> setPassword(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required PasswordSetup setup,
|
||||
UuidValue? request,
|
||||
String? code,
|
||||
}) => _answer('setPassword', () {
|
||||
add(person);
|
||||
passwords.add(person);
|
||||
});
|
||||
|
||||
@override
|
||||
Future<PasswordChallenge> beginPasswordSignIn(
|
||||
Session session, {
|
||||
required String address,
|
||||
required String network,
|
||||
}) => _answer('beginPasswordSignIn', () {
|
||||
networks.add(network);
|
||||
return PasswordChallenge(
|
||||
challenge: const Uuid().v7obj(),
|
||||
salt: ByteData(16),
|
||||
costs: newPasswordCosts,
|
||||
serverValue: ByteData(384),
|
||||
);
|
||||
});
|
||||
|
||||
PasswordProof _proven() =>
|
||||
proof ??
|
||||
(throw PasswordRefusedException(
|
||||
reason: PasswordRefusal.wrongCredentials,
|
||||
));
|
||||
|
||||
@override
|
||||
Future<PasswordProof> completePasswordSignIn(
|
||||
Session session, {
|
||||
required UuidValue challenge,
|
||||
required ByteData clientValue,
|
||||
required ByteData clientProof,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
}) => _answer('completePasswordSignIn', _proven);
|
||||
|
||||
@override
|
||||
Future<AuthenticatorEnrolment> enrolAuthenticator(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required String name,
|
||||
required String label,
|
||||
}) => _answer(
|
||||
'enrolAuthenticator',
|
||||
() => AuthenticatorEnrolment(
|
||||
authenticator: const Uuid().v7obj(),
|
||||
uri: 'otpauth://totp/learn.test:$label',
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<List<String>> confirmAuthenticator(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required UuidValue authenticator,
|
||||
required String code,
|
||||
}) => _answer('confirmAuthenticator', () {
|
||||
authenticated.add(person);
|
||||
return const ['K7QX-M2PD'];
|
||||
});
|
||||
|
||||
@override
|
||||
Future<List<Authenticator>> authenticatorsOf(
|
||||
Session session,
|
||||
UuidValue person,
|
||||
) => _answer('authenticatorsOf', () => const <Authenticator>[]);
|
||||
|
||||
@override
|
||||
Future<void> removeAuthenticator(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required UuidValue authenticator,
|
||||
String? code,
|
||||
}) => _answer('removeAuthenticator', () {});
|
||||
|
||||
@override
|
||||
Future<List<String>> newRecoveryCodes(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required String code,
|
||||
}) => _answer('newRecoveryCodes', () => const ['K7QX-M2PD']);
|
||||
|
||||
@override
|
||||
Future<ResetPending> resetAuthenticator(
|
||||
Session session, {
|
||||
required UuidValue challenge,
|
||||
required ByteData clientValue,
|
||||
required ByteData clientProof,
|
||||
required String address,
|
||||
required String language,
|
||||
UuidValue? request,
|
||||
String? code,
|
||||
UuidValue? keyChallenge,
|
||||
ByteData? signature,
|
||||
}) => _answer(
|
||||
'resetAuthenticator',
|
||||
() => ResetPending(takesEffectAt: DateTime.utc(2026, 10, 23)),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<KeyProofChallenge> beginKeyProof(
|
||||
Session session, {
|
||||
required String address,
|
||||
required String network,
|
||||
}) => _answer('beginKeyProof', () {
|
||||
networks.add(network);
|
||||
return KeyProofChallenge(
|
||||
challenge: const Uuid().v7obj(),
|
||||
nonce: ByteData(32),
|
||||
encryptionPublicKey: ByteData(32),
|
||||
signingPublicKey: ByteData(32),
|
||||
recoveryKeys: WrappedKeys(nonce: ByteData(24), cipherText: ByteData(112)),
|
||||
);
|
||||
});
|
||||
|
||||
@override
|
||||
Future<UuidValue> resetPasswordWithPhrase(
|
||||
Session session, {
|
||||
required UuidValue challenge,
|
||||
required ByteData signature,
|
||||
required NewPassword password,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
}) => _answer('resetPasswordWithPhrase', () => _proven().account);
|
||||
|
||||
@override
|
||||
Future<ResetPending> startAccountReset(
|
||||
Session session, {
|
||||
required String address,
|
||||
required String language,
|
||||
required UuidValue request,
|
||||
required String code,
|
||||
}) => _answer(
|
||||
'startAccountReset',
|
||||
() => ResetPending(takesEffectAt: DateTime.utc(2026, 10, 23)),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<UuidValue> completeAccountReset(
|
||||
Session session, {
|
||||
required String address,
|
||||
required UuidValue request,
|
||||
required String code,
|
||||
required PasswordSetup setup,
|
||||
}) => _answer('completeAccountReset', () => _proven().account);
|
||||
|
||||
@override
|
||||
Future<void> deleteAccount(Session session, UuidValue person) =>
|
||||
_answer('deleteAccount', () {
|
||||
|
|
@ -491,6 +661,135 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
group('passwords, by asking the register (ADR 0017)', () {
|
||||
test('sign the account the register proved in here, made a person '
|
||||
'here the first time, the device network passed on', () async {
|
||||
final carla = const Uuid().v7obj();
|
||||
register.proof = PasswordProof(
|
||||
account: carla,
|
||||
serverProof: ByteData(32),
|
||||
encryptionPublicKey: ByteData(32),
|
||||
signingPublicKey: ByteData(32),
|
||||
passwordKeys: WrappedKeys(
|
||||
nonce: ByteData(24),
|
||||
cipherText: ByteData(112),
|
||||
),
|
||||
needsAuthenticator: false,
|
||||
);
|
||||
|
||||
final challenge = await endpoints.passwordSignIn.begin(
|
||||
notSignedIn(),
|
||||
address: 'carla@example.org',
|
||||
);
|
||||
final signedIn = await endpoints.passwordSignIn.complete(
|
||||
notSignedIn(),
|
||||
challenge: challenge.challenge,
|
||||
clientValue: ByteData(384),
|
||||
clientProof: ByteData(32),
|
||||
totpCode: '123456',
|
||||
);
|
||||
|
||||
expect(register.asked, [
|
||||
'beginPasswordSignIn',
|
||||
'completePasswordSignIn',
|
||||
]);
|
||||
expect(register.networks, hasLength(1));
|
||||
expect(signedIn.authUserId, carla);
|
||||
expect(await scopesOf(carla), {identified.name});
|
||||
expect(
|
||||
(await ServerSideSession.db.find(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(carla),
|
||||
)).single.method,
|
||||
passwordMethod,
|
||||
);
|
||||
});
|
||||
|
||||
test('refuse as the register refuses, signing nobody in', () async {
|
||||
final people = await AuthUser.db.count(session);
|
||||
|
||||
await expectLater(
|
||||
endpoints.passwordSignIn.complete(
|
||||
notSignedIn(),
|
||||
challenge: const Uuid().v7obj(),
|
||||
clientValue: ByteData(384),
|
||||
clientProof: ByteData(32),
|
||||
),
|
||||
throwsA(
|
||||
isA<PasswordRefusedException>().having(
|
||||
(error) => error.reason,
|
||||
'reason',
|
||||
PasswordRefusal.wrongCredentials,
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(await AuthUser.db.count(session), people);
|
||||
});
|
||||
|
||||
test('set one at the register, the person becoming identified '
|
||||
'here', () async {
|
||||
final device = await PasswordDevice.make();
|
||||
|
||||
await endpoints.password.set(
|
||||
asAnna(),
|
||||
setup: device.setup(),
|
||||
request: const Uuid().v7obj(),
|
||||
code: 'K7QX-M2PD',
|
||||
);
|
||||
|
||||
expect(register.passwords, {anna});
|
||||
expect(await scopesOf(anna), contains(identified.name));
|
||||
expect(
|
||||
await AnonymousAccount.db.count(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(anna),
|
||||
),
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
test('are offered as the register offers them, and not while it is '
|
||||
'out of reach', () async {
|
||||
expect(
|
||||
(await endpoints.instance.basics(notSignedIn())).passwordSignIn,
|
||||
isTrue,
|
||||
);
|
||||
register.passwordOffered = false;
|
||||
expect(
|
||||
(await endpoints.instance.basics(notSignedIn())).passwordSignIn,
|
||||
isFalse,
|
||||
);
|
||||
register
|
||||
..passwordOffered = true
|
||||
..failing = RegisterUnavailableException('test');
|
||||
expect(
|
||||
(await endpoints.instance.basics(notSignedIn())).passwordSignIn,
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('keep GitHub from signing into an account that has one, as '
|
||||
'the register knows it', () async {
|
||||
register.gitHub['1001'] = ben;
|
||||
expect(await gitHubAccountToSignIn(session, '1001'), ben);
|
||||
|
||||
register
|
||||
..passwords.add(ben)
|
||||
..authenticated.add(ben);
|
||||
|
||||
await expectLater(
|
||||
gitHubAccountToSignIn(session, '1001'),
|
||||
throwsA(
|
||||
isA<GitHubSignInRefusedException>().having(
|
||||
(error) => error.reason,
|
||||
'reason',
|
||||
GitHubSignInRefusal.usePassword,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('profiles', () {
|
||||
test('are saved and found at the register', () async {
|
||||
final saved = await endpoints.profile.save(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:halkyon_base_server/base.dart';
|
||||
|
|
@ -13,6 +14,7 @@ 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';
|
||||
|
||||
const _secret = 'a shared secret only the instance knows';
|
||||
|
|
@ -31,6 +33,15 @@ final class _Mailbox implements CodeMailer {
|
|||
required String language,
|
||||
}) async => sent.add((to: to, code: code));
|
||||
|
||||
@override
|
||||
Future<void> sendResetNotice(
|
||||
Session session, {
|
||||
required String to,
|
||||
required AccountResetKind kind,
|
||||
required DateTime takesEffectAt,
|
||||
required String language,
|
||||
}) async {}
|
||||
|
||||
String codeFor(String address) =>
|
||||
sent.lastWhere((mail) => mail.to == address).code;
|
||||
}
|
||||
|
|
@ -442,6 +453,216 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
group('passwords (ADR 0017)', () {
|
||||
late DateTime now;
|
||||
Future<T> atNow<T>(Future<T> Function() body) =>
|
||||
withClock(Clock(() => now), body);
|
||||
|
||||
setUp(() {
|
||||
usePasswordKeys(pod, testPasswordKeys);
|
||||
now = DateTime.utc(2026, 10, 20, 12);
|
||||
});
|
||||
|
||||
test('are offered as the register offers them', () async {
|
||||
expect(await remote.passwordSignInOffered(session), isTrue);
|
||||
usePasswordKeys(pod, null);
|
||||
expect(await remote.passwordSignInOffered(session), isFalse);
|
||||
await expectLater(
|
||||
remote.beginPasswordSignIn(
|
||||
session,
|
||||
address: 'ben@example.org',
|
||||
network: '203.0.113.7',
|
||||
),
|
||||
refused(PasswordRefusal.unavailable),
|
||||
);
|
||||
});
|
||||
|
||||
test("cross for a member's device, every operation, with what the "
|
||||
'register refuses', () async {
|
||||
final device = await PasswordDevice.make();
|
||||
await atNow(() async {
|
||||
final annaOnTodo = const Uuid().v7obj();
|
||||
final request = await remote.sendSignInCode(
|
||||
session,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
network: '203.0.113.7',
|
||||
);
|
||||
await remote.setPassword(
|
||||
session,
|
||||
person: annaOnTodo,
|
||||
setup: device.setup(),
|
||||
request: request,
|
||||
code: mailbox.codeFor('anna@example.org'),
|
||||
);
|
||||
expect(
|
||||
(await AuthUser.db.findById(session, annaOnTodo))!.scopeNames,
|
||||
contains(identified.name),
|
||||
);
|
||||
await expectLater(
|
||||
remote.setPassword(
|
||||
session,
|
||||
person: ben,
|
||||
setup: device.setup().copyWith(salt: ByteData(3)),
|
||||
),
|
||||
refused(PasswordRefusal.invalidSetup),
|
||||
);
|
||||
|
||||
final enrolment = await remote.enrolAuthenticator(
|
||||
session,
|
||||
person: annaOnTodo,
|
||||
name: 'Phone',
|
||||
label: 'anna@example.org',
|
||||
);
|
||||
final codes = await remote.confirmAuthenticator(
|
||||
session,
|
||||
person: annaOnTodo,
|
||||
authenticator: enrolment.authenticator,
|
||||
code: codeOf(enrolment),
|
||||
);
|
||||
expect(codes, hasLength(recoveryCodeCount));
|
||||
expect(
|
||||
(await remote.authenticatorsOf(session, annaOnTodo)).single.id,
|
||||
enrolment.authenticator,
|
||||
);
|
||||
final spare = await remote.enrolAuthenticator(
|
||||
session,
|
||||
person: annaOnTodo,
|
||||
name: 'Spare',
|
||||
label: '',
|
||||
);
|
||||
await remote.removeAuthenticator(
|
||||
session,
|
||||
person: annaOnTodo,
|
||||
authenticator: spare.authenticator,
|
||||
);
|
||||
final ways = await remote.signInWaysOf(session, annaOnTodo);
|
||||
expect(ways.password && ways.authenticator, isTrue);
|
||||
|
||||
now = now.add(totpPeriod);
|
||||
final challenge = await remote.beginPasswordSignIn(
|
||||
session,
|
||||
address: 'anna@example.org',
|
||||
network: '203.0.113.7',
|
||||
);
|
||||
final answer = device.answer(challenge);
|
||||
final proof = await remote.completePasswordSignIn(
|
||||
session,
|
||||
challenge: challenge.challenge,
|
||||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: codeOf(enrolment, at: now),
|
||||
);
|
||||
expect(proof.account, annaOnTodo);
|
||||
expect(answer.verifiesServer(proof.serverProof), isTrue);
|
||||
expect(proof.passwordKeys.toJson(), device.passwordKeys.toJson());
|
||||
final again = await remote.beginPasswordSignIn(
|
||||
session,
|
||||
address: 'anna@example.org',
|
||||
network: '203.0.113.7',
|
||||
);
|
||||
final againAnswer = device.answer(again);
|
||||
await expectLater(
|
||||
remote.completePasswordSignIn(
|
||||
session,
|
||||
challenge: again.challenge,
|
||||
clientValue: againAnswer.clientValue,
|
||||
clientProof: againAnswer.clientProof,
|
||||
totpCode: codeOf(enrolment, at: now),
|
||||
),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
|
||||
now = now.add(totpPeriod);
|
||||
expect(
|
||||
await remote.newRecoveryCodes(
|
||||
session,
|
||||
person: annaOnTodo,
|
||||
code: codeOf(enrolment, at: now),
|
||||
),
|
||||
hasLength(recoveryCodeCount),
|
||||
);
|
||||
|
||||
final keyChallenge = await remote.beginKeyProof(
|
||||
session,
|
||||
address: 'anna@example.org',
|
||||
network: '203.0.113.7',
|
||||
);
|
||||
expect(
|
||||
keyChallenge.recoveryKeys.toJson(),
|
||||
device.recoveryKeys.toJson(),
|
||||
);
|
||||
now = now.add(totpPeriod);
|
||||
expect(
|
||||
await remote.resetPasswordWithPhrase(
|
||||
session,
|
||||
challenge: keyChallenge.challenge,
|
||||
signature: await device.sign(keyChallenge),
|
||||
password: device.newPassword(),
|
||||
totpCode: codeOf(enrolment, at: now),
|
||||
),
|
||||
annaOnTodo,
|
||||
);
|
||||
|
||||
final forReset = await remote.beginPasswordSignIn(
|
||||
session,
|
||||
address: 'anna@example.org',
|
||||
network: '203.0.113.7',
|
||||
);
|
||||
final resetAnswer = device.answer(forReset);
|
||||
final resetRequest = await remote.sendSignInCode(
|
||||
session,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
network: '203.0.113.7',
|
||||
);
|
||||
final pending = await remote.resetAuthenticator(
|
||||
session,
|
||||
challenge: forReset.challenge,
|
||||
clientValue: resetAnswer.clientValue,
|
||||
clientProof: resetAnswer.clientProof,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
request: resetRequest,
|
||||
code: mailbox.codeFor('anna@example.org'),
|
||||
);
|
||||
expect(pending.takesEffectAt, now.add(resetDelay));
|
||||
|
||||
final startRequest = await remote.sendSignInCode(
|
||||
session,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
network: '203.0.113.7',
|
||||
);
|
||||
await remote.startAccountReset(
|
||||
session,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
request: startRequest,
|
||||
code: mailbox.codeFor('anna@example.org'),
|
||||
);
|
||||
now = now.add(resetDelay);
|
||||
final fresh = await PasswordDevice.make(seed: 99);
|
||||
final completeRequest = await remote.sendSignInCode(
|
||||
session,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
network: '203.0.113.7',
|
||||
);
|
||||
expect(
|
||||
await remote.completeAccountReset(
|
||||
session,
|
||||
address: 'anna@example.org',
|
||||
request: completeRequest,
|
||||
code: mailbox.codeFor('anna@example.org'),
|
||||
setup: fresh.setup(),
|
||||
),
|
||||
annaOnTodo,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('deleting an account', () {
|
||||
test('deletes it here and tells every member, keeping no notice '
|
||||
'once they confirmed', () async {
|
||||
|
|
@ -660,6 +881,17 @@ final class _NoSession implements Session {
|
|||
}
|
||||
|
||||
Future<void> _deleteAll(Session session) async {
|
||||
await SignInChallenge.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.id.notEquals(null),
|
||||
);
|
||||
await AccountReset.db.deleteWhere(session, where: (t) => t.id > 0);
|
||||
await RecoveryCode.db.deleteWhere(session, where: (t) => t.id > 0);
|
||||
await TotpAuthenticator.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.id.notEquals(null),
|
||||
);
|
||||
await AccountPassword.db.deleteWhere(session, where: (t) => t.id > 0);
|
||||
await AccountDeletionNotice.db.deleteWhere(session, where: (t) => t.id > 0);
|
||||
await PublicProfile.db.deleteWhere(session, where: (t) => t.id > 0);
|
||||
await RateLimitedRequestAttempt.db.deleteWhere(
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ void useBase(
|
|||
tokenManagerBuilders: tokenManagerBuilders,
|
||||
identityProviderBuilders: identityProviderBuilders(pod),
|
||||
);
|
||||
// No password sign-in unless a test gives it keys, whatever the test
|
||||
// run's passwords hold.
|
||||
usePasswordKeys(pod, null);
|
||||
}
|
||||
|
||||
/// Signs [person] in on a new device and returns the new session's id.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
import 'package:halkyon_base_server/base.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
|
||||
/// Keeps every code and notice mailed, in place of a mail service.
|
||||
final class Mailbox implements CodeMailer {
|
||||
final codes = <({String to, String code, String language})>[];
|
||||
final notices =
|
||||
<
|
||||
({
|
||||
String to,
|
||||
AccountResetKind kind,
|
||||
DateTime takesEffectAt,
|
||||
String language,
|
||||
})
|
||||
>[];
|
||||
|
||||
/// Whether it takes no mail, as a mail service down.
|
||||
bool refusing = false;
|
||||
|
||||
@override
|
||||
Future<void> send(
|
||||
Session session, {
|
||||
required String to,
|
||||
required String code,
|
||||
required String language,
|
||||
}) async {
|
||||
if (refusing) {
|
||||
throw Exception('Connection refused');
|
||||
}
|
||||
codes.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 {
|
||||
if (refusing) {
|
||||
throw Exception('Connection refused');
|
||||
}
|
||||
notices.add((
|
||||
to: to,
|
||||
kind: kind,
|
||||
takesEffectAt: takesEffectAt,
|
||||
language: language,
|
||||
));
|
||||
}
|
||||
|
||||
/// The last code mailed to [address].
|
||||
String codeFor(String address) =>
|
||||
codes.lastWhere((mail) => mail.to == address).code;
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:cryptography/cryptography.dart';
|
||||
import 'package:halkyon_base_client/srp.dart' as srp;
|
||||
import 'package:halkyon_base_server/base.dart';
|
||||
import 'package:halkyon_base_server/src/srp/srp_math.dart';
|
||||
|
||||
/// The instance keys the tests' register holds (ADR 0017).
|
||||
const testPasswordKeys = (
|
||||
totpKey: 'a TOTP key of the tests, 32 characters at least',
|
||||
recoveryCodeKey: 'a recovery code key of the tests, long enough',
|
||||
decoyKey: 'a decoy key of the tests, also long enough here',
|
||||
);
|
||||
|
||||
/// A device with an account's password, as halkyon_keys makes one, without
|
||||
/// libsodium: SRP's x and the account's keys random, the signing key a real
|
||||
/// Ed25519 pair, so it can prove the recovery phrase. What the register
|
||||
/// checks is all here; what only a device opens (the wrapped keys) is
|
||||
/// random bytes of the right length.
|
||||
final class PasswordDevice {
|
||||
PasswordDevice._(this._random, this.signingKeys, this.signingPublicKey)
|
||||
: salt = _bytes(_random, 16),
|
||||
privateValue = numberOf(_bytes(_random, 32)),
|
||||
encryptionPublicKey = _bytes(_random, 32);
|
||||
|
||||
/// A new device's password, from [seed].
|
||||
static Future<PasswordDevice> make({int seed = 17}) async {
|
||||
final random = Random(seed);
|
||||
final keys = await Ed25519().newKeyPairFromSeed(_bytes(random, 32));
|
||||
final public = await keys.extractPublicKey();
|
||||
return PasswordDevice._(random, keys, Uint8List.fromList(public.bytes));
|
||||
}
|
||||
|
||||
final Random _random;
|
||||
|
||||
/// The account's signing key pair, which the recovery phrase opens.
|
||||
final SimpleKeyPair signingKeys;
|
||||
final Uint8List signingPublicKey;
|
||||
final Uint8List encryptionPublicKey;
|
||||
|
||||
/// The password's salt and x, as Argon2id would make them.
|
||||
Uint8List salt;
|
||||
BigInt privateValue;
|
||||
|
||||
final _clientGroup = srp.SrpGroup.rfc5054Group3072;
|
||||
|
||||
/// v = g^x mod N, as long as N.
|
||||
ByteData get verifier => ByteData.sublistView(
|
||||
padded(
|
||||
srp.srpVerifier(_clientGroup, privateValue),
|
||||
passwordGroup.length,
|
||||
),
|
||||
);
|
||||
|
||||
WrappedKeys _wrapped() => WrappedKeys(
|
||||
nonce: ByteData.sublistView(_bytes(_random, 24)),
|
||||
cipherText: ByteData.sublistView(_bytes(_random, wrappedKeysLength)),
|
||||
);
|
||||
|
||||
/// The password's keys as the setup wrapped them.
|
||||
late final WrappedKeys passwordKeys = _wrapped();
|
||||
late final WrappedKeys recoveryKeys = _wrapped();
|
||||
|
||||
/// The setup the device sends for this password.
|
||||
PasswordSetup setup() => PasswordSetup(
|
||||
salt: ByteData.sublistView(salt),
|
||||
costs: newPasswordCosts,
|
||||
verifier: verifier,
|
||||
encryptionPublicKey: ByteData.sublistView(encryptionPublicKey),
|
||||
signingPublicKey: ByteData.sublistView(signingPublicKey),
|
||||
passwordKeys: passwordKeys,
|
||||
recoveryKeys: recoveryKeys,
|
||||
);
|
||||
|
||||
/// A new password for the same keys: the device's salt and x change.
|
||||
NewPassword newPassword() {
|
||||
salt = _bytes(_random, 16);
|
||||
privateValue = numberOf(_bytes(_random, 32));
|
||||
return NewPassword(
|
||||
salt: ByteData.sublistView(salt),
|
||||
costs: newPasswordCosts,
|
||||
verifier: verifier,
|
||||
passwordKeys: _wrapped(),
|
||||
);
|
||||
}
|
||||
|
||||
/// The device's answer to [challenge] (A and M1), with its password or
|
||||
/// the [privateValue] of another; and whether a register's M2 is the one
|
||||
/// that answer expects.
|
||||
({
|
||||
ByteData clientValue,
|
||||
ByteData clientProof,
|
||||
bool Function(ByteData serverProof) verifiesServer,
|
||||
})
|
||||
answer(PasswordChallenge challenge, {BigInt? privateValue}) {
|
||||
final client = srp.SrpClient(_clientGroup, random: _random);
|
||||
final step = client.respond(
|
||||
identity: challenge.challenge.toBytes(),
|
||||
salt: Uint8List.sublistView(challenge.salt),
|
||||
privateValue: privateValue ?? this.privateValue,
|
||||
serverValue: numberOf(Uint8List.sublistView(challenge.serverValue)),
|
||||
);
|
||||
final proof = step as srp.SrpClientProof;
|
||||
return (
|
||||
clientValue: ByteData.sublistView(
|
||||
padded(client.publicValue, passwordGroup.length),
|
||||
),
|
||||
clientProof: ByteData.sublistView(proof.proof),
|
||||
verifiesServer: (serverProof) =>
|
||||
proof.verifiesServer(Uint8List.sublistView(serverProof)),
|
||||
);
|
||||
}
|
||||
|
||||
/// The signature proving the recovery phrase for [challenge]: the
|
||||
/// account's own, or [keys]' where given.
|
||||
Future<ByteData> sign(
|
||||
KeyProofChallenge challenge, {
|
||||
SimpleKeyPair? keys,
|
||||
}) async {
|
||||
final signature = await Ed25519().sign(
|
||||
keyProofMessage(
|
||||
challenge.challenge,
|
||||
Uint8List.sublistView(challenge.nonce),
|
||||
),
|
||||
keyPair: keys ?? signingKeys,
|
||||
);
|
||||
return ByteData.sublistView(Uint8List.fromList(signature.bytes));
|
||||
}
|
||||
}
|
||||
|
||||
/// Another signing key pair than any account's.
|
||||
Future<SimpleKeyPair> otherSigningKeys() =>
|
||||
Ed25519().newKeyPairFromSeed(List.filled(32, 7));
|
||||
|
||||
/// The code [enrolment]'s authenticator shows at [at] (now by default).
|
||||
String codeOf(AuthenticatorEnrolment enrolment, {DateTime? at}) => totpCode(
|
||||
base32Decode(Uri.parse(enrolment.uri).queryParameters['secret']!),
|
||||
step: totpStepAt(at ?? clock.now()),
|
||||
);
|
||||
|
||||
Uint8List _bytes(Random random, int length) =>
|
||||
Uint8List.fromList([for (var i = 0; i < length; i++) random.nextInt(256)]);
|
||||
|
|
@ -20,6 +20,22 @@ import 'package:halkyon_base_server/src/generated/auth/sign_in_ways.dart'
|
|||
as _ik4ah9vo;
|
||||
import 'package:halkyon_base_server/src/generated/instance/instance_basics.dart'
|
||||
as _ihusd8tw;
|
||||
import 'package:halkyon_base_server/src/generated/password/authenticator.dart'
|
||||
as _ip34xal6;
|
||||
import 'package:halkyon_base_server/src/generated/password/authenticator_enrolment.dart'
|
||||
as _id229ey1;
|
||||
import 'package:halkyon_base_server/src/generated/password/key_proof_challenge.dart'
|
||||
as _iaysae70;
|
||||
import 'package:halkyon_base_server/src/generated/password/new_password.dart'
|
||||
as _ijye8mcv;
|
||||
import 'package:halkyon_base_server/src/generated/password/password_challenge.dart'
|
||||
as _isz0atlx;
|
||||
import 'package:halkyon_base_server/src/generated/password/password_setup.dart'
|
||||
as _ikhyee47;
|
||||
import 'package:halkyon_base_server/src/generated/password/password_signed_in.dart'
|
||||
as _ikpyb5fh;
|
||||
import 'package:halkyon_base_server/src/generated/password/reset_pending.dart'
|
||||
as _isakxto6;
|
||||
import 'package:halkyon_base_server/src/generated/profile/profile.dart'
|
||||
as _it4tom37;
|
||||
import 'package:halkyon_base_server/src/generated/profile/profile_refusal.dart'
|
||||
|
|
@ -175,6 +191,14 @@ class TestEndpoints {
|
|||
|
||||
late final _InstanceEndpoint instance;
|
||||
|
||||
late final _AccountRecoveryEndpoint accountRecovery;
|
||||
|
||||
late final _AuthenticatorEndpoint authenticator;
|
||||
|
||||
late final _PasswordEndpoint password;
|
||||
|
||||
late final _PasswordSignInEndpoint passwordSignIn;
|
||||
|
||||
late final _ProfileEndpoint profile;
|
||||
|
||||
late final _ProfilesEndpoint profiles;
|
||||
|
|
@ -215,6 +239,22 @@ class _InternalTestEndpoints extends TestEndpoints
|
|||
endpoints,
|
||||
serializationManager,
|
||||
);
|
||||
accountRecovery = _AccountRecoveryEndpoint(
|
||||
endpoints,
|
||||
serializationManager,
|
||||
);
|
||||
authenticator = _AuthenticatorEndpoint(
|
||||
endpoints,
|
||||
serializationManager,
|
||||
);
|
||||
password = _PasswordEndpoint(
|
||||
endpoints,
|
||||
serializationManager,
|
||||
);
|
||||
passwordSignIn = _PasswordSignInEndpoint(
|
||||
endpoints,
|
||||
serializationManager,
|
||||
);
|
||||
profile = _ProfileEndpoint(
|
||||
endpoints,
|
||||
serializationManager,
|
||||
|
|
@ -597,6 +637,559 @@ class _InstanceEndpoint {
|
|||
}
|
||||
}
|
||||
|
||||
class _AccountRecoveryEndpoint {
|
||||
_AccountRecoveryEndpoint(
|
||||
this._endpointDispatch,
|
||||
this._serializationManager,
|
||||
);
|
||||
|
||||
final _is.EndpointDispatch _endpointDispatch;
|
||||
|
||||
final _is.SerializationManager _serializationManager;
|
||||
|
||||
_ida.Future<_is.UuidValue> sendCode(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required String address,
|
||||
required String language,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'accountRecovery',
|
||||
method: 'sendCode',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'accountRecovery',
|
||||
methodName: 'sendCode',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'address': address,
|
||||
'language': language,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_is.UuidValue>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<_isakxto6.ResetPending> resetAuthenticator(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required _is.UuidValue challenge,
|
||||
required _idt.ByteData clientValue,
|
||||
required _idt.ByteData clientProof,
|
||||
required String address,
|
||||
required String language,
|
||||
_is.UuidValue? request,
|
||||
String? code,
|
||||
_is.UuidValue? keyChallenge,
|
||||
_idt.ByteData? signature,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'accountRecovery',
|
||||
method: 'resetAuthenticator',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'accountRecovery',
|
||||
methodName: 'resetAuthenticator',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'challenge': challenge,
|
||||
'clientValue': clientValue,
|
||||
'clientProof': clientProof,
|
||||
'address': address,
|
||||
'language': language,
|
||||
'request': request,
|
||||
'code': code,
|
||||
'keyChallenge': keyChallenge,
|
||||
'signature': signature,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_isakxto6.ResetPending>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<_iaysae70.KeyProofChallenge> beginKeyProof(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required String address,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'accountRecovery',
|
||||
method: 'beginKeyProof',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'accountRecovery',
|
||||
methodName: 'beginKeyProof',
|
||||
parameters: _ist.testObjectToJson({'address': address}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_iaysae70.KeyProofChallenge>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<_iacs.AuthSuccess> resetPassword(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required _is.UuidValue challenge,
|
||||
required _idt.ByteData signature,
|
||||
required _ijye8mcv.NewPassword password,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'accountRecovery',
|
||||
method: 'resetPassword',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'accountRecovery',
|
||||
methodName: 'resetPassword',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'challenge': challenge,
|
||||
'signature': signature,
|
||||
'password': password,
|
||||
'totpCode': totpCode,
|
||||
'recoveryCode': recoveryCode,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_iacs.AuthSuccess>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<_isakxto6.ResetPending> startAccountReset(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required String address,
|
||||
required String language,
|
||||
required _is.UuidValue request,
|
||||
required String code,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'accountRecovery',
|
||||
method: 'startAccountReset',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'accountRecovery',
|
||||
methodName: 'startAccountReset',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'address': address,
|
||||
'language': language,
|
||||
'request': request,
|
||||
'code': code,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_isakxto6.ResetPending>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<_iacs.AuthSuccess> completeAccountReset(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required String address,
|
||||
required _is.UuidValue request,
|
||||
required String code,
|
||||
required _ikhyee47.PasswordSetup setup,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'accountRecovery',
|
||||
method: 'completeAccountReset',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'accountRecovery',
|
||||
methodName: 'completeAccountReset',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'address': address,
|
||||
'request': request,
|
||||
'code': code,
|
||||
'setup': setup,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_iacs.AuthSuccess>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _AuthenticatorEndpoint {
|
||||
_AuthenticatorEndpoint(
|
||||
this._endpointDispatch,
|
||||
this._serializationManager,
|
||||
);
|
||||
|
||||
final _is.EndpointDispatch _endpointDispatch;
|
||||
|
||||
final _is.SerializationManager _serializationManager;
|
||||
|
||||
_ida.Future<_id229ey1.AuthenticatorEnrolment> enrol(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required String name,
|
||||
required String label,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'authenticator',
|
||||
method: 'enrol',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'authenticator',
|
||||
methodName: 'enrol',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'name': name,
|
||||
'label': label,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_id229ey1.AuthenticatorEnrolment>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<List<String>> confirm(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required _is.UuidValue authenticator,
|
||||
required String code,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'authenticator',
|
||||
method: 'confirm',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'authenticator',
|
||||
methodName: 'confirm',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'authenticator': authenticator,
|
||||
'code': code,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<List<String>>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<List<_ip34xal6.Authenticator>> list(
|
||||
_ist.TestSessionBuilder sessionBuilder,
|
||||
) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'authenticator',
|
||||
method: 'list',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'authenticator',
|
||||
methodName: 'list',
|
||||
parameters: _ist.testObjectToJson({}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<List<_ip34xal6.Authenticator>>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<void> remove(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required _is.UuidValue authenticator,
|
||||
String? code,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'authenticator',
|
||||
method: 'remove',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'authenticator',
|
||||
methodName: 'remove',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'authenticator': authenticator,
|
||||
'code': code,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<void>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<List<String>> newRecoveryCodes(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required String code,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'authenticator',
|
||||
method: 'newRecoveryCodes',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'authenticator',
|
||||
methodName: 'newRecoveryCodes',
|
||||
parameters: _ist.testObjectToJson({'code': code}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<List<String>>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _PasswordEndpoint {
|
||||
_PasswordEndpoint(
|
||||
this._endpointDispatch,
|
||||
this._serializationManager,
|
||||
);
|
||||
|
||||
final _is.EndpointDispatch _endpointDispatch;
|
||||
|
||||
final _is.SerializationManager _serializationManager;
|
||||
|
||||
_ida.Future<void> set(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required _ikhyee47.PasswordSetup setup,
|
||||
_is.UuidValue? request,
|
||||
String? code,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'password',
|
||||
method: 'set',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'password',
|
||||
methodName: 'set',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'setup': setup,
|
||||
'request': request,
|
||||
'code': code,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<void>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _PasswordSignInEndpoint {
|
||||
_PasswordSignInEndpoint(
|
||||
this._endpointDispatch,
|
||||
this._serializationManager,
|
||||
);
|
||||
|
||||
final _is.EndpointDispatch _endpointDispatch;
|
||||
|
||||
final _is.SerializationManager _serializationManager;
|
||||
|
||||
_ida.Future<_isz0atlx.PasswordChallenge> begin(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required String address,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'passwordSignIn',
|
||||
method: 'begin',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'passwordSignIn',
|
||||
methodName: 'begin',
|
||||
parameters: _ist.testObjectToJson({'address': address}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_isz0atlx.PasswordChallenge>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<_ikpyb5fh.PasswordSignedIn> complete(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required _is.UuidValue challenge,
|
||||
required _idt.ByteData clientValue,
|
||||
required _idt.ByteData clientProof,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'passwordSignIn',
|
||||
method: 'complete',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'passwordSignIn',
|
||||
methodName: 'complete',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'challenge': challenge,
|
||||
'clientValue': clientValue,
|
||||
'clientProof': clientProof,
|
||||
'totpCode': totpCode,
|
||||
'recoveryCode': recoveryCode,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_ikpyb5fh.PasswordSignedIn>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _ProfileEndpoint {
|
||||
_ProfileEndpoint(
|
||||
this._endpointDispatch,
|
||||
|
|
|
|||
Loading…
Reference in a new issue