feat(auth): change the password and the recovery phrase while signed in

ADR 0017, step 3. `password.beginChange` and `password.change` take the
old password's SRP proof and a TOTP or recovery code, and the same keys
wrapped under the new password; a session alone changes nothing, and an
account without a confirmed authenticator is told to set one up first.
`password.setRecoveryKeys` replaces the keys' wrapping under the
recovery phrase for a TOTP code, so a device holding the keys can make
a new phrase.

A change of password ends every other session of the account in every
service of the instance; resets of the password and of the account end
them all. The register ends its own and keeps a notice per member
(`sessions_ended_notice`, a migration that only creates it), told at
once and hourly until it confirms, as deletions are told: it names a
moment, so sessions made since stay, and the session that asked for a
change. A member answering one ends its sessions made before that
moment; a member signing someone in after a reset ends its own first
and makes the new session only once its clock is past the moment.

A reset not due says when it takes effect (`takesEffectAt` on
`PasswordRefusedException`), across members too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-20 14:53:58 +02:00
commit d00da1f24a
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
5 changed files with 43506 additions and 126 deletions

View file

@ -0,0 +1,459 @@
import 'dart:typed_data';
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/mailbox.dart';
import '../support/password_device.dart';
import '../support/password_support.dart';
import '../test_tools/serverpod_test_tools.dart';
final _todo = Uri.parse('http://todo.test:8082');
/// The members of the tests' instance, as the register tells them.
final class _Members implements MemberNotifier {
final sessionsTold = <(Uri, UuidValue, DateTime, UuidValue?)>[];
bool answering = true;
@override
Future<bool> accountGone(Uri member, UuidValue account) async => answering;
@override
Future<bool> sessionsEnded(
Uri member,
UuidValue account, {
required DateTime endedBefore,
UuidValue? keepSession,
}) async {
sessionsTold.add((member, account, endedBefore, keepSession));
return answering;
}
}
/// Changing what keeps an account while signed in (ADR 0017, step 3): the
/// password, with the old one and a code, and the recovery phrase's
/// wrapping, with a code; and the sessions every change or reset of the
/// password ends, in every service of the instance.
void main() {
withServerpod(
'Changing the password',
rollbackDatabase: RollbackDatabase.disabled,
(sessionBuilder, endpoints) {
late Session session;
late Serverpod pod;
late Mailbox mailbox;
late _Members members;
late PasswordDevice device;
late DateTime now;
late UuidValue anna;
late UuidValue annasPhone;
late UuidValue annasLaptop;
late AuthenticatorEnrolment enrolment;
Future<T> atNow<T>(Future<T> Function() body) =>
withClock(Clock(() => now), body);
void nextStep() => now = now.add(totpPeriod);
TestSessionBuilder notSignedIn() => sessionBuilder.copyWith(
authentication: AuthenticationOverride.unauthenticated(),
);
TestSessionBuilder onPhone() => sessionBuilder.copyWith(
authentication: AuthenticationOverride.authenticationInfo(
anna.uuid,
{identified},
authId: annasPhone.uuid,
),
);
Future<({UuidValue request, String code})> mailedCode(
String address,
) async {
final request = await endpoints.accountRecovery.sendCode(
notSignedIn(),
address: address,
language: 'en',
);
return (request: request, code: mailbox.codeFor(address));
}
Future<PasswordSignedIn> signIn({
BigInt? privateValue,
String? totpCode,
}) => atNow(() async {
final challenge = await endpoints.passwordSignIn.begin(
notSignedIn(),
address: 'anna@example.org',
);
final answer = device.answer(challenge, privateValue: privateValue);
return await endpoints.passwordSignIn.complete(
notSignedIn(),
challenge: challenge.challenge,
clientValue: answer.clientValue,
clientProof: answer.clientProof,
totpCode: totpCode ?? codeOf(enrolment, at: now),
);
});
/// Changes Anna's password from her phone, the old one proved by
/// [privateValue] (hers by default) with [totpCode] (her
/// authenticator's now by default).
Future<void> change({
BigInt? privateValue,
String? totpCode,
bool withoutCode = false,
NewPassword? password,
TestSessionBuilder? as,
}) => atNow(() async {
final challenge = await endpoints.password.beginChange(onPhone());
final answer = device.answer(challenge, privateValue: privateValue);
await endpoints.password.change(
as ?? onPhone(),
challenge: challenge.challenge,
clientValue: answer.clientValue,
clientProof: answer.clientProof,
totpCode: withoutCode ? null : totpCode ?? codeOf(enrolment, at: now),
password: password ?? device.newPassword(),
);
});
Future<Set<UuidValue>> sessionsOf(UuidValue person) async => {
for (final row in await ServerSideSession.db.find(
session,
where: (t) => t.authUserId.equals(person),
))
row.id!,
};
setUp(() async {
useBase(sessionBuilder, accounts: HoldsRegister(members: [_todo]));
session = sessionBuilder.build();
pod = session.serverpod;
await deleteAll(session);
await SessionsEndedNotice.db.deleteWhere(
session,
where: (t) => t.id > 0,
);
mailbox = Mailbox();
EmailCodes.of(pod).mailer = mailbox;
members = _Members();
MemberNotifier.use(pod, members);
usePasswordKeys(pod, testPasswordKeys);
now = DateTime.utc(2026, 10, 20, 12);
device = await PasswordDevice.make();
anna = await newAnonymousPerson(session);
annasPhone = await signInNewDevice(session, anna);
annasLaptop = await signInNewDevice(session, anna);
final code = await mailedCode('anna@example.org');
await endpoints.password.set(
onPhone(),
setup: device.setup(),
request: code.request,
code: code.code,
);
enrolment = await endpoints.authenticator.enrol(
onPhone(),
name: 'Phone',
label: 'anna@example.org',
);
await atNow(
() => endpoints.authenticator.confirm(
onPhone(),
authenticator: enrolment.authenticator,
code: codeOf(enrolment, at: now),
),
);
nextStep();
});
tearDown(() async {
EmailCodes.of(pod).mailer = null;
MemberNotifier.use(pod, null);
usePasswordKeys(pod, null);
final cleanup = sessionBuilder.build();
await SessionsEndedNotice.db.deleteWhere(
cleanup,
where: (t) => t.id > 0,
);
await deleteAll(cleanup);
});
group('the password', () {
test('changes with the old one and a code, the keys staying, every '
'other session ending here and at the members', () async {
final before = (await AccountPassword.db.findFirstRow(
session,
where: (t) => t.authUserId.equals(anna),
))!;
final old = device.privateValue;
await change();
nextStep();
final signedIn = await signIn();
expect(signedIn.authUserId, anna);
nextStep();
await expectLater(
signIn(privateValue: old),
refused(PasswordRefusal.wrongCredentials),
);
final after = (await AccountPassword.db.findFirstRow(
session,
where: (t) => t.authUserId.equals(anna),
))!;
expect(
Uint8List.sublistView(after.encryptionPublicKey),
Uint8List.sublistView(before.encryptionPublicKey),
);
expect(
Uint8List.sublistView(after.recoveryKeysCipherText),
Uint8List.sublistView(before.recoveryKeysCipherText),
);
// The phone that changed it stays; the laptop is signed out; the
// sign-in since is a session of its own.
final sessions = await sessionsOf(anna);
expect(sessions, contains(annasPhone));
expect(sessions, isNot(contains(annasLaptop)));
final (member, account, endedBefore, kept) =
members.sessionsTold.first;
expect(member, _todo);
expect(account, anna);
expect(endedBefore, DateTime.utc(2026, 10, 20, 12, 0, 30));
expect(kept, annasPhone);
expect(await SessionsEndedNotice.db.count(session), 0);
});
test('is refused alike for a wrong old password, a missing or wrong '
"code, and another person's proof, nothing changing", () async {
final kept = Uint8List.sublistView(
(await AccountPassword.db.findFirstRow(
session,
where: (t) => t.authUserId.equals(anna),
))!.verifier,
);
final mine = device.privateValue;
await expectLater(
change(privateValue: BigInt.from(4242)),
refused(PasswordRefusal.wrongCredentials),
);
device.privateValue = mine;
await expectLater(
change(withoutCode: true),
refused(PasswordRefusal.wrongCredentials),
);
device.privateValue = mine;
await expectLater(
change(totpCode: '000000'),
refused(PasswordRefusal.wrongCredentials),
);
device.privateValue = mine;
final ben = await newAnonymousPerson(session);
final bensLaptop = await signInNewDevice(session, ben);
await expectLater(
change(
as: sessionBuilder.copyWith(
authentication: AuthenticationOverride.authenticationInfo(
ben.uuid,
{identified},
authId: bensLaptop.uuid,
),
),
),
refused(PasswordRefusal.wrongCredentials),
);
device.privateValue = mine;
await expectLater(
change(
password: device.newPassword().copyWith(salt: ByteData(3)),
),
refused(PasswordRefusal.invalidSetup),
);
expect(
Uint8List.sublistView(
(await AccountPassword.db.findFirstRow(
session,
where: (t) => t.authUserId.equals(anna),
))!.verifier,
),
kept,
);
expect(await sessionsOf(anna), {annasPhone, annasLaptop});
expect(members.sessionsTold, isEmpty);
});
test('is begun only by someone with a password', () async {
final ben = await newAnonymousPerson(session);
final bensLaptop = await signInNewDevice(session, ben);
await expectLater(
endpoints.password.beginChange(
sessionBuilder.copyWith(
authentication: AuthenticationOverride.authenticationInfo(
ben.uuid,
{},
authId: bensLaptop.uuid,
),
),
),
refused(PasswordRefusal.noPassword),
);
});
test('tells a member that missed it again, hourly, until it '
'confirms', () async {
members.answering = false;
await change();
expect(await SessionsEndedNotice.db.count(session), 1);
members.answering = true;
expect(await deliverSessionNotices(session), 1);
expect(await SessionsEndedNotice.db.count(session), 0);
});
});
group('the recovery phrase', () {
test("takes new wrapped keys for a code: the old phrase's wrapping "
'goes', () async {
final fresh = WrappedKeys(
nonce: ByteData(24),
cipherText: ByteData(wrappedKeysLength),
);
await atNow(
() => endpoints.password.setRecoveryKeys(
onPhone(),
code: codeOf(enrolment, at: now),
recoveryKeys: fresh,
),
);
final proof = await endpoints.accountRecovery.beginKeyProof(
notSignedIn(),
address: 'anna@example.org',
);
expect(proof.recoveryKeys.toJson(), fresh.toJson());
});
test('is refused for a wrong code, keys out of range, and without a '
'password, keeping the wrapping', () async {
final fresh = WrappedKeys(
nonce: ByteData(24),
cipherText: ByteData(wrappedKeysLength),
);
await expectLater(
atNow(
() => endpoints.password.setRecoveryKeys(
onPhone(),
code: '000000',
recoveryKeys: fresh,
),
),
refused(PasswordRefusal.wrongCode),
);
await expectLater(
atNow(
() => endpoints.password.setRecoveryKeys(
onPhone(),
code: codeOf(enrolment, at: now),
recoveryKeys: WrappedKeys(
nonce: ByteData(24),
cipherText: ByteData(8),
),
),
),
refused(PasswordRefusal.invalidSetup),
);
final ben = await newAnonymousPerson(session);
await expectLater(
endpoints.password.setRecoveryKeys(
sessionBuilder.copyWith(
authentication: AuthenticationOverride.authenticationInfo(
ben.uuid,
{},
),
),
code: '123456',
recoveryKeys: fresh,
),
refused(PasswordRefusal.noPassword),
);
final proof = await endpoints.accountRecovery.beginKeyProof(
notSignedIn(),
address: 'anna@example.org',
);
expect(
proof.recoveryKeys.toJson(),
device.recoveryKeys.toJson(),
);
});
});
group('a reset', () {
test('of the password with the phrase ends the sessions at the '
'members too, the new session made after', () async {
final proof = await endpoints.accountRecovery.beginKeyProof(
notSignedIn(),
address: 'anna@example.org',
);
final signature = await device.sign(proof);
final signedIn = await atNow(
() => endpoints.accountRecovery.resetPassword(
notSignedIn(),
challenge: proof.challenge,
signature: signature,
password: device.newPassword(),
totpCode: codeOf(enrolment, at: now),
),
);
expect(signedIn.authUserId, anna);
final sessions = await sessionsOf(anna);
expect(sessions, hasLength(1));
expect(sessions, isNot(contains(annasPhone)));
expect(sessions, isNot(contains(annasLaptop)));
expect(members.sessionsTold.single.$2, anna);
expect(members.sessionsTold.single.$4, isNull);
});
test('not due says when it takes effect', () async {
final start = await mailedCode('anna@example.org');
final pending = await atNow(
() => endpoints.accountRecovery.startAccountReset(
notSignedIn(),
address: 'anna@example.org',
language: 'en',
request: start.request,
code: start.code,
),
);
final complete = await mailedCode('anna@example.org');
final fresh = await PasswordDevice.make(seed: 99);
await expectLater(
atNow(
() => endpoints.accountRecovery.completeAccountReset(
notSignedIn(),
address: 'anna@example.org',
request: complete.request,
code: complete.code,
setup: fresh.setup(),
),
),
throwsA(
isA<PasswordRefusedException>()
.having((e) => e.reason, 'reason', PasswordRefusal.notDue)
.having(
(e) => e.takesEffectAt,
'takesEffectAt',
pending.takesEffectAt,
),
),
);
});
});
},
);
}

