Skip to content

Commit

Permalink
TOTP: get the remaining time before TOTP expiration (#151)
Browse files Browse the repository at this point in the history
* Update TOTP.php
* Update TOTPInterface.php
* Tests
  • Loading branch information
Spomky authored Feb 15, 2022
1 parent f881732 commit b41eac3
Show file tree
Hide file tree
Showing 8 changed files with 59 additions and 14 deletions.
8 changes: 5 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/" }
Expand Down
12 changes: 3 additions & 9 deletions ecs.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'])
;
};
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,7 @@
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<listeners>
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener"/>
</listeners>
</phpunit>
1 change: 1 addition & 0 deletions src/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
9 changes: 8 additions & 1 deletion src/TOTP.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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));
Expand Down
2 changes: 2 additions & 0 deletions src/TOTPInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,7 @@ public function now(): string;
*/
public function getPeriod(): int;

public function expiresIn(): int;

public function getEpoch(): int;
}
37 changes: 36 additions & 1 deletion tests/TOTPTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -26,7 +28,6 @@ public function labelNotDefined(): void
$this->expectExceptionMessage('The label is not set.');
$hotp = TOTP::create();
$hotp->getProvisioningUri();
var_dump($hotp->getProvisioningUri());
}

/**
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit b41eac3

Please sign in to comment.