Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Classes/Controller/BackendController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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(
Expand Down
10 changes: 8 additions & 2 deletions Classes/Controller/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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;
}
Expand Down
4 changes: 2 additions & 2 deletions Classes/Service/TOTPService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions Configuration/Settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down