View file

@ -193,8 +193,8 @@ void main() {
where: (t) => t.authUserId.equals(anna),
))!;
expect(
after.salt.buffer.asUint8List(),
before.salt.buffer.asUint8List(),
Uint8List.sublistView(after.salt),
Uint8List.sublistView(before.salt),
);
expect(await AuthUser.db.count(session), 1);
});

View file

@ -366,15 +366,21 @@ final class _Register implements AccountRegister {
);
});
/// The moment a reset ended the account's sessions at the register.
DateTime endedBefore = DateTime.utc(2026, 10, 20, 12);
@override
Future<UuidValue> resetPasswordWithPhrase(
Future<AccountSessionsEnded> resetPasswordWithPhrase(
Session session, {
required UuidValue challenge,
required ByteData signature,
required NewPassword password,
String? totpCode,
String? recoveryCode,
}) => _answer('resetPasswordWithPhrase', () => _proven().account);
}) => _answer(
'resetPasswordWithPhrase',
() => (account: _proven().account, endedBefore: endedBefore),
);
@override
Future<ResetPending> startAccountReset(
@ -389,13 +395,56 @@ final class _Register implements AccountRegister {
);
@override
Future<UuidValue> completeAccountReset(
Future<AccountSessionsEnded> completeAccountReset(
Session session, {
required String address,
required UuidValue request,
required String code,
required PasswordSetup setup,
}) => _answer('completeAccountReset', () => _proven().account);
}) => _answer(
'completeAccountReset',
() => (account: _proven().account, endedBefore: endedBefore),
);
/// The sessions kept by the password changes asked, by person.
final changed = <UuidValue, UuidValue?>{};
@override
Future<PasswordChallenge> beginPasswordChange(
Session session, {
required UuidValue person,
}) => _answer(
'beginPasswordChange',
() => PasswordChallenge(
challenge: const Uuid().v7obj(),
salt: ByteData(16),
costs: newPasswordCosts,
serverValue: ByteData(384),
),
);
@override
Future<void> changePassword(
Session session, {
required UuidValue person,
required UuidValue challenge,
required ByteData clientValue,
required ByteData clientProof,
String? totpCode,
String? recoveryCode,
required NewPassword password,
UuidValue? keepSession,
}) => _answer('changePassword', () {
changed[person] = keepSession;
});
@override
Future<void> setRecoveryKeys(
Session session, {
required UuidValue person,
required String code,
required WrappedKeys recoveryKeys,
}) => _answer('setRecoveryKeys', () {});
@override
Future<void> deleteAccount(Session session, UuidValue person) =>
@ -985,6 +1034,146 @@ void main() {
});
});
group('told by the register that sessions ended (ADR 0017)', () {
Future<InstanceAnswer> tell(
Map<String, Object?> body, {
String? authorization = 'Bearer $_secret',
}) => answerSessionsEnded(
session,
jsonEncode(body),
authorization: authorization,
);
Future<DateTime> createdAt(UuidValue id) async =>
(await ServerSideSession.db.findById(session, id))!.createdAt;
test('ends those of the account made before the moment, but the one '
'kept and those made since, and nobody else', () async {
final bensPhone = await signInNewDevice(session, ben);
final endedBefore = (await createdAt(
bensPhone,
)).add(const Duration(microseconds: 1));
await Future<void>.delayed(const Duration(milliseconds: 5));
final bensTablet = await signInNewDevice(session, ben);
final answer = await tell({
'account': ben.uuid,
'endedBefore': endedBefore.toIso8601String(),
'keepSession': bensLaptop.uuid,
});
expect(answer.status, 200);
final left = {
for (final row in await ServerSideSession.db.find(
session,
where: (t) => t.authUserId.equals(ben),
))
row.id!,
};
expect(left, {bensLaptop, bensTablet});
expect(
await ServerSideSession.db.findById(session, annasPhone),
isNotNull,
);
expect(register.asked, isEmpty);
});
test('refuses without the secret and what it cannot read, and is '
'answered by no register, ending nothing', () async {
final everything = {
'account': ben.uuid,
'endedBefore': DateTime.now().toUtc().toIso8601String(),
};
expect((await tell(everything, authorization: null)).status, 401);
expect(
(await tell(everything, authorization: 'Bearer wrong')).status,
401,
);
expect(
(await tell({'account': ben.uuid, 'endedBefore': 'soon'})).status,
400,
);
expect((await tell({'account': ben.uuid})).status, 400);
useBase(sessionBuilder);
expect((await tell(everything)).status, 404);
expect(
await ServerSideSession.db.findById(session, bensLaptop),
isNotNull,
);
});
});
group('changing a password, by asking the register (ADR 0017)', () {
test('keeps the session asking: its ID goes to the register, which '
'tells every service', () async {
final challenge = await endpoints.password.beginChange(asBen());
await endpoints.password.change(
asBen(),
challenge: challenge.challenge,
clientValue: ByteData(384),
clientProof: ByteData(32),
totpCode: '123456',
password: NewPassword(
salt: ByteData(16),
costs: newPasswordCosts,
verifier: ByteData(384),
passwordKeys: WrappedKeys(
nonce: ByteData(24),
cipherText: ByteData(112),
),
),
);
expect(register.asked, ['beginPasswordChange', 'changePassword']);
expect(register.changed, {ben: bensLaptop});
});
test('a reset ends the sessions here made before the register ended '
'them, the new one made after', () async {
register
..proof = PasswordProof(
account: ben,
serverProof: ByteData(32),
encryptionPublicKey: ByteData(32),
signingPublicKey: ByteData(32),
passwordKeys: WrappedKeys(
nonce: ByteData(24),
cipherText: ByteData(112),
),
needsAuthenticator: false,
)
..endedBefore = DateTime.now().toUtc();
final signedIn = await endpoints.accountRecovery.resetPassword(
notSignedIn(),
challenge: const Uuid().v7obj(),
signature: ByteData(64),
password: NewPassword(
salt: ByteData(16),
costs: newPasswordCosts,
verifier: ByteData(384),
passwordKeys: WrappedKeys(
nonce: ByteData(24),
cipherText: ByteData(112),
),
),
totpCode: '123456',
);
expect(signedIn.authUserId, ben);
final left = await ServerSideSession.db.find(
session,
where: (t) => t.authUserId.equals(ben),
);
expect(left.single.id, isNot(bensLaptop));
expect(
left.single.createdAt.isBefore(register.endedBefore),
isFalse,
);
});
});
group('pruning', () {
late UuidValue carla;

View file

@ -50,6 +50,9 @@ final class _Mailbox implements CodeMailer {
/// says (confirming by default).
final class _Members implements MemberNotifier {
final told = <(Uri, UuidValue)>[];
/// The sessions ended they were told of: member, account, moment, kept.
final sessionsTold = <(Uri, UuidValue, DateTime, UuidValue?)>[];
bool Function(Uri member) answers = (_) => true;
@override
@ -57,6 +60,17 @@ final class _Members implements MemberNotifier {
told.add((member, account));
return answers(member);
}
@override
Future<bool> sessionsEnded(
Uri member,
UuidValue account, {
required DateTime endedBefore,
UuidValue? keepSession,
}) async {
sessionsTold.add((member, account, endedBefore, keepSession));
return answers(member);
}
}
void main() {
@ -623,8 +637,10 @@ void main() {
password: device.newPassword(),
totpCode: codeOf(enrolment, at: now),
),
annaOnTodo,
(account: annaOnTodo, endedBefore: now),
);
// Every member is told to end the account's sessions too.
expect(members.sessionsTold.single, (_todo, annaOnTodo, now, null));
final forReset = await remote.beginPasswordSignIn(
session,
@ -679,12 +695,140 @@ void main() {
code: mailbox.codeFor('anna@example.org'),
setup: fresh.setup(),
),
annaOnTodo,
(account: annaOnTodo, endedBefore: now),
);
});
});
});
group('changes while signed in (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(
"cross for a member's device: the password, keeping the "
"member's session, the phrase's wrapping, and a reset's date",
() async {
final device = await PasswordDevice.make();
await atNow(() async {
final annaOnTodo = const Uuid().v7obj();
final annasPhone = 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'),
);
final enrolment = await remote.enrolAuthenticator(
session,
person: annaOnTodo,
name: 'Phone',
label: 'anna@example.org',
);
await remote.confirmAuthenticator(
session,
person: annaOnTodo,
authenticator: enrolment.authenticator,
code: codeOf(enrolment),
);
now = now.add(totpPeriod);
final challenge = await remote.beginPasswordChange(
session,
person: annaOnTodo,
);
final answer = device.answer(challenge);
await remote.changePassword(
session,
person: annaOnTodo,
challenge: challenge.challenge,
clientValue: answer.clientValue,
clientProof: answer.clientProof,
totpCode: codeOf(enrolment),
password: device.newPassword(),
keepSession: annasPhone,
);
expect(members.sessionsTold.single, (
_todo,
annaOnTodo,
now,
annasPhone,
));
now = now.add(totpPeriod);
final fresh = WrappedKeys(
nonce: ByteData(24),
cipherText: ByteData(wrappedKeysLength),
);
await remote.setRecoveryKeys(
session,
person: annaOnTodo,
code: codeOf(enrolment),
recoveryKeys: fresh,
);
await expectLater(
remote.setRecoveryKeys(
session,
person: annaOnTodo,
code: codeOf(enrolment),
recoveryKeys: fresh,
),
refused(PasswordRefusal.wrongCode),
);
final start = await remote.sendSignInCode(
session,
address: 'anna@example.org',
language: 'en',
network: '203.0.113.7',
);
final pending = await remote.startAccountReset(
session,
address: 'anna@example.org',
language: 'en',
request: start,
code: mailbox.codeFor('anna@example.org'),
);
final early = await remote.sendSignInCode(
session,
address: 'anna@example.org',
language: 'en',
network: '203.0.113.7',
);
await expectLater(
remote.completeAccountReset(
session,
address: 'anna@example.org',
request: early,
code: mailbox.codeFor('anna@example.org'),
setup: device.setup(),
),
throwsA(
isA<PasswordRefusedException>().having(
(error) => error.takesEffectAt,
'takesEffectAt',
pending.takesEffectAt,
),
),
);
});
},
);
});
group('ways in without a working password (ADR 0017)', () {
late DateTime now;
Future<T> atNow<T>(Future<T> Function() body) =>

View file

@ -36,6 +36,8 @@ import 'package:halkyon_base_server/src/generated/password/password_signed_in.da
as _ikpyb5fh;
import 'package:halkyon_base_server/src/generated/password/reset_pending.dart'
as _isakxto6;
import 'package:halkyon_base_server/src/generated/password/wrapped_keys.dart'
as _iji0wmu8;
import 'package:halkyon_base_server/src/generated/profile/profile.dart'
as _it4tom37;
import 'package:halkyon_base_server/src/generated/profile/profile_refusal.dart'
@ -1107,6 +1109,114 @@ class _PasswordEndpoint {
}
});
}
_ida.Future<_isz0atlx.PasswordChallenge> beginChange(
_ist.TestSessionBuilder sessionBuilder,
) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'password',
method: 'beginChange',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'password',
methodName: 'beginChange',
parameters: _ist.testObjectToJson({}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<_isz0atlx.PasswordChallenge>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<void> change(
_ist.TestSessionBuilder sessionBuilder, {
required _is.UuidValue challenge,
required _idt.ByteData clientValue,
required _idt.ByteData clientProof,
String? totpCode,
String? recoveryCode,
required _ijye8mcv.NewPassword password,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'password',
method: 'change',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'password',
methodName: 'change',
parameters: _ist.testObjectToJson({
'challenge': challenge,
'clientValue': clientValue,
'clientProof': clientProof,
'totpCode': totpCode,
'recoveryCode': recoveryCode,
'password': password,
}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<void>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<void> setRecoveryKeys(
_ist.TestSessionBuilder sessionBuilder, {
required String code,
required _iji0wmu8.WrappedKeys recoveryKeys,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'password',
method: 'setRecoveryKeys',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'password',
methodName: 'setRecoveryKeys',
parameters: _ist.testObjectToJson({
'code': code,
'recoveryKeys': recoveryKeys,
}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<void>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
}
class _PasswordSignInEndpoint {