fix(auth): an account whose second factor doesn't work deletes itself
An account whose setup never finished, or whose authenticators' reset took effect, could not delete itself (the re-review's eighth finding): deletion asked for a TOTP or recovery code it had no way to give. Such an account now deletes itself with the password's proof and a code by mail to its address asked for deleting (EmailCodePurpose.deletion, which nothing else takes), the address checked against the account's as everywhere after the password; an account whose second factor works still takes a code from it. The app's deletion sheet asks for the password and mails the code there, then takes it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
1d4320370b
commit
293ec6b103
19 changed files with 752 additions and 185 deletions
|
|
@ -212,5 +212,7 @@
|
|||
"passwordCodesWait": "Zu viele falsche Codes hintereinander: Bis {date} wird kein Code angenommen. Waren es nicht deine, kennt jemand dein Passwort: Ändere es dann.",
|
||||
"passwordDeleteIntro": "Dein Konto wird mit allem darin gelöscht, in jedem Dienst dieser Instanz. Gib zur Bestätigung dein Passwort und einen Code deiner Authenticator-App ein.",
|
||||
"passwordSessionEnded": "Zu viele falsche Codes hintereinander von diesem Gerät: Es wurde abgemeldet. Melde dich wieder mit deinem Passwort und einem Code an.",
|
||||
"passwordAddressUnknown": "Dieses Gerät kennt die Adresse deines Kontos nicht, die das braucht. Melde dich ab und wieder mit deiner Adresse und deinem Passwort an."
|
||||
"passwordAddressUnknown": "Dieses Gerät kennt die Adresse deines Kontos nicht, die das braucht. Melde dich ab und wieder mit deiner Adresse und deinem Passwort an.",
|
||||
"passwordDeleteIntroByMail": "Dein Konto wird mit allem darin gelöscht, in jedem Dienst dieser Instanz. Seine Authenticator-App ist nicht eingerichtet, daher bestätigt es ein Code per E-Mail: Gib dein Passwort ein, dann den Code, den wir an deine Adresse schicken.",
|
||||
"passwordDeleteSendCode": "Code per E-Mail schicken"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -459,5 +459,7 @@
|
|||
},
|
||||
"passwordDeleteIntro": "Your account goes with everything in it, in every service of this instance. To confirm, type your password and a code from your authenticator app.",
|
||||
"passwordSessionEnded": "Too many wrong codes in a row from this device: it was signed out. Sign in again with your password and a code.",
|
||||
"passwordAddressUnknown": "This device doesn't know your account's address, which this takes. Sign out, then sign in again with your address and password."
|
||||
"passwordAddressUnknown": "This device doesn't know your account's address, which this takes. Sign out, then sign in again with your address and password.",
|
||||
"passwordDeleteIntroByMail": "Your account goes with everything in it, in every service of this instance. Its authenticator isn't set up, so a code by mail confirms it: type your password, then the code we email to your address.",
|
||||
"passwordDeleteSendCode": "Email me a code"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1380,6 +1380,18 @@ abstract class HalkyonAppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'This device doesn\'t know your account\'s address, which this takes. Sign out, then sign in again with your address and password.'**
|
||||
String get passwordAddressUnknown;
|
||||
|
||||
/// No description provided for @passwordDeleteIntroByMail.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your account goes with everything in it, in every service of this instance. Its authenticator isn\'t set up, so a code by mail confirms it: type your password, then the code we email to your address.'**
|
||||
String get passwordDeleteIntroByMail;
|
||||
|
||||
/// No description provided for @passwordDeleteSendCode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Email me a code'**
|
||||
String get passwordDeleteSendCode;
|
||||
}
|
||||
|
||||
class _HalkyonAppLocalizationsDelegate
|
||||
|
|
|
|||
|
|
@ -800,4 +800,11 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
|||
@override
|
||||
String get passwordAddressUnknown =>
|
||||
'Dieses Gerät kennt die Adresse deines Kontos nicht, die das braucht. Melde dich ab und wieder mit deiner Adresse und deinem Passwort an.';
|
||||
|
||||
@override
|
||||
String get passwordDeleteIntroByMail =>
|
||||
'Dein Konto wird mit allem darin gelöscht, in jedem Dienst dieser Instanz. Seine Authenticator-App ist nicht eingerichtet, daher bestätigt es ein Code per E-Mail: Gib dein Passwort ein, dann den Code, den wir an deine Adresse schicken.';
|
||||
|
||||
@override
|
||||
String get passwordDeleteSendCode => 'Code per E-Mail schicken';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -783,6 +783,13 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
|||
@override
|
||||
String get passwordAddressUnknown =>
|
||||
'This device doesn\'t know your account\'s address, which this takes. Sign out, then sign in again with your address and password.';
|
||||
|
||||
@override
|
||||
String get passwordDeleteIntroByMail =>
|
||||
'Your account goes with everything in it, in every service of this instance. Its authenticator isn\'t set up, so a code by mail confirms it: type your password, then the code we email to your address.';
|
||||
|
||||
@override
|
||||
String get passwordDeleteSendCode => 'Email me a code';
|
||||
}
|
||||
|
||||
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:halkyon_app/src/app/app_scope.dart';
|
|||
import 'package:halkyon_app/src/auth/bloc/session_bloc.dart';
|
||||
import 'package:halkyon_app/src/instance/instance_scope.dart';
|
||||
import 'package:halkyon_app/src/instance/widget/account_switcher_sheet.dart';
|
||||
import 'package:halkyon_app/src/password/model/account_security.dart';
|
||||
import 'package:halkyon_app/src/password/model/password_failed.dart';
|
||||
import 'package:halkyon_app/src/password/widget/delete_account_sheet.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
|
|
@ -125,18 +126,22 @@ class const AccountTiles({
|
|||
)) {
|
||||
return;
|
||||
}
|
||||
final bool hasPassword;
|
||||
final AccountSecurity? security;
|
||||
try {
|
||||
hasPassword = identified && (await passwords.security()).hasPassword;
|
||||
security = identified ? await passwords.security() : null;
|
||||
} on PasswordFailed {
|
||||
// Unreachable: deleting would fail too, as the session bloc says.
|
||||
session.add(const SessionEventDeleteRequested());
|
||||
return;
|
||||
}
|
||||
if (!hasPassword) {
|
||||
if (security == null || !security.hasPassword) {
|
||||
session.add(const SessionEventDeleteRequested());
|
||||
} else if (context.mounted) {
|
||||
await showDeleteAccountWithPassword(context);
|
||||
// Without a second factor that works, a code by mail confirms it.
|
||||
await showDeleteAccountWithPassword(
|
||||
context,
|
||||
byMail: !security.finished,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,13 +113,17 @@ abstract interface class PasswordGateway() {
|
|||
|
||||
/// Deletes the signed-in person's account, which has a password: [answer]
|
||||
/// to [challenge] ([beginChange]) proves it, with a [totpCode] or
|
||||
/// [recoveryCode], with the account's [address] and [language] (as a
|
||||
/// change takes them); this device then forgets its session.
|
||||
/// [recoveryCode], or where no second factor works a [code] mailed for
|
||||
/// [request] asked for deleting, with the account's [address] and
|
||||
/// [language] (as a change takes them); this device then forgets its
|
||||
/// session.
|
||||
Future<void> deleteAccount(
|
||||
protocol.PasswordChallenge challenge,
|
||||
SrpAnswer answer, {
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
String? request,
|
||||
String? code,
|
||||
required String address,
|
||||
required String language,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -355,12 +355,16 @@ class PasswordRepository(
|
|||
|
||||
/// Deletes the signed-in person's account, which has a password, with
|
||||
/// [password] and a [totpCode] or [recoveryCode], as a change of password
|
||||
/// takes (a session alone deletes only an account without one): on the
|
||||
/// instance and every service of it, then signed out here.
|
||||
/// takes (a session alone deletes only an account without one), or where
|
||||
/// no second factor works a [code] mailed for [request]
|
||||
/// ([mailDeletionCode]): on the instance and every service of it, then
|
||||
/// signed out here.
|
||||
Future<void> deleteAccount({
|
||||
required String password,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
String? request,
|
||||
String? code,
|
||||
required String language,
|
||||
}) async {
|
||||
final address = await _addressHere();
|
||||
|
|
@ -375,11 +379,31 @@ class PasswordRepository(
|
|||
await _crypto.answer(unlock, challenge, host: _gateway.host),
|
||||
totpCode: totpCode?.replaceAll(' ', ''),
|
||||
recoveryCode: recoveryCode,
|
||||
request: request,
|
||||
code: code,
|
||||
address: address,
|
||||
language: language,
|
||||
);
|
||||
}
|
||||
|
||||
/// Mails a code for deleting the signed-in person's account to the
|
||||
/// address this device signed in with, where no second factor works (a
|
||||
/// setup never finished): that address, and the request the code
|
||||
/// answers.
|
||||
Future<({String address, String request})> mailDeletionCode({
|
||||
required String language,
|
||||
}) async {
|
||||
final address = await _addressHere();
|
||||
return (
|
||||
address: address,
|
||||
request: await sendCode(
|
||||
address,
|
||||
language: language,
|
||||
purpose: MailCodePurpose.deletion,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// A new recovery phrase for the signed-in person's keys, to show; the
|
||||
/// instance takes it with [keepPhrase].
|
||||
Future<NewPhrase> newPhrase() async {
|
||||
|
|
|
|||
|
|
@ -197,6 +197,8 @@ class ServerpodPasswordGateway(
|
|||
SrpAnswer answer, {
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
String? request,
|
||||
String? code,
|
||||
required String address,
|
||||
required String language,
|
||||
}) => _failing(() async {
|
||||
|
|
@ -207,6 +209,8 @@ class ServerpodPasswordGateway(
|
|||
clientProof: answer.clientProof,
|
||||
totpCode: totpCode,
|
||||
recoveryCode: recoveryCode,
|
||||
request: _idOf(request),
|
||||
code: code,
|
||||
host: host,
|
||||
address: address,
|
||||
language: language,
|
||||
|
|
|
|||
|
|
@ -9,16 +9,21 @@ import 'package:material_ui/material_ui.dart';
|
|||
|
||||
/// Deletes the signed-in person's account, which has a password: the
|
||||
/// password and a code from an authenticator, as changing the password
|
||||
/// takes (ADR 0017; the second reading). Once deleted, the person is signed
|
||||
/// out here, and the app asks how to start.
|
||||
Future<void> showDeleteAccountWithPassword(BuildContext context) =>
|
||||
showAdaptiveSheet<void>(
|
||||
context,
|
||||
swipeable: false,
|
||||
builder: (context) => const _DeleteAccount(),
|
||||
);
|
||||
/// takes (ADR 0017; the second reading), or [byMail], where no second
|
||||
/// factor works (a setup never finished), the password and a code by mail
|
||||
/// asked for deleting. Once deleted, the person is signed out here, and the
|
||||
/// app asks how to start.
|
||||
Future<void> showDeleteAccountWithPassword(
|
||||
BuildContext context, {
|
||||
bool byMail = false,
|
||||
}) => showAdaptiveSheet<void>(
|
||||
context,
|
||||
swipeable: false,
|
||||
builder: (context) => _DeleteAccount(byMail: byMail),
|
||||
);
|
||||
|
||||
class const _DeleteAccount() extends StatefulWidget {
|
||||
class const _DeleteAccount({required final bool byMail})
|
||||
extends StatefulWidget {
|
||||
@override
|
||||
State<_DeleteAccount> createState() => _DeleteAccountState();
|
||||
}
|
||||
|
|
@ -28,6 +33,9 @@ class _DeleteAccountState() extends State<_DeleteAccount> {
|
|||
final _code = TextEditingController();
|
||||
late final _step = SecurityStepCubit<bool>();
|
||||
|
||||
/// The code mailed for deleting, once asked for: its address and request.
|
||||
({String address, String request})? _mailed;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_password.dispose();
|
||||
|
|
@ -39,10 +47,22 @@ class _DeleteAccountState() extends State<_DeleteAccount> {
|
|||
Future<void> _submit() async {
|
||||
final passwords = AppScope.of(context).passwords;
|
||||
final language = mailLanguageOf(context);
|
||||
final mailed = _mailed;
|
||||
if (widget.byMail && mailed == null) {
|
||||
// The code first; the password goes with it.
|
||||
await _step.run(() async {
|
||||
final asked = await passwords.mailDeletionCode(language: language);
|
||||
setState(() => _mailed = asked);
|
||||
return false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
await _step.run(() async {
|
||||
await passwords.deleteAccount(
|
||||
password: _password.text,
|
||||
totpCode: _code.text,
|
||||
totpCode: widget.byMail ? null : _code.text,
|
||||
request: mailed?.request,
|
||||
code: widget.byMail ? _code.text : null,
|
||||
language: language,
|
||||
);
|
||||
return true;
|
||||
|
|
@ -64,22 +84,41 @@ class _DeleteAccountState() extends State<_DeleteAccount> {
|
|||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SheetIntro(l10n.passwordDeleteIntro),
|
||||
SheetIntro(
|
||||
widget.byMail
|
||||
? l10n.passwordDeleteIntroByMail
|
||||
: l10n.passwordDeleteIntro,
|
||||
),
|
||||
PasswordField(
|
||||
controller: _password,
|
||||
label: l10n.passwordCurrent,
|
||||
autofocus: true,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
CodeField(
|
||||
controller: _code,
|
||||
kind: CodeKind.totp,
|
||||
errorText: switch (state.problem) {
|
||||
final failed? => passwordProblemText(context, failed),
|
||||
null => null,
|
||||
},
|
||||
onSubmitted: (_) => _submit(),
|
||||
),
|
||||
if (widget.byMail && _mailed == null)
|
||||
if (state.problem case final failed?)
|
||||
Text(
|
||||
passwordProblemText(context, failed),
|
||||
style: TextStyle(color: colors.error),
|
||||
)
|
||||
else
|
||||
const SizedBox.shrink()
|
||||
else ...[
|
||||
if (_mailed case (:final address, request: _))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Text(l10n.passwordMailCodeSent(address)),
|
||||
),
|
||||
CodeField(
|
||||
controller: _code,
|
||||
kind: widget.byMail ? CodeKind.mail : CodeKind.totp,
|
||||
errorText: switch (state.problem) {
|
||||
final failed? => passwordProblemText(context, failed),
|
||||
null => null,
|
||||
},
|
||||
onSubmitted: (_) => _submit(),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -89,7 +128,11 @@ class _DeleteAccountState() extends State<_DeleteAccount> {
|
|||
foregroundColor: colors.onError,
|
||||
),
|
||||
onPressed: state.busy ? null : _submit,
|
||||
child: Text(l10n.accountDelete),
|
||||
child: Text(
|
||||
widget.byMail && _mailed == null
|
||||
? l10n.passwordDeleteSendCode
|
||||
: l10n.accountDelete,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -546,13 +546,29 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
|||
SrpAnswer answer, {
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
String? request,
|
||||
String? code,
|
||||
required String address,
|
||||
required String language,
|
||||
}) async {
|
||||
final account = await _call(
|
||||
'deleteAccount',
|
||||
() => _ownerProved(challenge, answer, totpCode, recoveryCode, address),
|
||||
);
|
||||
final account = await _call('deleteAccount', () {
|
||||
final proved = _proved(challenge, answer);
|
||||
if (proved.finished) {
|
||||
return _ownerCoded(proved, totpCode, recoveryCode, address);
|
||||
}
|
||||
// No second factor works: the password and a code by mail asked for
|
||||
// deleting, as the register takes them.
|
||||
if (proved.person != _signedIn()) {
|
||||
throw const PasswordFailed(PasswordProblem.wrongCredentials);
|
||||
}
|
||||
_requireAddress(proved, address);
|
||||
if (request == null ||
|
||||
(code ?? '').trim().isEmpty ||
|
||||
_proven(request, code, MailCodePurpose.deletion) != proved.address) {
|
||||
throw const PasswordFailed(PasswordProblem.wrongCredentials);
|
||||
}
|
||||
return proved;
|
||||
});
|
||||
accounts.remove(account.address);
|
||||
await _auth.deleteAccount();
|
||||
}
|
||||
|
|
@ -581,8 +597,16 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
|||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
String address,
|
||||
) => _ownerCoded(_proved(challenge, answer), totpCode, recoveryCode, address);
|
||||
|
||||
/// [account], whose password proved right, once it is the signed-in
|
||||
/// person's and a code came: refused as the register refuses a change.
|
||||
FakePasswordAccount _ownerCoded(
|
||||
FakePasswordAccount account,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
String address,
|
||||
) {
|
||||
final account = _proved(challenge, answer);
|
||||
if (account.person != _signedIn() ||
|
||||
((totpCode ?? '').isEmpty && (recoveryCode ?? '').isEmpty)) {
|
||||
throw const PasswordFailed(PasswordProblem.wrongCredentials);
|
||||
|
|
|
|||
|
|
@ -173,6 +173,53 @@ void main() {
|
|||
expect(find.text('Signed out'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('deleting an account whose setup never finished asks for the '
|
||||
'password and a code by mail', (tester) async {
|
||||
// The first person the fake password gateway makes.
|
||||
const person = '00000000-0000-4000-8000-000000000001';
|
||||
gateway = FakeAuthGateway(
|
||||
status: const SignedIn(authUserId: person, isIdentified: true),
|
||||
);
|
||||
await start(tester);
|
||||
dependencies.passwordGateway.addAccount(
|
||||
'anna@example.org',
|
||||
password: 'correct horse',
|
||||
crypto: dependencies.crypto,
|
||||
finished: false,
|
||||
);
|
||||
dependencies.addresses.addresses[person] = 'anna@example.org';
|
||||
Finder field(String label) => find.widgetWithText(TextField, label);
|
||||
|
||||
await tester.tap(find.text('Delete account'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Delete account').last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('a code by mail confirms it'), findsOneWidget);
|
||||
await tester.enterText(field('Current password'), 'correct horse');
|
||||
await tester.tap(find.text('Email me a code'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
dependencies.passwordGateway.mailed.single.purpose,
|
||||
MailCodePurpose.deletion,
|
||||
);
|
||||
expect(find.textContaining('anna@example.org'), findsOneWidget);
|
||||
expect(gateway.accountDeletions, 0);
|
||||
|
||||
await tester.enterText(
|
||||
field('Code from the email'),
|
||||
dependencies.passwordGateway.codeFor('anna@example.org'),
|
||||
);
|
||||
await tester.tap(find.text('Delete account').last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(gateway.accountDeletions, 1);
|
||||
expect(
|
||||
dependencies.passwordGateway.accounts,
|
||||
isNot(contains('anna@example.org')),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('deleting an account with a password asks for it and a '
|
||||
'code, which a wrong one fails, deleting nothing', (tester) async {
|
||||
// The first person the fake password gateway makes.
|
||||
|
|
|
|||
|
|
@ -166,6 +166,49 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
test('an account whose setup never finished deletes itself with the '
|
||||
'password and a code by mail asked for deleting, nothing less', () async {
|
||||
gateway.addAccount(
|
||||
'carla@example.org',
|
||||
password: 'battery staple',
|
||||
crypto: crypto,
|
||||
finished: false,
|
||||
);
|
||||
final unlocked = await passwords.unlock(
|
||||
'carla@example.org',
|
||||
'battery staple',
|
||||
);
|
||||
final request = await passwords.sendCode(
|
||||
'carla@example.org',
|
||||
language: 'en',
|
||||
purpose: MailCodePurpose.finishSetup,
|
||||
);
|
||||
await passwords.enter(
|
||||
await passwords.signIn(
|
||||
unlocked,
|
||||
request: request,
|
||||
code: gateway.codeFor('carla@example.org'),
|
||||
language: 'en',
|
||||
),
|
||||
);
|
||||
await settle();
|
||||
|
||||
await expectLater(
|
||||
passwords.deleteAccount(password: 'battery staple', language: 'en'),
|
||||
failsWith(PasswordProblem.wrongCredentials),
|
||||
);
|
||||
final mailed = await passwords.mailDeletionCode(language: 'en');
|
||||
expect(mailed.address, 'carla@example.org');
|
||||
expect(gateway.mailed.last.purpose, MailCodePurpose.deletion);
|
||||
await passwords.deleteAccount(
|
||||
password: 'battery staple',
|
||||
request: mailed.request,
|
||||
code: gateway.codeFor('carla@example.org'),
|
||||
language: 'en',
|
||||
);
|
||||
expect(gateway.accounts, isNot(contains('carla@example.org')));
|
||||
});
|
||||
|
||||
group('signing up', () {
|
||||
test('makes the account with the address a code proves, pending until '
|
||||
'its authenticator is set up', () async {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ abstract class DeletionProof
|
|||
required this.clientProof,
|
||||
this.totpCode,
|
||||
this.recoveryCode,
|
||||
this.request,
|
||||
this.code,
|
||||
required this.host,
|
||||
required this.address,
|
||||
required this.language,
|
||||
|
|
@ -37,6 +39,8 @@ abstract class DeletionProof
|
|||
required _idt.ByteData clientProof,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
_isc.UuidValue? request,
|
||||
String? code,
|
||||
required String host,
|
||||
required String address,
|
||||
required String language,
|
||||
|
|
@ -55,6 +59,10 @@ abstract class DeletionProof
|
|||
),
|
||||
totpCode: jsonSerialization['totpCode'] as String?,
|
||||
recoveryCode: jsonSerialization['recoveryCode'] as String?,
|
||||
request: jsonSerialization['request'] == null
|
||||
? null
|
||||
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['request']),
|
||||
code: jsonSerialization['code'] as String?,
|
||||
host: jsonSerialization['host'] as String,
|
||||
address: jsonSerialization['address'] as String,
|
||||
language: jsonSerialization['language'] as String,
|
||||
|
|
@ -71,6 +79,14 @@ abstract class DeletionProof
|
|||
|
||||
String? recoveryCode;
|
||||
|
||||
/// A code by mail asked for deleting (`EmailCodePurpose.deletion`), in
|
||||
/// place of the codes above where no second factor works: a setup never
|
||||
/// finished, or the authenticators' reset taken effect (the re-review's
|
||||
/// eighth finding).
|
||||
_isc.UuidValue? request;
|
||||
|
||||
String? code;
|
||||
|
||||
/// The host the device addressed the instance by: the server it reached
|
||||
/// checks it is its own before the register binds the proof to it.
|
||||
String host;
|
||||
|
|
@ -91,6 +107,8 @@ abstract class DeletionProof
|
|||
_idt.ByteData? clientProof,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
_isc.UuidValue? request,
|
||||
String? code,
|
||||
String? host,
|
||||
String? address,
|
||||
String? language,
|
||||
|
|
@ -104,6 +122,8 @@ abstract class DeletionProof
|
|||
'clientProof': clientProof.toJson(),
|
||||
if (totpCode != null) 'totpCode': totpCode,
|
||||
if (recoveryCode != null) 'recoveryCode': recoveryCode,
|
||||
if (request != null) 'request': request?.toJson(),
|
||||
if (code != null) 'code': code,
|
||||
'host': host,
|
||||
'address': address,
|
||||
'language': language,
|
||||
|
|
@ -119,6 +139,8 @@ abstract class DeletionProof
|
|||
'clientProof': clientProof.toJson(),
|
||||
if (totpCode != null) 'totpCode': totpCode,
|
||||
if (recoveryCode != null) 'recoveryCode': recoveryCode,
|
||||
if (request != null) 'request': request?.toJson(),
|
||||
if (code != null) 'code': code,
|
||||
'host': host,
|
||||
'address': address,
|
||||
'language': language,
|
||||
|
|
@ -140,6 +162,8 @@ class _DeletionProofImpl extends DeletionProof {
|
|||
required _idt.ByteData clientProof,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
_isc.UuidValue? request,
|
||||
String? code,
|
||||
required String host,
|
||||
required String address,
|
||||
required String language,
|
||||
|
|
@ -149,6 +173,8 @@ class _DeletionProofImpl extends DeletionProof {
|
|||
clientProof: clientProof,
|
||||
totpCode: totpCode,
|
||||
recoveryCode: recoveryCode,
|
||||
request: request,
|
||||
code: code,
|
||||
host: host,
|
||||
address: address,
|
||||
language: language,
|
||||
|
|
@ -164,6 +190,8 @@ class _DeletionProofImpl extends DeletionProof {
|
|||
_idt.ByteData? clientProof,
|
||||
Object? totpCode = _Undefined,
|
||||
Object? recoveryCode = _Undefined,
|
||||
Object? request = _Undefined,
|
||||
Object? code = _Undefined,
|
||||
String? host,
|
||||
String? address,
|
||||
String? language,
|
||||
|
|
@ -174,6 +202,8 @@ class _DeletionProofImpl extends DeletionProof {
|
|||
clientProof: clientProof ?? this.clientProof.clone(),
|
||||
totpCode: totpCode is String? ? totpCode : this.totpCode,
|
||||
recoveryCode: recoveryCode is String? ? recoveryCode : this.recoveryCode,
|
||||
request: request is _isc.UuidValue? ? request : this.request,
|
||||
code: code is String? ? code : this.code,
|
||||
host: host ?? this.host,
|
||||
address: address ?? this.address,
|
||||
language: language ?? this.language,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ abstract class DeletionProof
|
|||
required this.clientProof,
|
||||
this.totpCode,
|
||||
this.recoveryCode,
|
||||
this.request,
|
||||
this.code,
|
||||
required this.host,
|
||||
required this.address,
|
||||
required this.language,
|
||||
|
|
@ -37,6 +39,8 @@ abstract class DeletionProof
|
|||
required _idt.ByteData clientProof,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
_is.UuidValue? request,
|
||||
String? code,
|
||||
required String host,
|
||||
required String address,
|
||||
required String language,
|
||||
|
|
@ -55,6 +59,10 @@ abstract class DeletionProof
|
|||
),
|
||||
totpCode: jsonSerialization['totpCode'] as String?,
|
||||
recoveryCode: jsonSerialization['recoveryCode'] as String?,
|
||||
request: jsonSerialization['request'] == null
|
||||
? null
|
||||
: _is.UuidValueJsonExtension.fromJson(jsonSerialization['request']),
|
||||
code: jsonSerialization['code'] as String?,
|
||||
host: jsonSerialization['host'] as String,
|
||||
address: jsonSerialization['address'] as String,
|
||||
language: jsonSerialization['language'] as String,
|
||||
|
|
@ -71,6 +79,14 @@ abstract class DeletionProof
|
|||
|
||||
String? recoveryCode;
|
||||
|
||||
/// A code by mail asked for deleting (`EmailCodePurpose.deletion`), in
|
||||
/// place of the codes above where no second factor works: a setup never
|
||||
/// finished, or the authenticators' reset taken effect (the re-review's
|
||||
/// eighth finding).
|
||||
_is.UuidValue? request;
|
||||
|
||||
String? code;
|
||||
|
||||
/// The host the device addressed the instance by: the server it reached
|
||||
/// checks it is its own before the register binds the proof to it.
|
||||
String host;
|
||||
|
|
@ -91,6 +107,8 @@ abstract class DeletionProof
|
|||
_idt.ByteData? clientProof,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
_is.UuidValue? request,
|
||||
String? code,
|
||||
String? host,
|
||||
String? address,
|
||||
String? language,
|
||||
|
|
@ -104,6 +122,8 @@ abstract class DeletionProof
|
|||
'clientProof': clientProof.toJson(),
|
||||
if (totpCode != null) 'totpCode': totpCode,
|
||||
if (recoveryCode != null) 'recoveryCode': recoveryCode,
|
||||
if (request != null) 'request': request?.toJson(),
|
||||
if (code != null) 'code': code,
|
||||
'host': host,
|
||||
'address': address,
|
||||
'language': language,
|
||||
|
|
@ -119,6 +139,8 @@ abstract class DeletionProof
|
|||
'clientProof': clientProof.toJson(),
|
||||
if (totpCode != null) 'totpCode': totpCode,
|
||||
if (recoveryCode != null) 'recoveryCode': recoveryCode,
|
||||
if (request != null) 'request': request?.toJson(),
|
||||
if (code != null) 'code': code,
|
||||
'host': host,
|
||||
'address': address,
|
||||
'language': language,
|
||||
|
|
@ -140,6 +162,8 @@ class _DeletionProofImpl extends DeletionProof {
|
|||
required _idt.ByteData clientProof,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
_is.UuidValue? request,
|
||||
String? code,
|
||||
required String host,
|
||||
required String address,
|
||||
required String language,
|
||||
|
|
@ -149,6 +173,8 @@ class _DeletionProofImpl extends DeletionProof {
|
|||
clientProof: clientProof,
|
||||
totpCode: totpCode,
|
||||
recoveryCode: recoveryCode,
|
||||
request: request,
|
||||
code: code,
|
||||
host: host,
|
||||
address: address,
|
||||
language: language,
|
||||
|
|
@ -164,6 +190,8 @@ class _DeletionProofImpl extends DeletionProof {
|
|||
_idt.ByteData? clientProof,
|
||||
Object? totpCode = _Undefined,
|
||||
Object? recoveryCode = _Undefined,
|
||||
Object? request = _Undefined,
|
||||
Object? code = _Undefined,
|
||||
String? host,
|
||||
String? address,
|
||||
String? language,
|
||||
|
|
@ -174,6 +202,8 @@ class _DeletionProofImpl extends DeletionProof {
|
|||
clientProof: clientProof ?? this.clientProof.clone(),
|
||||
totpCode: totpCode is String? ? totpCode : this.totpCode,
|
||||
recoveryCode: recoveryCode is String? ? recoveryCode : this.recoveryCode,
|
||||
request: request is _is.UuidValue? ? request : this.request,
|
||||
code: code is String? ? code : this.code,
|
||||
host: host ?? this.host,
|
||||
address: address ?? this.address,
|
||||
language: language ?? this.language,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,12 @@ fields:
|
|||
clientProof: ByteData
|
||||
totpCode: String?
|
||||
recoveryCode: String?
|
||||
### A code by mail asked for deleting (`EmailCodePurpose.deletion`), in
|
||||
### place of the codes above where no second factor works: a setup never
|
||||
### finished, or the authenticators' reset taken effect (the re-review's
|
||||
### eighth finding).
|
||||
request: UuidValue?
|
||||
code: String?
|
||||
### The host the device addressed the instance by: the server it reached
|
||||
### checks it is its own before the register binds the proof to it.
|
||||
host: String
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:halkyon_base_server/src/auth/email.dart';
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart';
|
||||
import 'package:halkyon_base_server/src/password/password_config.dart';
|
||||
import 'package:halkyon_base_server/src/password/passwords.dart';
|
||||
|
|
@ -136,12 +137,22 @@ Future<void> setRecoveryKeys(
|
|||
}
|
||||
|
||||
/// Proves [person] may delete their account: one with a password only
|
||||
/// with [proof] (the password's proof and a code, bound to the host the
|
||||
/// device addressed, as a change of password takes: a session alone, or a
|
||||
/// phone with its authenticator, would otherwise delete the account and
|
||||
/// everything encrypted under its keys); one without, by its session alone,
|
||||
/// as before. Refused as [changePassword] refuses, and without [proof]
|
||||
/// where one is needed ([PasswordRefusal.wrongCredentials]).
|
||||
/// with [proof] (the password's proof bound to the host the device
|
||||
/// addressed, the account's address, and a code, as a change of password
|
||||
/// takes: a session alone, or a phone with its authenticator, would
|
||||
/// otherwise delete the account and everything encrypted under its keys);
|
||||
/// one without, by its session alone, as before.
|
||||
///
|
||||
/// The code is a TOTP or recovery code while the account's second factor
|
||||
/// works; where none does (a setup never finished, or the authenticators'
|
||||
/// reset taken effect), a code by mail to the account's address asked for
|
||||
/// deleting ([EmailCodePurpose.deletion]; the re-review's eighth finding:
|
||||
/// such accounts couldn't delete themselves at all).
|
||||
///
|
||||
/// Refused as [changePassword] refuses, without [proof] where one is
|
||||
/// needed, and without the code its account takes
|
||||
/// ([PasswordRefusal.wrongCredentials]); a code by mail's own refusals as
|
||||
/// `proveAddressCode`'s.
|
||||
Future<void> requireOwnerToDelete(
|
||||
Session session,
|
||||
UuidValue person,
|
||||
|
|
@ -159,6 +170,34 @@ Future<void> requireOwnerToDelete(
|
|||
if (proof == null) {
|
||||
throw refusedFor(PasswordRefusal.wrongCredentials);
|
||||
}
|
||||
if (!await _secondFactorWorks(session, person)) {
|
||||
final proved = await _provePasswordOwner(
|
||||
session,
|
||||
keys,
|
||||
person: person,
|
||||
challenge: proof.challenge,
|
||||
clientValue: proof.clientValue,
|
||||
clientProof: proof.clientProof,
|
||||
host: proof.host,
|
||||
address: proof.address,
|
||||
language: proof.language,
|
||||
);
|
||||
final request = proof.request;
|
||||
final code = proof.code?.trim() ?? '';
|
||||
if (request == null || code.isEmpty) {
|
||||
throw refusedFor(PasswordRefusal.wrongCredentials);
|
||||
}
|
||||
final proven = await proveAddressCode(
|
||||
session,
|
||||
request: request,
|
||||
code: code,
|
||||
purpose: EmailCodePurpose.deletion,
|
||||
);
|
||||
if (proven != proved.addressHash) {
|
||||
throw refusedFor(PasswordRefusal.wrongCredentials);
|
||||
}
|
||||
return;
|
||||
}
|
||||
await _proveOwner(
|
||||
session,
|
||||
keys,
|
||||
|
|
@ -174,6 +213,22 @@ Future<void> requireOwnerToDelete(
|
|||
);
|
||||
}
|
||||
|
||||
/// Whether [person]'s second factor works: they have a confirmed
|
||||
/// authenticator, and no reset of the authenticators took effect.
|
||||
Future<bool> _secondFactorWorks(Session session, UuidValue person) async {
|
||||
if (!await hasConfirmedAuthenticator(session, person)) {
|
||||
return false;
|
||||
}
|
||||
// SELECT * FROM "account_reset" WHERE "authUserId" = $1 AND "kind" = $2
|
||||
final reset = await AccountReset.db.findFirstRow(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.authUserId.equals(person) &
|
||||
t.kind.equals(AccountResetKind.authenticator),
|
||||
);
|
||||
return reset == null || reset.takesEffectAt.isAfter(clock.now());
|
||||
}
|
||||
|
||||
/// [person]'s password row, once they proved owning the account for a
|
||||
/// change: the old password's proof for [challenge], [person]'s own, and a
|
||||
/// [totpCode] or [recoveryCode] ([secondFactor], which also calls off a reset
|
||||
|
|
@ -197,24 +252,15 @@ Future<AccountPassword> _proveOwner(
|
|||
required String address,
|
||||
required String language,
|
||||
}) async {
|
||||
final proved = await provePassword(
|
||||
final proved = await _provePasswordOwner(
|
||||
session,
|
||||
keys,
|
||||
person: person,
|
||||
challenge: challenge,
|
||||
clientValue: clientValue,
|
||||
clientProof: clientProof,
|
||||
host: host,
|
||||
purpose: SignInChallengePurpose.change,
|
||||
);
|
||||
final row = proved.password;
|
||||
if (row.authUserId != person) {
|
||||
throw refusedFor(PasswordRefusal.wrongCredentials);
|
||||
}
|
||||
// Before any code is checked.
|
||||
final notice = requireNoticeTo(
|
||||
session.serverpod,
|
||||
proved.addressHash,
|
||||
typed: address,
|
||||
address: address,
|
||||
language: language,
|
||||
);
|
||||
if ((totpCode?.trim() ?? '').isEmpty &&
|
||||
|
|
@ -229,9 +275,50 @@ Future<AccountPassword> _proveOwner(
|
|||
person,
|
||||
totpCode: totpCode,
|
||||
recoveryCode: recoveryCode,
|
||||
notice: notice,
|
||||
notice: proved.notice,
|
||||
)) {
|
||||
throw refusedFor(PasswordRefusal.setupIncomplete);
|
||||
}
|
||||
return row;
|
||||
return proved.password;
|
||||
}
|
||||
|
||||
/// The password's proof for [challenge], [person]'s own and a change's,
|
||||
/// with the account's [address] (checked here, before any code) for the
|
||||
/// notice of codes waiting; refused ([PasswordRefusal.wrongCredentials])
|
||||
/// for a wrong password, a challenge that isn't [person]'s, or an address
|
||||
/// not theirs.
|
||||
Future<({AccountPassword password, String addressHash, CodesWaitNotice notice})>
|
||||
_provePasswordOwner(
|
||||
Session session,
|
||||
PasswordKeys keys, {
|
||||
required UuidValue person,
|
||||
required UuidValue challenge,
|
||||
required ByteData clientValue,
|
||||
required ByteData clientProof,
|
||||
required String host,
|
||||
required String address,
|
||||
required String language,
|
||||
}) async {
|
||||
final proved = await provePassword(
|
||||
session,
|
||||
keys,
|
||||
challenge: challenge,
|
||||
clientValue: clientValue,
|
||||
clientProof: clientProof,
|
||||
host: host,
|
||||
purpose: SignInChallengePurpose.change,
|
||||
);
|
||||
if (proved.password.authUserId != person) {
|
||||
throw refusedFor(PasswordRefusal.wrongCredentials);
|
||||
}
|
||||
return (
|
||||
password: proved.password,
|
||||
addressHash: proved.addressHash,
|
||||
notice: requireNoticeTo(
|
||||
session.serverpod,
|
||||
proved.addressHash,
|
||||
typed: address,
|
||||
language: language,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -710,6 +710,133 @@ void main() {
|
|||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'of an account whose setup never finished takes the password and a '
|
||||
'code by mail asked for deleting, nothing less, nor one asked for '
|
||||
'anything else',
|
||||
() async {
|
||||
final ben = await newAnonymousPerson(session);
|
||||
final bensLaptop = await signInNewDevice(session, ben);
|
||||
final bens = await PasswordDevice.make(seed: 7);
|
||||
TestSessionBuilder asBen() => sessionBuilder.copyWith(
|
||||
authentication: AuthenticationOverride.authenticationInfo(
|
||||
ben.uuid,
|
||||
{identified},
|
||||
authId: bensLaptop.uuid,
|
||||
),
|
||||
);
|
||||
final setUp = await mailedCode('ben@example.org');
|
||||
await endpoints.password.set(
|
||||
asBen(),
|
||||
setup: bens.setup(),
|
||||
request: setUp.request,
|
||||
code: setUp.code,
|
||||
);
|
||||
Future<void> deleteBen({
|
||||
EmailCodePurpose? mailedFor,
|
||||
String? totpCode,
|
||||
}) => atNow(() async {
|
||||
final mailed = mailedFor == null
|
||||
? null
|
||||
: await mailedCode('ben@example.org', purpose: mailedFor);
|
||||
final challenge = await endpoints.password.beginChange(asBen());
|
||||
final answer = bens.answer(challenge);
|
||||
await endpoints.account.delete(
|
||||
asBen(),
|
||||
proof: DeletionProof(
|
||||
challenge: challenge.challenge,
|
||||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: totpCode,
|
||||
request: mailed?.request,
|
||||
code: mailed?.code,
|
||||
host: testHost,
|
||||
address: 'ben@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
await expectLater(
|
||||
deleteBen(),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
await expectLater(
|
||||
deleteBen(totpCode: '123456'),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
await expectLater(
|
||||
deleteBen(mailedFor: EmailCodePurpose.signUp),
|
||||
refusedAsEmail(EmailSignInRefusal.codeExpired),
|
||||
);
|
||||
expect(await AuthUser.db.findById(session, ben), isNotNull);
|
||||
|
||||
await deleteBen(mailedFor: EmailCodePurpose.deletion);
|
||||
expect(await AuthUser.db.findById(session, ben), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'once the reset of the authenticators took effect, takes the '
|
||||
'password and a code by mail asked for deleting',
|
||||
() async {
|
||||
await atNow(() async {
|
||||
final challenge = await endpoints.passwordSignIn.begin(
|
||||
notSignedIn(),
|
||||
address: 'anna@example.org',
|
||||
);
|
||||
final answer = device.answer(challenge);
|
||||
final code = await mailedCode(
|
||||
'anna@example.org',
|
||||
purpose: EmailCodePurpose.authenticatorReset,
|
||||
);
|
||||
await endpoints.accountRecovery.resetAuthenticator(
|
||||
notSignedIn(),
|
||||
challenge: challenge.challenge,
|
||||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
request: code.request,
|
||||
code: code.code,
|
||||
host: testHost,
|
||||
);
|
||||
});
|
||||
// Before it takes effect, the second factor works: no code
|
||||
// from it, no deleting.
|
||||
await expectLater(
|
||||
delete(withoutCode: true),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
now = now.add(resetDelay);
|
||||
|
||||
await atNow(() async {
|
||||
final mailed = await mailedCode(
|
||||
'anna@example.org',
|
||||
purpose: EmailCodePurpose.deletion,
|
||||
);
|
||||
final challenge = await endpoints.password.beginChange(
|
||||
onPhone(),
|
||||
);
|
||||
final answer = device.answer(challenge);
|
||||
await endpoints.account.delete(
|
||||
onPhone(),
|
||||
proof: DeletionProof(
|
||||
challenge: challenge.challenge,
|
||||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
request: mailed.request,
|
||||
code: mailed.code,
|
||||
host: testHost,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
);
|
||||
});
|
||||
expect(await AuthUser.db.findById(session, anna), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'of an anonymous person takes its session alone, as before',
|
||||
() async {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@
|
|||
import 'dart:async' as _ida;
|
||||
import 'dart:io' as _idi;
|
||||
import 'dart:typed_data' as _idt;
|
||||
|
||||
import 'package:halkyon_base_server/src/generated/auth/email_code_purpose.dart'
|
||||
as _i5775c0l;
|
||||
import 'package:halkyon_base_server/src/generated/auth/sign_in_ways.dart'
|
||||
|
|
@ -293,10 +292,12 @@ class _AccountEndpoint {
|
|||
parameters: _ist.testObjectToJson({'proof': proof}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<void>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<void>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -321,10 +322,12 @@ class _AccountEndpoint {
|
|||
parameters: _ist.testObjectToJson({}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<_ik4ah9vo.SignInWays>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_ik4ah9vo.SignInWays>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -361,10 +364,12 @@ class _AnonymousIdpEndpoint {
|
|||
parameters: _ist.testObjectToJson({'token': token}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<_iacs.AuthSuccess>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_iacs.AuthSuccess>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -407,10 +412,12 @@ class _EmailCodeEndpoint {
|
|||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<_is.UuidValue>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_is.UuidValue>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -453,10 +460,12 @@ class _GitHubSignInEndpoint {
|
|||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<_iacs.AuthSuccess>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_iacs.AuthSuccess>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -499,10 +508,12 @@ class _GitHubUpgradeEndpoint {
|
|||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<void>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<void>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -538,10 +549,12 @@ class _InstanceEndpoint {
|
|||
parameters: _ist.testObjectToJson({}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<_ihusd8tw.InstanceBasics>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_ihusd8tw.InstanceBasics>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -598,10 +611,12 @@ class _AccountRecoveryEndpoint {
|
|||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<_isakxto6.ResetPending>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_isakxto6.ResetPending>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -627,10 +642,12 @@ class _AccountRecoveryEndpoint {
|
|||
parameters: _ist.testObjectToJson({'address': address}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<_iaysae70.KeyProofChallenge>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_iaysae70.KeyProofChallenge>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -672,10 +689,12 @@ class _AccountRecoveryEndpoint {
|
|||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<_iacs.AuthSuccess>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_iacs.AuthSuccess>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -709,10 +728,12 @@ class _AccountRecoveryEndpoint {
|
|||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<_isakxto6.ResetPending>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_isakxto6.ResetPending>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -746,10 +767,12 @@ class _AccountRecoveryEndpoint {
|
|||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<_iacs.AuthSuccess>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_iacs.AuthSuccess>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -790,10 +813,12 @@ class _AuthenticatorEndpoint {
|
|||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<_id229ey1.AuthenticatorEnrolment>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_id229ey1.AuthenticatorEnrolment>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -825,10 +850,12 @@ class _AuthenticatorEndpoint {
|
|||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<List<String>>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<List<String>>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -853,10 +880,12 @@ class _AuthenticatorEndpoint {
|
|||
parameters: _ist.testObjectToJson({}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<List<_ip34xal6.Authenticator>>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<List<_ip34xal6.Authenticator>>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -886,10 +915,12 @@ class _AuthenticatorEndpoint {
|
|||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<void>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<void>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -915,10 +946,12 @@ class _AuthenticatorEndpoint {
|
|||
parameters: _ist.testObjectToJson({'code': code}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<List<String>>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<List<String>>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -961,10 +994,12 @@ class _PasswordEndpoint {
|
|||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<void>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<void>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -989,10 +1024,12 @@ class _PasswordEndpoint {
|
|||
parameters: _ist.testObjectToJson({}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<_isz0atlx.PasswordChallenge>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_isz0atlx.PasswordChallenge>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -1036,10 +1073,12 @@ class _PasswordEndpoint {
|
|||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<void>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<void>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -1085,10 +1124,12 @@ class _PasswordEndpoint {
|
|||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<void>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<void>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -1125,10 +1166,12 @@ class _PasswordSignInEndpoint {
|
|||
parameters: _ist.testObjectToJson({'address': address}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<_isz0atlx.PasswordChallenge>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_isz0atlx.PasswordChallenge>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -1174,10 +1217,12 @@ class _PasswordSignInEndpoint {
|
|||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<_ikpyb5fh.PasswordSignedIn>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_ikpyb5fh.PasswordSignedIn>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -1209,10 +1254,12 @@ class _PasswordSignInEndpoint {
|
|||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<_iacs.AuthSuccess>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_iacs.AuthSuccess>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -1248,10 +1295,12 @@ class _ProfileEndpoint {
|
|||
parameters: _ist.testObjectToJson({}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<_it4tom37.Profile?>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_it4tom37.Profile?>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -1285,10 +1334,12 @@ class _ProfileEndpoint {
|
|||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<_it4tom37.Profile>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_it4tom37.Profile>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -1313,10 +1364,12 @@ class _ProfileEndpoint {
|
|||
parameters: _ist.testObjectToJson({}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<_it4tom37.Profile>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_it4tom37.Profile>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -1342,10 +1395,12 @@ class _ProfileEndpoint {
|
|||
parameters: _ist.testObjectToJson({'handle': handle}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<_idszjcpz.ProfileRefusal?>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_idszjcpz.ProfileRefusal?>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -1371,10 +1426,12 @@ class _ProfileEndpoint {
|
|||
parameters: _ist.testObjectToJson({'png': png}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<_it4tom37.Profile>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_it4tom37.Profile>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -1399,10 +1456,12 @@ class _ProfileEndpoint {
|
|||
parameters: _ist.testObjectToJson({}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<_it4tom37.Profile>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_it4tom37.Profile>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -1425,10 +1484,12 @@ class _ProfileEndpoint {
|
|||
parameters: _ist.testObjectToJson({}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<void>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<void>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -1465,10 +1526,12 @@ class _ProfilesEndpoint {
|
|||
parameters: _ist.testObjectToJson({'handle': handle}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue = await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
) as _ida.Future<_it4tom37.Profile>);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_it4tom37.Profile>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
|
|||
Loading…
Reference in a new issue