fix(auth): a member's clock running ahead of the register's is refused too
The full reading's L10: issueSessionAfter refused a member clock more than five seconds behind the register's, but one running ahead without bound, so sessions made on the member in the gap before a reset's moment outlived the register's notice. Now a clock that far ahead is refused alike (PasswordRefusal.passwordChanged: the reset is done, the device signs in again), with a warning to set both clocks by NTP. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
0119aacbc7
commit
8e6625855a
2 changed files with 73 additions and 6 deletions
|
|
@ -73,9 +73,10 @@ 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. A clock more than [maxClockSkew] behind is refused
|
/// arrives, leaves it. A clock more than [maxClockSkew] behind, or ahead
|
||||||
/// ([PasswordRefusal.passwordChanged]): the reset is done, and the device
|
/// (the full reading's L10: sessions made here in the gap would outlive the
|
||||||
/// signs in again with the new password.
|
/// notice), 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, {
|
||||||
|
|
@ -94,6 +95,18 @@ Future<AuthSuccess> issueSessionAfter(
|
||||||
// The reset is done: the device signs in again, with the new password.
|
// The reset is done: the device signs in again, with the new password.
|
||||||
throw PasswordRefusedException(reason: PasswordRefusal.passwordChanged);
|
throw PasswordRefusedException(reason: PasswordRefusal.passwordChanged);
|
||||||
}
|
}
|
||||||
|
// Ahead as far, sessions made here in the gap before the register's
|
||||||
|
// moment would outlive its notice (the full reading's L10): refused alike.
|
||||||
|
// (The register answered moments ago: its moment can't be further back
|
||||||
|
// than that, but for a clock running ahead.)
|
||||||
|
if (-wait > maxClockSkew) {
|
||||||
|
session.log(
|
||||||
|
"This server's clock is ${(-wait).inSeconds} s ahead of the "
|
||||||
|
"register's: set both by NTP.",
|
||||||
|
level: LogLevel.warning,
|
||||||
|
);
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
|
|
@ -102,9 +115,9 @@ Future<AuthSuccess> issueSessionAfter(
|
||||||
|
|
||||||
/// How far behind the register's clock a member's may run for
|
/// How far behind the register's clock a member's may run for
|
||||||
/// [issueSessionAfter] to wait for it: further, the request would hang as
|
/// [issueSessionAfter] to wait for it: further, the request would hang as
|
||||||
/// long, so it is refused. Clocks set by NTP differ by milliseconds; a
|
/// long, so it is refused; as far ahead, alike. Clocks set by NTP differ by
|
||||||
/// member's clock running ahead of the register's by up to this much
|
/// milliseconds; a member's clock running ahead of the register's by up to
|
||||||
/// leaves sessions made in that gap, which the ADR accepts.
|
/// this much leaves sessions made in that gap, which the ADR accepts.
|
||||||
const maxClockSkew = Duration(seconds: 5);
|
const maxClockSkew = Duration(seconds: 5);
|
||||||
|
|
||||||
/// What [person] becomes here once a way in is theirs, as linking makes them
|
/// What [person] becomes here once a way in is theirs, as linking makes them
|
||||||
|
|
|
||||||
|
|
@ -1474,6 +1474,60 @@ void main() {
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("refuses a clock further ahead of the register's alike, as "
|
||||||
|
'sessions made here in the gap would outlive its notice (the '
|
||||||
|
"full reading's L10)", () async {
|
||||||
|
register
|
||||||
|
..proof = PasswordProof(
|
||||||
|
account: ben,
|
||||||
|
serverProof: ByteData(32),
|
||||||
|
encryptionPublicKey: ByteData(32),
|
||||||
|
signingPublicKey: ByteData(32),
|
||||||
|
passwordKeys: WrappedKeys(
|
||||||
|
nonce: ByteData(24),
|
||||||
|
cipherText: ByteData(112),
|
||||||
|
),
|
||||||
|
needsAuthenticator: false,
|
||||||
|
)
|
||||||
|
..endedBefore = DateTime.now().toUtc().subtract(
|
||||||
|
const Duration(minutes: 1),
|
||||||
|
);
|
||||||
|
Future<int> sessionsOfBen() => ServerSideSession.db.count(
|
||||||
|
session,
|
||||||
|
where: (t) => t.authUserId.equals(ben),
|
||||||
|
);
|
||||||
|
final before = await sessionsOfBen();
|
||||||
|
await expectLater(
|
||||||
|
endpoints.accountRecovery.resetPassword(
|
||||||
|
notSignedIn(),
|
||||||
|
challenge: const Uuid().v7obj(),
|
||||||
|
signature: ByteData(64),
|
||||||
|
password: NewPassword(
|
||||||
|
salt: ByteData(16),
|
||||||
|
costs: newPasswordCosts,
|
||||||
|
verifier: ByteData(384),
|
||||||
|
passwordKeys: WrappedKeys(
|
||||||
|
nonce: ByteData(24),
|
||||||
|
cipherText: ByteData(112),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
totpCode: '123456',
|
||||||
|
host: testHost,
|
||||||
|
address: 'anna@example.org',
|
||||||
|
language: 'en',
|
||||||
|
),
|
||||||
|
throwsA(
|
||||||
|
isA<PasswordRefusedException>().having(
|
||||||
|
(error) => error.reason,
|
||||||
|
'reason',
|
||||||
|
PasswordRefusal.passwordChanged,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// Nobody signed in: no session made.
|
||||||
|
expect(await sessionsOfBen(), before);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('pruning', () {
|
group('pruning', () {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue