fix(auth): every call with a code after the password takes the address
Whoever guessed codes decided whether the owner was mailed (the re-review's fourth finding): the address a sign-in sent was optional, so a guesser left it out and no mail went, and a wait begun without a mail used up the one mail there was. Every call carrying a code after the password or the phrase proved (signing in, the phrase's reset, a change of password, a new phrase, deletion) now takes the account's address and the language of its mails, and is refused (wrongCredentials, as anything wrong after the password) before any code is checked when the address isn't the account's; every wait mails the owner, not only the first. The app sends the address the person typed, or for changes while signed in the one this device signed in with, and says so where it doesn't know it (addressUnknown). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
e8c4a3d472
commit
2a21672b43
36 changed files with 795 additions and 347 deletions
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
import '../auth/email_sign_in_refusal.dart' as _in9x16lz;
|
||||
|
||||
/// Mailing a code, accepting it, or signing in with an email address was
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
import '../auth/github_sign_in_refusal.dart' as _irgbwy1y;
|
||||
|
||||
/// Signing in with GitHub, or linking a GitHub account, was refused; nothing
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:typed_data' as _idt;
|
||||
|
||||
import 'package:halkyon_base_server/src/generated/password/deletion_proof.dart'
|
||||
as _i619l6vv;
|
||||
import 'package:halkyon_base_server/src/generated/password/new_password.dart'
|
||||
|
|
@ -24,6 +25,7 @@ import 'package:serverpod_auth_core_server/serverpod_auth_core_server.dart'
|
|||
as _iacs;
|
||||
import 'package:serverpod_auth_idp_server/serverpod_auth_idp_server.dart'
|
||||
as _iais;
|
||||
|
||||
import '../auth/account_endpoint.dart' as _ii6ycf6v;
|
||||
import '../auth/anonymous_idp_endpoint.dart' as _in0zita6;
|
||||
import '../auth/email_code_endpoint.dart' as _in3cimlf;
|
||||
|
|
@ -433,13 +435,13 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
),
|
||||
'address': _is.ParameterDescription(
|
||||
name: 'address',
|
||||
type: _is.getType<String?>(),
|
||||
nullable: true,
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'language': _is.ParameterDescription(
|
||||
name: 'language',
|
||||
type: _is.getType<String?>(),
|
||||
nullable: true,
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
|
|
@ -750,6 +752,16 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'address': _is.ParameterDescription(
|
||||
name: 'address',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'language': _is.ParameterDescription(
|
||||
name: 'language',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
|
|
@ -765,6 +777,8 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
recoveryCode: params['recoveryCode'],
|
||||
password: params['password'],
|
||||
host: params['host'],
|
||||
address: params['address'],
|
||||
language: params['language'],
|
||||
),
|
||||
),
|
||||
'setRecoveryKeys': _is.MethodConnector(
|
||||
|
|
@ -810,6 +824,16 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'address': _is.ParameterDescription(
|
||||
name: 'address',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'language': _is.ParameterDescription(
|
||||
name: 'language',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
|
|
@ -826,6 +850,8 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
recoveryKeys: params['recoveryKeys'],
|
||||
recoveryProofKey: params['recoveryProofKey'],
|
||||
host: params['host'],
|
||||
address: params['address'],
|
||||
language: params['language'],
|
||||
),
|
||||
),
|
||||
},
|
||||
|
|
@ -900,13 +926,13 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
),
|
||||
'address': _is.ParameterDescription(
|
||||
name: 'address',
|
||||
type: _is.getType<String?>(),
|
||||
nullable: true,
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'language': _is.ParameterDescription(
|
||||
name: 'language',
|
||||
type: _is.getType<String?>(),
|
||||
nullable: true,
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:typed_data' as _idt;
|
||||
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
/// An account's password as the register keeps it (ADR 0017): nothing that
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
import '../password/account_reset_kind.dart' as _itrk9fro;
|
||||
|
||||
/// A reset asked for that takes effect only later (ADR 0017), so the
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:typed_data' as _idt;
|
||||
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
/// What deleting an account that has a password takes (ADR 0017; the second
|
||||
|
|
@ -27,6 +28,8 @@ abstract class DeletionProof
|
|||
this.totpCode,
|
||||
this.recoveryCode,
|
||||
required this.host,
|
||||
required this.address,
|
||||
required this.language,
|
||||
});
|
||||
|
||||
factory DeletionProof({
|
||||
|
|
@ -36,6 +39,8 @@ abstract class DeletionProof
|
|||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
required String host,
|
||||
required String address,
|
||||
required String language,
|
||||
}) = _DeletionProofImpl;
|
||||
|
||||
factory DeletionProof.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
|
|
@ -52,6 +57,8 @@ abstract class DeletionProof
|
|||
totpCode: jsonSerialization['totpCode'] as String?,
|
||||
recoveryCode: jsonSerialization['recoveryCode'] as String?,
|
||||
host: jsonSerialization['host'] as String,
|
||||
address: jsonSerialization['address'] as String,
|
||||
language: jsonSerialization['language'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -69,6 +76,13 @@ abstract class DeletionProof
|
|||
/// checks it is its own before the register binds the proof to it.
|
||||
String host;
|
||||
|
||||
/// The account's address as the device typed it, and the language its
|
||||
/// mails are worded in: checked before the code, so the owner is mailed
|
||||
/// should codes come to wait (the re-review's fourth finding).
|
||||
String address;
|
||||
|
||||
String language;
|
||||
|
||||
/// Returns a shallow copy of this [DeletionProof]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
|
|
@ -79,6 +93,8 @@ abstract class DeletionProof
|
|||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
String? host,
|
||||
String? address,
|
||||
String? language,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
|
|
@ -90,6 +106,8 @@ abstract class DeletionProof
|
|||
if (totpCode != null) 'totpCode': totpCode,
|
||||
if (recoveryCode != null) 'recoveryCode': recoveryCode,
|
||||
'host': host,
|
||||
'address': address,
|
||||
'language': language,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -103,6 +121,8 @@ abstract class DeletionProof
|
|||
if (totpCode != null) 'totpCode': totpCode,
|
||||
if (recoveryCode != null) 'recoveryCode': recoveryCode,
|
||||
'host': host,
|
||||
'address': address,
|
||||
'language': language,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -122,6 +142,8 @@ class _DeletionProofImpl extends DeletionProof {
|
|||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
required String host,
|
||||
required String address,
|
||||
required String language,
|
||||
}) : super._(
|
||||
challenge: challenge,
|
||||
clientValue: clientValue,
|
||||
|
|
@ -129,6 +151,8 @@ class _DeletionProofImpl extends DeletionProof {
|
|||
totpCode: totpCode,
|
||||
recoveryCode: recoveryCode,
|
||||
host: host,
|
||||
address: address,
|
||||
language: language,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [DeletionProof]
|
||||
|
|
@ -142,6 +166,8 @@ class _DeletionProofImpl extends DeletionProof {
|
|||
Object? totpCode = _Undefined,
|
||||
Object? recoveryCode = _Undefined,
|
||||
String? host,
|
||||
String? address,
|
||||
String? language,
|
||||
}) {
|
||||
return DeletionProof(
|
||||
challenge: challenge ?? this.challenge,
|
||||
|
|
@ -150,6 +176,8 @@ class _DeletionProofImpl extends DeletionProof {
|
|||
totpCode: totpCode is String? ? totpCode : this.totpCode,
|
||||
recoveryCode: recoveryCode is String? ? recoveryCode : this.recoveryCode,
|
||||
host: host ?? this.host,
|
||||
address: address ?? this.address,
|
||||
language: language ?? this.language,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@
|
|||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:typed_data' as _idt;
|
||||
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart' as _ibewtqgw;
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
import '../password/wrapped_keys.dart' as _i2rxumte;
|
||||
|
||||
/// What a device needs to prove the recovery phrase (ADR 0017): the
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@
|
|||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:typed_data' as _idt;
|
||||
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart' as _ibewtqgw;
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
import '../password/password_costs.dart' as _ioe6tior;
|
||||
import '../password/wrapped_keys.dart' as _i2rxumte;
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@
|
|||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:typed_data' as _idt;
|
||||
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart' as _ibewtqgw;
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
import '../password/password_costs.dart' as _ioe6tior;
|
||||
|
||||
/// The register's answer to the first step of signing in with a password
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@
|
|||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:typed_data' as _idt;
|
||||
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart' as _ibewtqgw;
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
import '../password/wrapped_keys.dart' as _i2rxumte;
|
||||
|
||||
/// What the register gives a member (or itself) once a password sign-in's
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
import '../password/password_refusal.dart' as _i647hpdf;
|
||||
|
||||
/// Something to do with a password, an authenticator or a reset was refused
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@
|
|||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:typed_data' as _idt;
|
||||
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart' as _ibewtqgw;
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
import '../password/password_costs.dart' as _ioe6tior;
|
||||
import '../password/wrapped_keys.dart' as _i2rxumte;
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@
|
|||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:typed_data' as _idt;
|
||||
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart' as _ibewtqgw;
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
import '../password/wrapped_keys.dart' as _i2rxumte;
|
||||
|
||||
/// Signed in with a password (ADR 0017): SRP's proof M2, which the device
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
import '../password/sign_in_challenge_purpose.dart' as _i7xlz4xk;
|
||||
|
||||
/// A step a device began and must finish within two minutes, once (ADR
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:typed_data' as _idt;
|
||||
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
/// An authenticator app an account's TOTP codes come from (ADR 0017): its
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:typed_data' as _idt;
|
||||
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
/// An account's private keys sealed under one key (ADR 0017), as
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
import '../profile/profile_refusal.dart' as _i5pl5k19;
|
||||
|
||||
/// Saving or removing a public profile was refused; nothing changed.
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import 'package:serverpod_auth_core_server/serverpod_auth_core_server.dart'
|
|||
as _iacs;
|
||||
import 'package:serverpod_auth_idp_server/serverpod_auth_idp_server.dart'
|
||||
as _iais;
|
||||
|
||||
import 'auth/email_code_request.dart' as _iqxum1j8;
|
||||
import 'auth/email_identity.dart' as _id1dpd4g;
|
||||
import 'auth/email_sign_in_refusal.dart' as _ieyvcag1;
|
||||
|
|
|
|||
|
|
@ -110,10 +110,13 @@ Future<ResetPending> resetAuthenticator(
|
|||
/// them, [secondFactor], which calls off every reset waiting): the account's
|
||||
/// keys stay, wrapped under the new password's key by the device. Every
|
||||
/// session of the account here ends. Returns the account, for the caller's
|
||||
/// session.
|
||||
/// session. [address] and [language]: the account's address, as the device
|
||||
/// typed it, checked before the code (the owner is mailed should codes come
|
||||
/// to wait).
|
||||
///
|
||||
/// Refused ([PasswordRefusedException]) for a new password out of range
|
||||
/// ([PasswordRefusal.invalidSetup]), and as [proveRecoveryPhrase] and
|
||||
/// ([PasswordRefusal.invalidSetup]), for [address] not the account's
|
||||
/// ([PasswordRefusal.wrongCredentials]), and as [proveRecoveryPhrase] and
|
||||
/// [secondFactor] refuse.
|
||||
Future<AccountSessionsEnded> resetPasswordWithPhrase(
|
||||
Session session, {
|
||||
|
|
@ -123,8 +126,8 @@ Future<AccountSessionsEnded> resetPasswordWithPhrase(
|
|||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
required String host,
|
||||
String? address,
|
||||
String? language,
|
||||
required String address,
|
||||
required String language,
|
||||
}) async {
|
||||
final keys = requirePasswordKeys(session.serverpod);
|
||||
if (!isNewPassword(password)) {
|
||||
|
|
@ -140,15 +143,15 @@ Future<AccountSessionsEnded> resetPasswordWithPhrase(
|
|||
digest: newPasswordDigest(password),
|
||||
);
|
||||
final account = row.authUserId;
|
||||
// Before any code is checked.
|
||||
final notice = await _requireNoticeOf(session, account, address, language);
|
||||
await secondFactor(
|
||||
session,
|
||||
keys,
|
||||
account,
|
||||
totpCode: totpCode,
|
||||
recoveryCode: recoveryCode,
|
||||
notice: address == null
|
||||
? null
|
||||
: await _noticeOf(session, account, address, language),
|
||||
notice: notice,
|
||||
);
|
||||
final endedBefore = await session.db.transaction((transaction) async {
|
||||
await writeNewPassword(session, row, password, transaction: transaction);
|
||||
|
|
@ -383,25 +386,28 @@ Future<ResetPending> _askForReset(
|
|||
});
|
||||
}
|
||||
|
||||
/// The notice of codes waiting for [account] to [typed], the address a
|
||||
/// device sent, if it is the account's; null otherwise.
|
||||
Future<CodesWaitNotice?> _noticeOf(
|
||||
/// The notice of codes waiting for [account], to [typed] if it is the
|
||||
/// account's address; refused ([PasswordRefusal.wrongCredentials])
|
||||
/// otherwise, before any code is checked, as [requireNoticeTo].
|
||||
Future<CodesWaitNotice> _requireNoticeOf(
|
||||
Session session,
|
||||
UuidValue account,
|
||||
String typed,
|
||||
String? language,
|
||||
String language,
|
||||
) async {
|
||||
final to = normalizedAddress(typed);
|
||||
if (to == null) {
|
||||
return null;
|
||||
}
|
||||
// SELECT * FROM "email_identity"
|
||||
// WHERE "addressHash" = $1 AND "authUserId" = $2
|
||||
final identity = await EmailIdentity.db.findFirstRow(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.addressHash.equals(addressHash(session.serverpod, to)) &
|
||||
t.authUserId.equals(account),
|
||||
);
|
||||
return identity == null ? null : (to: to, language: language ?? 'en');
|
||||
final identity = to == null
|
||||
? null
|
||||
: await EmailIdentity.db.findFirstRow(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.addressHash.equals(addressHash(session.serverpod, to)) &
|
||||
t.authUserId.equals(account),
|
||||
);
|
||||
if (to == null || identity == null) {
|
||||
throw refusedFor(PasswordRefusal.wrongCredentials);
|
||||
}
|
||||
return (to: to, language: language);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,8 +71,8 @@ class AccountRecoveryEndpoint extends Endpoint {
|
|||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
required String host,
|
||||
String? address,
|
||||
String? language,
|
||||
required String address,
|
||||
required String language,
|
||||
}) async {
|
||||
final ended = await AccountRegister.of(session.serverpod)
|
||||
.resetPasswordWithPhrase(
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue