fix(auth): a member behind the register's clock says the password changed
When a member's clock ran more than its allowance behind the register's, the session after a reset was refused as unavailable (the re-review's tenth finding), after the register had already reset the account and ended every session, and the app read it as "this instance has no password sign-in". It now has a refusal of its own (passwordChanged): the app says the password was changed and to sign in again with the new one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
293ec6b103
commit
09a627d1c7
14 changed files with 60 additions and 7 deletions
|
|
@ -214,5 +214,6 @@
|
||||||
"passwordSessionEnded": "Zu viele falsche Codes hintereinander von diesem Gerät: Es wurde abgemeldet. Melde dich wieder mit deinem Passwort und einem Code an.",
|
"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.",
|
"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"
|
"passwordDeleteSendCode": "Code per E-Mail schicken",
|
||||||
|
"passwordChangedSignInAgain": "Dein Passwort wurde geändert, aber dieser Dienst konnte dich gerade noch nicht anmelden. Melde dich wieder mit dem neuen Passwort an."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -461,5 +461,6 @@
|
||||||
"passwordSessionEnded": "Too many wrong codes in a row from this device: it was signed out. Sign in again with your password and a code.",
|
"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.",
|
"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"
|
"passwordDeleteSendCode": "Email me a code",
|
||||||
|
"passwordChangedSignInAgain": "Your password was changed, but this service couldn't sign you in just yet. Sign in again with the new password."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1392,6 +1392,12 @@ abstract class HalkyonAppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Email me a code'**
|
/// **'Email me a code'**
|
||||||
String get passwordDeleteSendCode;
|
String get passwordDeleteSendCode;
|
||||||
|
|
||||||
|
/// No description provided for @passwordChangedSignInAgain.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Your password was changed, but this service couldn\'t sign you in just yet. Sign in again with the new password.'**
|
||||||
|
String get passwordChangedSignInAgain;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HalkyonAppLocalizationsDelegate
|
class _HalkyonAppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -807,4 +807,8 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordDeleteSendCode => 'Code per E-Mail schicken';
|
String get passwordDeleteSendCode => 'Code per E-Mail schicken';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get passwordChangedSignInAgain =>
|
||||||
|
'Dein Passwort wurde geändert, aber dieser Dienst konnte dich gerade noch nicht anmelden. Melde dich wieder mit dem neuen Passwort an.';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -790,6 +790,10 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordDeleteSendCode => 'Email me a code';
|
String get passwordDeleteSendCode => 'Email me a code';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get passwordChangedSignInAgain =>
|
||||||
|
'Your password was changed, but this service couldn\'t sign you in just yet. Sign in again with the new password.';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
||||||
|
|
|
||||||
|
|
@ -462,6 +462,7 @@ PasswordProblem _problemOf(protocol.PasswordRefusal reason) => switch (reason) {
|
||||||
protocol.PasswordRefusal.otherInstance => PasswordProblem.instanceUnproven,
|
protocol.PasswordRefusal.otherInstance => PasswordProblem.instanceUnproven,
|
||||||
protocol.PasswordRefusal.codesWait => PasswordProblem.codesWait,
|
protocol.PasswordRefusal.codesWait => PasswordProblem.codesWait,
|
||||||
protocol.PasswordRefusal.sessionEnded => PasswordProblem.sessionEnded,
|
protocol.PasswordRefusal.sessionEnded => PasswordProblem.sessionEnded,
|
||||||
|
protocol.PasswordRefusal.passwordChanged => PasswordProblem.passwordChanged,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Keeps one session in memory, for a client used once.
|
/// Keeps one session in memory, for a client used once.
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,10 @@ enum PasswordProblem() {
|
||||||
/// device signs in again.
|
/// device signs in again.
|
||||||
sessionEnded,
|
sessionEnded,
|
||||||
|
|
||||||
|
/// The password was changed by a reset, but this service couldn't open a
|
||||||
|
/// session for it yet: sign in again with the new password.
|
||||||
|
passwordChanged,
|
||||||
|
|
||||||
/// No reset waits for the address.
|
/// No reset waits for the address.
|
||||||
noReset,
|
noReset,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ String passwordProblemText(BuildContext context, PasswordFailed failed) {
|
||||||
null => l10n.passwordTooManyAttempts,
|
null => l10n.passwordTooManyAttempts,
|
||||||
},
|
},
|
||||||
PasswordProblem.sessionEnded => l10n.passwordSessionEnded,
|
PasswordProblem.sessionEnded => l10n.passwordSessionEnded,
|
||||||
|
PasswordProblem.passwordChanged => l10n.passwordChangedSignInAgain,
|
||||||
PasswordProblem.noReset => l10n.passwordNoReset,
|
PasswordProblem.noReset => l10n.passwordNoReset,
|
||||||
PasswordProblem.passwordExists => l10n.passwordExists,
|
PasswordProblem.passwordExists => l10n.passwordExists,
|
||||||
PasswordProblem.addressTaken => l10n.passwordAddressTaken,
|
PasswordProblem.addressTaken => l10n.passwordAddressTaken,
|
||||||
|
|
|
||||||
|
|
@ -63,5 +63,12 @@ void main() {
|
||||||
),
|
),
|
||||||
contains('signed out'),
|
contains('signed out'),
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
passwordProblemText(
|
||||||
|
context,
|
||||||
|
const PasswordFailed(PasswordProblem.passwordChanged),
|
||||||
|
),
|
||||||
|
allOf(contains('was changed'), contains('Sign in again')),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,13 @@ enum PasswordRefusal implements _isc.SerializableModel {
|
||||||
|
|
||||||
/// Ten codes in a row from this signed-in session were wrong: the
|
/// Ten codes in a row from this signed-in session were wrong: the
|
||||||
/// session ended (the re-review's third finding); sign in again.
|
/// session ended (the re-review's third finding); sign in again.
|
||||||
sessionEnded;
|
sessionEnded,
|
||||||
|
|
||||||
|
/// The password was changed (a reset carried out), but this service
|
||||||
|
/// couldn't open a session for it yet (its clock is behind the
|
||||||
|
/// register's): sign in again with the new password. Never "this
|
||||||
|
/// instance has no password sign-in" (the re-review's tenth finding).
|
||||||
|
passwordChanged;
|
||||||
|
|
||||||
static PasswordRefusal fromJson(String name) {
|
static PasswordRefusal fromJson(String name) {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
|
|
@ -130,6 +136,8 @@ enum PasswordRefusal implements _isc.SerializableModel {
|
||||||
return PasswordRefusal.codesWait;
|
return PasswordRefusal.codesWait;
|
||||||
case 'sessionEnded':
|
case 'sessionEnded':
|
||||||
return PasswordRefusal.sessionEnded;
|
return PasswordRefusal.sessionEnded;
|
||||||
|
case 'passwordChanged':
|
||||||
|
return PasswordRefusal.passwordChanged;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError(
|
throw ArgumentError(
|
||||||
'Value "$name" cannot be converted to "PasswordRefusal"',
|
'Value "$name" cannot be converted to "PasswordRefusal"',
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,13 @@ enum PasswordRefusal implements _is.SerializableModel {
|
||||||
|
|
||||||
/// Ten codes in a row from this signed-in session were wrong: the
|
/// Ten codes in a row from this signed-in session were wrong: the
|
||||||
/// session ended (the re-review's third finding); sign in again.
|
/// session ended (the re-review's third finding); sign in again.
|
||||||
sessionEnded;
|
sessionEnded,
|
||||||
|
|
||||||
|
/// The password was changed (a reset carried out), but this service
|
||||||
|
/// couldn't open a session for it yet (its clock is behind the
|
||||||
|
/// register's): sign in again with the new password. Never "this
|
||||||
|
/// instance has no password sign-in" (the re-review's tenth finding).
|
||||||
|
passwordChanged;
|
||||||
|
|
||||||
static PasswordRefusal fromJson(String name) {
|
static PasswordRefusal fromJson(String name) {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
|
|
@ -130,6 +136,8 @@ enum PasswordRefusal implements _is.SerializableModel {
|
||||||
return PasswordRefusal.codesWait;
|
return PasswordRefusal.codesWait;
|
||||||
case 'sessionEnded':
|
case 'sessionEnded':
|
||||||
return PasswordRefusal.sessionEnded;
|
return PasswordRefusal.sessionEnded;
|
||||||
|
case 'passwordChanged':
|
||||||
|
return PasswordRefusal.passwordChanged;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError(
|
throw ArgumentError(
|
||||||
'Value "$name" cannot be converted to "PasswordRefusal"',
|
'Value "$name" cannot be converted to "PasswordRefusal"',
|
||||||
|
|
|
||||||
|
|
@ -56,3 +56,8 @@ values:
|
||||||
### Ten codes in a row from this signed-in session were wrong: the
|
### Ten codes in a row from this signed-in session were wrong: the
|
||||||
### session ended (the re-review's third finding); sign in again.
|
### session ended (the re-review's third finding); sign in again.
|
||||||
- sessionEnded
|
- sessionEnded
|
||||||
|
### The password was changed (a reset carried out), but this service
|
||||||
|
### couldn't open a session for it yet (its clock is behind the
|
||||||
|
### register's): sign in again with the new password. Never "this
|
||||||
|
### instance has no password sign-in" (the re-review's tenth finding).
|
||||||
|
- passwordChanged
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,9 @@ Future<AuthSuccess> issueSession(
|
||||||
/// password or of the account): those here end first, as the register's
|
/// password or of the account): those here end first, as the register's
|
||||||
/// notice to this server will end them, and the new one is made only once
|
/// notice to this server will end them, and the new one is made only once
|
||||||
/// this server's clock is past that moment, so the notice, whenever it
|
/// this server's clock is past that moment, so the notice, whenever it
|
||||||
/// arrives, leaves it.
|
/// arrives, leaves it. A clock more than [maxClockSkew] behind is refused
|
||||||
|
/// ([PasswordRefusal.passwordChanged]): the reset is done, and the device
|
||||||
|
/// signs in again with the new password.
|
||||||
Future<AuthSuccess> issueSessionAfter(
|
Future<AuthSuccess> issueSessionAfter(
|
||||||
Session session,
|
Session session,
|
||||||
UuidValue account, {
|
UuidValue account, {
|
||||||
|
|
@ -89,7 +91,8 @@ Future<AuthSuccess> issueSessionAfter(
|
||||||
'set both by NTP.',
|
'set both by NTP.',
|
||||||
level: LogLevel.warning,
|
level: LogLevel.warning,
|
||||||
);
|
);
|
||||||
throw PasswordRefusedException(reason: PasswordRefusal.unavailable);
|
// The reset is done: the device signs in again, with the new password.
|
||||||
|
throw PasswordRefusedException(reason: PasswordRefusal.passwordChanged);
|
||||||
}
|
}
|
||||||
if (!wait.isNegative) {
|
if (!wait.isNegative) {
|
||||||
await Future<void>.delayed(wait + const Duration(milliseconds: 1));
|
await Future<void>.delayed(wait + const Duration(milliseconds: 1));
|
||||||
|
|
|
||||||
|
|
@ -1159,7 +1159,7 @@ void main() {
|
||||||
isA<PasswordRefusedException>().having(
|
isA<PasswordRefusedException>().having(
|
||||||
(error) => error.reason,
|
(error) => error.reason,
|
||||||
'reason',
|
'reason',
|
||||||
PasswordRefusal.unavailable,
|
PasswordRefusal.passwordChanged,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue