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,