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!