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:
Tizian 「ティツィアーン」 2026-10-21 13:38:07 +02:00
commit 8e6625855a
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -73,9 +73,10 @@ Future<AuthSuccess> issueSession(
/// 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
/// this server's clock is past that moment, so the notice, whenever 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.
/// arrives, leaves it. A clock more than [maxClockSkew] behind, or ahead
/// (the full reading's L10: sessions made here in the gap would outlive the
/// notice), is refused ([PasswordRefusal.passwordChanged]): the reset is
/// done, and the device signs in again with the new password.
Future<AuthSuccess> issueSessionAfter(
Session session,
UuidValue account, {
@ -94,6 +95,18 @@ Future<AuthSuccess> issueSessionAfter(
// The reset is done: the device signs in again, with the new password.
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) {
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
/// [issueSessionAfter] to wait for it: further, the request would hang as
/// long, so it is refused. Clocks set by NTP differ by milliseconds; a
/// member's clock running ahead of the register's by up to this much
/// leaves sessions made in that gap, which the ADR accepts.
/// long, so it is refused; as far ahead, alike. Clocks set by NTP differ by
/// milliseconds; a member's clock running ahead of the register's by up to
/// this much leaves sessions made in that gap, which the ADR accepts.
const maxClockSkew = Duration(seconds: 5);
/// What [person] becomes here once a way in is theirs, as linking makes them