From 2dbad297278c34aa10bed519776ad6be80812bf0 Mon Sep 17 00:00:00 2001 From: Felix Gradinaru Date: Fri, 16 Jan 2026 15:20:43 +0100 Subject: [PATCH] feat: make TOTP verification window configurable Add totpVerificationWindow setting to allow operators to configure the tolerance for TOTP code verification. - Add optional $window parameter to TOTPService::checkIfOtpIsValid() - Inject configuration in LoginController and BackendController - Default to 0 (strict, original behavior) for backward compatibility - Document the new setting in README.md Setting totpVerificationWindow to 1 is recommended as it prevents authentication failures caused by: - Clock drift between user device and server - Period boundary race conditions (code generated at end of period) - Network latency during code submission --- Classes/Controller/BackendController.php | 8 +++++++- Classes/Controller/LoginController.php | 10 ++++++++-- Classes/Service/TOTPService.php | 4 ++-- Configuration/Settings.yaml | 3 +++ README.md | 12 ++++++++++++ 5 files changed, 32 insertions(+), 5 deletions(-) diff --git a/Classes/Controller/BackendController.php b/Classes/Controller/BackendController.php index 5e99390..72b25a0 100644 --- a/Classes/Controller/BackendController.php +++ b/Classes/Controller/BackendController.php @@ -71,6 +71,12 @@ class BackendController extends AbstractModuleController */ protected $translator; + /** + * @Flow\InjectConfiguration(path="totpVerificationWindow", package="Sandstorm.NeosTwoFactorAuthentication") + * @var int|null + */ + protected $totpVerificationWindow; + protected $defaultViewObjectName = FusionView::class; /** @@ -139,7 +145,7 @@ public function newAction(): void */ public function createAction(string $secret, string $secondFactorFromApp): void { - $isValid = TOTPService::checkIfOtpIsValid($secret, $secondFactorFromApp); + $isValid = TOTPService::checkIfOtpIsValid($secret, $secondFactorFromApp, $this->totpVerificationWindow); if (!$isValid) { $this->addFlashMessage( diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php index 33c7ebc..569f1d5 100644 --- a/Classes/Controller/LoginController.php +++ b/Classes/Controller/LoginController.php @@ -82,6 +82,12 @@ class LoginController extends ActionController */ protected $translator; + /** + * @Flow\InjectConfiguration(path="totpVerificationWindow", package="Sandstorm.NeosTwoFactorAuthentication") + * @var int|null + */ + protected $totpVerificationWindow; + /** * This action decides which tokens are already authenticated * and decides which is next to authenticate @@ -184,7 +190,7 @@ public function setupSecondFactorAction(?string $username = null): void */ public function createSecondFactorAction(string $secret, string $secondFactorFromApp): void { - $isValid = TOTPService::checkIfOtpIsValid($secret, $secondFactorFromApp); + $isValid = TOTPService::checkIfOtpIsValid($secret, $secondFactorFromApp, $this->totpVerificationWindow); if (!$isValid) { $this->addFlashMessage( @@ -239,7 +245,7 @@ private function enteredTokenMatchesAnySecondFactor(string $enteredSecondFactor, /** @var SecondFactor[] $secondFactors */ $secondFactors = $this->secondFactorRepository->findByAccount($account); foreach ($secondFactors as $secondFactor) { - $isValid = TOTPService::checkIfOtpIsValid($secondFactor->getSecret(), $enteredSecondFactor); + $isValid = TOTPService::checkIfOtpIsValid($secondFactor->getSecret(), $enteredSecondFactor, $this->totpVerificationWindow); if ($isValid) { return true; } diff --git a/Classes/Service/TOTPService.php b/Classes/Service/TOTPService.php index bab5e20..7304184 100644 --- a/Classes/Service/TOTPService.php +++ b/Classes/Service/TOTPService.php @@ -35,10 +35,10 @@ public static function generateNewTotp(): TOTP return TOTP::create(); } - public static function checkIfOtpIsValid(string $secret, string $submittedOtp): bool + public static function checkIfOtpIsValid(string $secret, string $submittedOtp, ?int $window = null): bool { $otp = TOTP::create($secret); - return $otp->verify($submittedOtp); + return $otp->verify($submittedOtp, null, $window); } public function generateQRCodeForTokenAndAccount(TOTP $otp, Account $account): string diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index 559bc32..f821427 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -59,3 +59,6 @@ Sandstorm: enforce2FAForRoles: [] # (optional) if set this will be used as a naming convention for the TOTP. If empty the Site name will be used issuerName: '' + # Time window for TOTP verification (number of adjacent 30-second periods to accept) + # 0 = strict (only current period, default), 1 = recommended (current + adjacent periods) + totpVerificationWindow: 0 \ No newline at end of file diff --git a/README.md b/README.md index f086fc1..f21b66a 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,18 @@ Sandstorm: issuerName: '' ``` +### TOTP Verification Window +To allow for clock drift and network latency, you can configure a time window for TOTP verification: +```yml +Sandstorm: + NeosTwoFactorAuthentication: + # Number of adjacent 30-second periods to accept + # 0 = strict (only current period, default) + # 1 = recommended (current + adjacent periods, ~90 second tolerance) + totpVerificationWindow: 1 +``` +Setting this to `1` is recommended as it prevents authentication failures caused by slight clock differences between the user's device and the server, or delays when entering the code. + ## Tested 2FA apps Thx to @Sebobo @Benjamin-K for creating a list of supported and testet apps!