feat(auth): a session ended for its wrong proofs warns its owner
The tenth wrong owner proof in a row ends a session (the third reading); someone holding that session tried the password or its codes. The register now keeps the moment with the account's password (`account_password.sessionEndedForProofsAt`): the account's sign-in ways and its next password sign-in carry it, until the owner dismisses it (`password.dismissSessionEndedWarning`, their session alone: it hides a warning and secures nothing) or sets a new password. A member ending its own session tells the register (`noteSessionEndedForProofs`); a register out of reach leaves the owner unwarned, the session ended all the same. Migration (module, Learn's and Todo's servers, Learn's device schema): ALTER TABLE "account_password" ADD COLUMN "sessionEndedForProofsAt" timestamp without time zone; nullable, so every row keeps its values and has no warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
858fffdce3
commit
5e7d7f4e36
3 changed files with 41919 additions and 34 deletions
|
|
@ -22,6 +22,7 @@ abstract class SignInWays
|
|||
bool? authenticator,
|
||||
bool? passwordOnly,
|
||||
bool? secondFactorWorks,
|
||||
this.sessionEndedForProofsAt,
|
||||
}) : password = password ?? false,
|
||||
authenticator = authenticator ?? false,
|
||||
passwordOnly = passwordOnly ?? false,
|
||||
|
|
@ -34,6 +35,7 @@ abstract class SignInWays
|
|||
bool? authenticator,
|
||||
bool? passwordOnly,
|
||||
bool? secondFactorWorks,
|
||||
DateTime? sessionEndedForProofsAt,
|
||||
}) = _SignInWaysImpl;
|
||||
|
||||
factory SignInWays.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
|
|
@ -54,6 +56,12 @@ abstract class SignInWays
|
|||
: _isc.BoolJsonExtension.fromJson(
|
||||
jsonSerialization['secondFactorWorks'],
|
||||
),
|
||||
sessionEndedForProofsAt:
|
||||
jsonSerialization['sessionEndedForProofsAt'] == null
|
||||
? null
|
||||
: _isc.DateTimeJsonExtension.fromJson(
|
||||
jsonSerialization['sessionEndedForProofsAt'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -77,6 +85,11 @@ abstract class SignInWays
|
|||
/// takes a code by mail in place of a TOTP or recovery code.
|
||||
bool secondFactorWorks;
|
||||
|
||||
/// When a session of the account was last ended for ten wrong proofs in
|
||||
/// a row, the owner's warning until they dismiss it or set a new
|
||||
/// password (ADR 0017, the fourth reading); null for none.
|
||||
DateTime? sessionEndedForProofsAt;
|
||||
|
||||
/// Returns a shallow copy of this [SignInWays]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
|
|
@ -87,6 +100,7 @@ abstract class SignInWays
|
|||
bool? authenticator,
|
||||
bool? passwordOnly,
|
||||
bool? secondFactorWorks,
|
||||
DateTime? sessionEndedForProofsAt,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
|
|
@ -98,6 +112,8 @@ abstract class SignInWays
|
|||
'authenticator': authenticator,
|
||||
'passwordOnly': passwordOnly,
|
||||
'secondFactorWorks': secondFactorWorks,
|
||||
if (sessionEndedForProofsAt != null)
|
||||
'sessionEndedForProofsAt': sessionEndedForProofsAt?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -111,6 +127,8 @@ abstract class SignInWays
|
|||
'authenticator': authenticator,
|
||||
'passwordOnly': passwordOnly,
|
||||
'secondFactorWorks': secondFactorWorks,
|
||||
if (sessionEndedForProofsAt != null)
|
||||
'sessionEndedForProofsAt': sessionEndedForProofsAt?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -120,6 +138,8 @@ abstract class SignInWays
|
|||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _SignInWaysImpl extends SignInWays {
|
||||
_SignInWaysImpl({
|
||||
required bool gitHub,
|
||||
|
|
@ -128,6 +148,7 @@ class _SignInWaysImpl extends SignInWays {
|
|||
bool? authenticator,
|
||||
bool? passwordOnly,
|
||||
bool? secondFactorWorks,
|
||||
DateTime? sessionEndedForProofsAt,
|
||||
}) : super._(
|
||||
gitHub: gitHub,
|
||||
email: email,
|
||||
|
|
@ -135,6 +156,7 @@ class _SignInWaysImpl extends SignInWays {
|
|||
authenticator: authenticator,
|
||||
passwordOnly: passwordOnly,
|
||||
secondFactorWorks: secondFactorWorks,
|
||||
sessionEndedForProofsAt: sessionEndedForProofsAt,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [SignInWays]
|
||||
|
|
@ -148,6 +170,7 @@ class _SignInWaysImpl extends SignInWays {
|
|||
bool? authenticator,
|
||||
bool? passwordOnly,
|
||||
bool? secondFactorWorks,
|
||||
Object? sessionEndedForProofsAt = _Undefined,
|
||||
}) {
|
||||
return SignInWays(
|
||||
gitHub: gitHub ?? this.gitHub,
|
||||
|
|
@ -156,6 +179,9 @@ class _SignInWaysImpl extends SignInWays {
|
|||
authenticator: authenticator ?? this.authenticator,
|
||||
passwordOnly: passwordOnly ?? this.passwordOnly,
|
||||
secondFactorWorks: secondFactorWorks ?? this.secondFactorWorks,
|
||||
sessionEndedForProofsAt: sessionEndedForProofsAt is DateTime?
|
||||
? sessionEndedForProofsAt
|
||||
: this.sessionEndedForProofsAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -506,6 +506,17 @@ class EndpointPassword extends _isc.EndpointRef {
|
|||
'recoveryProofKey': recoveryProofKey,
|
||||
},
|
||||
);
|
||||
|
||||
/// Dismisses the caller's warning that a session of theirs was ended for
|
||||
/// ten wrong proofs in a row (`account.signInWays`'
|
||||
/// `sessionEndedForProofsAt`). Their session alone does: it hides a
|
||||
/// warning and changes nothing about how the account is secured.
|
||||
_ida.Future<void> dismissSessionEndedWarning() =>
|
||||
caller.callServerEndpoint<void>(
|
||||
'halkyon_base.password',
|
||||
'dismissSessionEndedWarning',
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
/// Signing in with an address, a password and a code from an authenticator
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ abstract class PasswordSignedIn
|
|||
required this.signingPublicKey,
|
||||
required this.passwordKeys,
|
||||
required this.needsAuthenticator,
|
||||
this.sessionEndedForProofsAt,
|
||||
required this.authStrategy,
|
||||
required this.token,
|
||||
this.tokenExpiresAt,
|
||||
|
|
@ -43,6 +44,7 @@ abstract class PasswordSignedIn
|
|||
required _idt.ByteData signingPublicKey,
|
||||
required _i2rxumte.WrappedKeys passwordKeys,
|
||||
required bool needsAuthenticator,
|
||||
DateTime? sessionEndedForProofsAt,
|
||||
required String authStrategy,
|
||||
required String token,
|
||||
DateTime? tokenExpiresAt,
|
||||
|
|
@ -68,6 +70,12 @@ abstract class PasswordSignedIn
|
|||
needsAuthenticator: _isc.BoolJsonExtension.fromJson(
|
||||
jsonSerialization['needsAuthenticator'],
|
||||
),
|
||||
sessionEndedForProofsAt:
|
||||
jsonSerialization['sessionEndedForProofsAt'] == null
|
||||
? null
|
||||
: _isc.DateTimeJsonExtension.fromJson(
|
||||
jsonSerialization['sessionEndedForProofsAt'],
|
||||
),
|
||||
authStrategy: jsonSerialization['authStrategy'] as String,
|
||||
token: jsonSerialization['token'] as String,
|
||||
tokenExpiresAt: jsonSerialization['tokenExpiresAt'] == null
|
||||
|
|
@ -95,6 +103,11 @@ abstract class PasswordSignedIn
|
|||
|
||||
bool needsAuthenticator;
|
||||
|
||||
/// When a session of the account was last ended for ten wrong proofs in
|
||||
/// a row (someone with a session tried the password or its codes), until
|
||||
/// the owner dismisses the warning or sets a new password; null for none.
|
||||
DateTime? sessionEndedForProofsAt;
|
||||
|
||||
/// The session, as Serverpod's `AuthSuccess` has it (the device makes one
|
||||
/// of these fields for its session manager).
|
||||
String authStrategy;
|
||||
|
|
@ -118,6 +131,7 @@ abstract class PasswordSignedIn
|
|||
_idt.ByteData? signingPublicKey,
|
||||
_i2rxumte.WrappedKeys? passwordKeys,
|
||||
bool? needsAuthenticator,
|
||||
DateTime? sessionEndedForProofsAt,
|
||||
String? authStrategy,
|
||||
String? token,
|
||||
DateTime? tokenExpiresAt,
|
||||
|
|
@ -134,6 +148,8 @@ abstract class PasswordSignedIn
|
|||
'signingPublicKey': signingPublicKey.toJson(),
|
||||
'passwordKeys': passwordKeys.toJson(),
|
||||
'needsAuthenticator': needsAuthenticator,
|
||||
if (sessionEndedForProofsAt != null)
|
||||
'sessionEndedForProofsAt': sessionEndedForProofsAt?.toJson(),
|
||||
'authStrategy': authStrategy,
|
||||
'token': token,
|
||||
if (tokenExpiresAt != null) 'tokenExpiresAt': tokenExpiresAt?.toJson(),
|
||||
|
|
@ -152,6 +168,8 @@ abstract class PasswordSignedIn
|
|||
'signingPublicKey': signingPublicKey.toJson(),
|
||||
'passwordKeys': passwordKeys.toJsonForProtocol(),
|
||||
'needsAuthenticator': needsAuthenticator,
|
||||
if (sessionEndedForProofsAt != null)
|
||||
'sessionEndedForProofsAt': sessionEndedForProofsAt?.toJson(),
|
||||
'authStrategy': authStrategy,
|
||||
'token': token,
|
||||
if (tokenExpiresAt != null) 'tokenExpiresAt': tokenExpiresAt?.toJson(),
|
||||
|
|
@ -176,6 +194,7 @@ class _PasswordSignedInImpl extends PasswordSignedIn {
|
|||
required _idt.ByteData signingPublicKey,
|
||||
required _i2rxumte.WrappedKeys passwordKeys,
|
||||
required bool needsAuthenticator,
|
||||
DateTime? sessionEndedForProofsAt,
|
||||
required String authStrategy,
|
||||
required String token,
|
||||
DateTime? tokenExpiresAt,
|
||||
|
|
@ -188,6 +207,7 @@ class _PasswordSignedInImpl extends PasswordSignedIn {
|
|||
signingPublicKey: signingPublicKey,
|
||||
passwordKeys: passwordKeys,
|
||||
needsAuthenticator: needsAuthenticator,
|
||||
sessionEndedForProofsAt: sessionEndedForProofsAt,
|
||||
authStrategy: authStrategy,
|
||||
token: token,
|
||||
tokenExpiresAt: tokenExpiresAt,
|
||||
|
|
@ -206,6 +226,7 @@ class _PasswordSignedInImpl extends PasswordSignedIn {
|
|||
_idt.ByteData? signingPublicKey,
|
||||
_i2rxumte.WrappedKeys? passwordKeys,
|
||||
bool? needsAuthenticator,
|
||||
Object? sessionEndedForProofsAt = _Undefined,
|
||||
String? authStrategy,
|
||||
String? token,
|
||||
Object? tokenExpiresAt = _Undefined,
|
||||
|
|
@ -220,6 +241,9 @@ class _PasswordSignedInImpl extends PasswordSignedIn {
|
|||
signingPublicKey: signingPublicKey ?? this.signingPublicKey.clone(),
|
||||
passwordKeys: passwordKeys ?? this.passwordKeys.copyWith(),
|
||||
needsAuthenticator: needsAuthenticator ?? this.needsAuthenticator,
|
||||
sessionEndedForProofsAt: sessionEndedForProofsAt is DateTime?
|
||||
? sessionEndedForProofsAt
|
||||
: this.sessionEndedForProofsAt,
|
||||
authStrategy: authStrategy ?? this.authStrategy,
|
||||
token: token ?? this.token,
|
||||
tokenExpiresAt: tokenExpiresAt is DateTime?
|
||||
|
|
|
|||
Loading…
Reference in a new issue