diff --git a/api/models/profile/TwoFactorAuthForm.php b/api/models/profile/TwoFactorAuthForm.php index 02c1734..126186c 100644 --- a/api/models/profile/TwoFactorAuthForm.php +++ b/api/models/profile/TwoFactorAuthForm.php @@ -23,6 +23,8 @@ class TwoFactorAuthForm extends ApiForm { public $token; + public $timestamp; + public $password; /** @@ -38,10 +40,16 @@ class TwoFactorAuthForm extends ApiForm { public function rules() { $bothScenarios = [self::SCENARIO_ACTIVATE, self::SCENARIO_DISABLE]; return [ + ['timestamp', 'integer', 'on' => [self::SCENARIO_ACTIVATE]], ['account', 'validateOtpDisabled', 'on' => self::SCENARIO_ACTIVATE], ['account', 'validateOtpEnabled', 'on' => self::SCENARIO_DISABLE], ['token', 'required', 'message' => E::OTP_TOKEN_REQUIRED, 'on' => $bothScenarios], - ['token', TotpValidator::class, 'account' => $this->account, 'window' => 30, 'on' => $bothScenarios], + ['token', TotpValidator::class, 'on' => $bothScenarios, + 'account' => $this->account, + 'timestamp' => function() { + return $this->timestamp; + }, + ], ['password', PasswordRequiredValidator::class, 'account' => $this->account, 'on' => $bothScenarios], ]; } diff --git a/api/validators/TotpValidator.php b/api/validators/TotpValidator.php index dac0c17..9b5f277 100644 --- a/api/validators/TotpValidator.php +++ b/api/validators/TotpValidator.php @@ -19,9 +19,17 @@ class TotpValidator extends Validator { * @var int|null Задаёт окно, в промежуток которого будет проверяться код. * Позволяет избежать ситуации, когда пользователь ввёл код в последнюю секунду * его существования и пока шёл запрос, тот протух. + * Значение задаётся в +- кодах, а не секундах. */ public $window; + /** + * @var int|callable|null Позволяет задать точное время, относительно которого будет + * выполняться проверка. Это может быть собственно время или функция, возвращающая значение. + * Если не задано, то будет использовано текущее время. + */ + public $timestamp; + public $skipOnEmpty = false; public function init() { @@ -41,11 +49,23 @@ class TotpValidator extends Validator { protected function validateValue($value) { $totp = new TOTP(null, $this->account->otp_secret); - if (!$totp->verify((string)$value, null, $this->window)) { + if (!$totp->verify((string)$value, $this->getTimestamp(), $this->window)) { return [E::OTP_TOKEN_INCORRECT, []]; } return null; } + private function getTimestamp(): ?int { + if ($this->timestamp === null) { + return null; + } + + if (is_callable($this->timestamp)) { + return (int)call_user_func($this->timestamp); + } + + return (int)$this->timestamp; + } + } diff --git a/tests/codeception/api/unit/validators/TotpValidatorTest.php b/tests/codeception/api/unit/validators/TotpValidatorTest.php index fc23f9f..9385356 100644 --- a/tests/codeception/api/unit/validators/TotpValidatorTest.php +++ b/tests/codeception/api/unit/validators/TotpValidatorTest.php @@ -14,7 +14,7 @@ class TotpValidatorTest extends TestCase { public function testValidateValue() { $account = new Account(); $account->otp_secret = 'some secret'; - $controlTotp = new TOTP(null, 'some secret'); + $controlTotp = new TOTP(null, $account->otp_secret); $validator = new TotpValidator(['account' => $account]); @@ -27,9 +27,24 @@ class TotpValidatorTest extends TestCase { $result = $this->callProtected($validator, 'validateValue', $controlTotp->at(time() - 31)); $this->assertEquals([E::OTP_TOKEN_INCORRECT, []], $result); - $validator->window = 60; + $validator->window = 2; $result = $this->callProtected($validator, 'validateValue', $controlTotp->at(time() - 31)); $this->assertNull($result); + + $at = time() - 400; + $validator->timestamp = $at; + $result = $this->callProtected($validator, 'validateValue', $controlTotp->now()); + $this->assertEquals([E::OTP_TOKEN_INCORRECT, []], $result); + + $result = $this->callProtected($validator, 'validateValue', $controlTotp->at($at)); + $this->assertNull($result); + + $at = function() { + return time() - 700; + }; + $validator->timestamp = $at; + $result = $this->callProtected($validator, 'validateValue', $controlTotp->at($at())); + $this->assertNull($result); } }