diff --git a/composer.json b/composer.json
index e3d9eb5..4249162 100644
--- a/composer.json
+++ b/composer.json
@@ -23,16 +23,18 @@
"thecodingmachine/safe": "^1.0|^2.0"
},
"require-dev": {
+ "ekino/phpstan-banned-code": "^1.0",
"infection/infection": "^0.26",
- "phpunit/phpunit": "^9.5",
"phpstan/phpstan": "^1.0",
"phpstan/phpstan-beberlei-assert": "^1.0",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"phpstan/phpstan-strict-rules": "^1.0",
- "thecodingmachine/phpstan-safe-rule": "^1.0",
+ "phpunit/phpunit": "^9.5",
"rector/rector": "^0.12.11",
- "symplify/easy-coding-standard": "^10.0"
+ "symfony/phpunit-bridge": "^6.0",
+ "symplify/easy-coding-standard": "^10.0",
+ "thecodingmachine/phpstan-safe-rule": "^1.0"
},
"autoload": {
"psr-4": { "OTPHP\\": "src/" }
diff --git a/ecs.php b/ecs.php
index 0454311..cccdf76 100644
--- a/ecs.php
+++ b/ecs.php
@@ -14,6 +14,7 @@
use PhpCsFixer\Fixer\LanguageConstruct\CombineConsecutiveIssetsFixer;
use PhpCsFixer\Fixer\LanguageConstruct\CombineConsecutiveUnsetsFixer;
use PhpCsFixer\Fixer\Phpdoc\AlignMultilineCommentFixer;
+use PhpCsFixer\Fixer\Phpdoc\GeneralPhpdocAnnotationRemoveFixer;
use PhpCsFixer\Fixer\Phpdoc\NoSuperfluousPhpdocTagsFixer;
use PhpCsFixer\Fixer\Phpdoc\PhpdocOrderFixer;
use PhpCsFixer\Fixer\Phpdoc\PhpdocTrimConsecutiveBlankLineSeparationFixer;
@@ -105,20 +106,13 @@
]])
;
+ $services->remove(GeneralPhpdocAnnotationRemoveFixer::class);
$services->remove(PhpUnitTestClassRequiresCoversFixer::class);
$parameters = $containerConfigurator->parameters();
$parameters
->set(Option::PARALLEL, true)
->set(Option::PATHS, [__DIR__])
- ->set(Option::SKIP, [
- __DIR__ . '/src/Kernel.php',
- __DIR__ . '/assets',
- __DIR__ . '/bin',
- __DIR__ . '/config',
- __DIR__ . '/heroku',
- __DIR__ . '/public',
- __DIR__ . '/var',
- ])
+ ->set(Option::SKIP, [__DIR__ . '/.github', __DIR__ . '/doc', __DIR__ . '/vendor'])
;
};
diff --git a/phpstan.neon b/phpstan.neon
index 4b0aa28..53210dd 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -13,3 +13,4 @@ includes:
- vendor/thecodingmachine/phpstan-safe-rule/phpstan-safe-rule.neon
- vendor/phpstan/phpstan-beberlei-assert/extension.neon
- vendor/phpstan/phpstan-phpunit/rules.neon
+ - vendor/ekino/phpstan-banned-code/extension.neon
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 5237004..6b57145 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -15,4 +15,7 @@
./tests
+
+
+
diff --git a/src/Factory.php b/src/Factory.php
index 4b91774..eba1a4e 100644
--- a/src/Factory.php
+++ b/src/Factory.php
@@ -19,6 +19,7 @@ public static function loadFromProvisioningUri(string $uri): OTPInterface
{
try {
$parsed_url = Url::fromString($uri);
+ Assertion::eq('otpauth', $parsed_url->getScheme());
} catch (Throwable $throwable) {
throw new InvalidArgumentException('Not a valid OTP provisioning URI', $throwable->getCode(), $throwable);
}
diff --git a/src/TOTP.php b/src/TOTP.php
index bba3f43..ea9c0af 100644
--- a/src/TOTP.php
+++ b/src/TOTP.php
@@ -29,7 +29,7 @@ public static function create(
public function getPeriod(): int
{
$value = $this->getParameter('period');
- Assertion::integer($value, 'Invalid "epoch" period.');
+ Assertion::integer($value, 'Invalid "period" parameter.');
return $value;
}
@@ -42,6 +42,13 @@ public function getEpoch(): int
return $value;
}
+ public function expiresIn(): int
+ {
+ $period = $this->getPeriod();
+
+ return $period - (time() % $this->getPeriod());
+ }
+
public function at(int $timestamp): string
{
return $this->generateOTP($this->timecode($timestamp));
diff --git a/src/TOTPInterface.php b/src/TOTPInterface.php
index 2d492f9..3a6f3e7 100644
--- a/src/TOTPInterface.php
+++ b/src/TOTPInterface.php
@@ -28,5 +28,7 @@ public function now(): string;
*/
public function getPeriod(): int;
+ public function expiresIn(): int;
+
public function getEpoch(): int;
}
diff --git a/tests/TOTPTest.php b/tests/TOTPTest.php
index 59cf116..d97d0ef 100644
--- a/tests/TOTPTest.php
+++ b/tests/TOTPTest.php
@@ -11,9 +11,11 @@
use ParagonIE\ConstantTime\Base32;
use PHPUnit\Framework\TestCase;
use RuntimeException;
+use Symfony\Bridge\PhpUnit\ClockMock;
/**
* @internal
+ * @group time-sensitive
*/
final class TOTPTest extends TestCase
{
@@ -26,7 +28,6 @@ public function labelNotDefined(): void
$this->expectExceptionMessage('The label is not set.');
$hotp = TOTP::create();
$hotp->getProvisioningUri();
- var_dump($hotp->getProvisioningUri());
}
/**
@@ -101,6 +102,19 @@ public function getProvisioningUri(): void
);
}
+ /**
+ * @test
+ * @dataProvider dataRemainingTimeBeforeExpiration
+ */
+ public function getRemainingTimeBeforeExpiration(int $timespamp, int $period, int $expectedRemainder): void
+ {
+ ClockMock::register(TOTP::class);
+ ClockMock::withClockMock($timespamp);
+ $otp = $this->createTOTP(6, 'sha1', $period);
+
+ static::assertSame($expectedRemainder, $otp->expiresIn());
+ }
+
/**
* @test
*/
@@ -314,6 +328,27 @@ public function qRCodeUri(): void
);
}
+ /**
+ * @return int[][]
+ */
+ public function dataRemainingTimeBeforeExpiration(): array
+ {
+ return [
+ [1644926810, 90, 40],
+ [1644926810, 30, 10],
+ [1644926810, 20, 10],
+ [1577833199, 90, 1],
+ [1577833199, 30, 1],
+ [1577833199, 20, 1],
+ [1577833200, 90, 90],
+ [1577833200, 30, 30],
+ [1577833200, 20, 20],
+ [1577833201, 90, 89],
+ [1577833201, 30, 29],
+ [1577833201, 20, 19],
+ ];
+ }
+
private function createTOTP(
int $digits,
string $digest,