Skip to content

Commit

Permalink
Add a console command for removing expired tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
HypeMC committed May 19, 2019
1 parent 3db1d68 commit 7806eda
Show file tree
Hide file tree
Showing 13 changed files with 409 additions and 0 deletions.
89 changes: 89 additions & 0 deletions Command/ClearExpiredTokensCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

namespace Trikoder\Bundle\OAuth2Bundle\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Trikoder\Bundle\OAuth2Bundle\Manager\AccessTokenManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\RefreshTokenManagerInterface;

final class ClearExpiredTokensCommand extends Command
{
protected static $defaultName = 'trikoder:oauth2:clear-expired-tokens';

/**
* @var AccessTokenManagerInterface
*/
private $accessTokenManager;

/**
* @var RefreshTokenManagerInterface
*/
private $refreshTokenManager;

public function __construct(
AccessTokenManagerInterface $accessTokenManager,
RefreshTokenManagerInterface $refreshTokenManager
) {
parent::__construct();

$this->accessTokenManager = $accessTokenManager;
$this->refreshTokenManager = $refreshTokenManager;
}

protected function configure(): void
{
$this
->setDescription('Clears all expired access and/or refresh tokens')
->addOption(
'access-tokens-only',
'a',
InputOption::VALUE_NONE,
'Clear only access tokens.'
)
->addOption(
'refresh-tokens-only',
'r',
InputOption::VALUE_NONE,
'Clear only refresh tokens.'
)
;
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);

$clearExpiredAccessTokens = !$input->getOption('refresh-tokens-only');
$clearExpiredRefreshTokens = !$input->getOption('access-tokens-only');

if (!$clearExpiredAccessTokens && !$clearExpiredRefreshTokens) {
$io->error('Please chose only one of the following options: "access-tokens-only", "refresh-tokens-only".');

return 1;
}

if (true === $clearExpiredAccessTokens) {
$numOfClearedAccessTokens = $this->accessTokenManager->clearExpired();
$io->success(sprintf(
'Cleared %d expired access token%s.',
$numOfClearedAccessTokens,
1 === $numOfClearedAccessTokens ? '' : 's'
));
}

if (true === $clearExpiredRefreshTokens) {
$numOfClearedRefreshTokens = $this->refreshTokenManager->clearExpired();
$io->success(sprintf(
'Cleared %d expired refresh token%s.',
$numOfClearedRefreshTokens,
1 === $numOfClearedRefreshTokens ? '' : 's'
));
}

return 0;
}
}
2 changes: 2 additions & 0 deletions Manager/AccessTokenManagerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ interface AccessTokenManagerInterface
public function find(string $identifier): ?AccessToken;

public function save(AccessToken $accessToken): void;

public function clearExpired(): int;
}
9 changes: 9 additions & 0 deletions Manager/Doctrine/AccessTokenManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,13 @@ public function save(AccessToken $accessToken): void
$this->entityManager->persist($accessToken);
$this->entityManager->flush();
}

public function clearExpired(): int
{
return $this->entityManager->createQueryBuilder()
->delete(AccessToken::class, 'at')
->where('at.expiry < CURRENT_TIMESTAMP()')
->getQuery()
->execute();
}
}
9 changes: 9 additions & 0 deletions Manager/Doctrine/RefreshTokenManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,13 @@ public function save(RefreshToken $refreshToken): void
$this->entityManager->persist($refreshToken);
$this->entityManager->flush();
}

public function clearExpired(): int
{
return $this->entityManager->createQueryBuilder()
->delete(RefreshToken::class, 'rt')
->where('rt.expiry < CURRENT_TIMESTAMP()')
->getQuery()
->execute();
}
}
13 changes: 13 additions & 0 deletions Manager/InMemory/AccessTokenManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Trikoder\Bundle\OAuth2Bundle\Manager\InMemory;

use DateTime;
use Trikoder\Bundle\OAuth2Bundle\Manager\AccessTokenManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Model\AccessToken;

Expand All @@ -27,4 +28,16 @@ public function save(AccessToken $accessToken): void
{
$this->accessTokens[$accessToken->getIdentifier()] = $accessToken;
}

public function clearExpired(): int
{
$count = \count($this->accessTokens);

$now = new DateTime();
$this->accessTokens = array_filter($this->accessTokens, function (AccessToken $accessToken) use ($now): bool {
return $accessToken->getExpiry() >= $now;
});

return $count - \count($this->accessTokens);
}
}
13 changes: 13 additions & 0 deletions Manager/InMemory/RefreshTokenManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Trikoder\Bundle\OAuth2Bundle\Manager\InMemory;

use DateTime;
use Trikoder\Bundle\OAuth2Bundle\Manager\RefreshTokenManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Model\RefreshToken;

Expand All @@ -27,4 +28,16 @@ public function save(RefreshToken $refreshToken): void
{
$this->refreshTokens[$refreshToken->getIdentifier()] = $refreshToken;
}

public function clearExpired(): int
{
$count = \count($this->refreshTokens);

$now = new DateTime();
$this->refreshTokens = array_filter($this->refreshTokens, function (RefreshToken $refreshToken) use ($now): bool {
return $refreshToken->getExpiry() >= $now;
});

return $count - \count($this->refreshTokens);
}
}
2 changes: 2 additions & 0 deletions Manager/RefreshTokenManagerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ interface RefreshTokenManagerInterface
public function find(string $identifier): ?RefreshToken;

public function save(RefreshToken $refreshToken): void;

public function clearExpired(): int;
}
5 changes: 5 additions & 0 deletions Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@
<argument type="service" id="Trikoder\Bundle\OAuth2Bundle\Manager\ClientManagerInterface" />
<tag name="console.command" />
</service>
<service id="trikoder.oauth2.command.clear_expired_tokens_command" class="Trikoder\Bundle\OAuth2Bundle\Command\ClearExpiredTokensCommand">
<argument type="service" id="Trikoder\Bundle\OAuth2Bundle\Manager\AccessTokenManagerInterface" />
<argument type="service" id="Trikoder\Bundle\OAuth2Bundle\Manager\RefreshTokenManagerInterface" />
<tag name="console.command" />
</service>

<!-- Utility services -->
<service id="trikoder.oauth2.converter.scope_converter" class="Trikoder\Bundle\OAuth2Bundle\Converter\ScopeConverter" />
Expand Down
108 changes: 108 additions & 0 deletions Tests/Acceptance/ClearExpiredTokensCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

namespace Trikoder\Bundle\OAuth2Bundle\Tests\Acceptance;

use DateTime;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Tester\CommandTester;
use Trikoder\Bundle\OAuth2Bundle\Manager\AccessTokenManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\ClientManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\RefreshTokenManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\ScopeManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Tests\Fixtures\FixtureFactory;

final class ClearExpiredTokensCommandTest extends AbstractAcceptanceTest
{
protected function setUp(): void
{
parent::setUp();

timecop_freeze(new DateTime());

FixtureFactory::initializeFixtures(
$this->client->getContainer()->get(ScopeManagerInterface::class),
$this->client->getContainer()->get(ClientManagerInterface::class),
$this->client->getContainer()->get(AccessTokenManagerInterface::class),
$this->client->getContainer()->get(RefreshTokenManagerInterface::class)
);
}

protected function tearDown(): void
{
timecop_return();

parent::tearDown();
}

public function testClearExpiredAccessAndRefreshTokens(): void
{
$command = $this->command();
$commandTester = new CommandTester($command);

$exitCode = $commandTester->execute([
'command' => $command->getName(),
]);

$this->assertSame(0, $exitCode);

$output = $commandTester->getDisplay();
$this->assertStringContainsString('Cleared 1 expired access token.', $output);
$this->assertStringContainsString('Cleared 1 expired refresh token.', $output);
}

public function testClearExpiredAccessTokens(): void
{
$command = $this->command();
$commandTester = new CommandTester($command);

$exitCode = $commandTester->execute([
'command' => $command->getName(),
'--access-tokens-only' => true,
]);

$this->assertSame(0, $exitCode);

$output = $commandTester->getDisplay();
$this->assertStringContainsString('Cleared 1 expired access token.', $output);
$this->assertStringNotContainsString('Cleared 1 expired refresh token.', $output);
}

public function testClearExpiredRefreshTokens(): void
{
$command = $this->command();
$commandTester = new CommandTester($command);

$exitCode = $commandTester->execute([
'command' => $command->getName(),
'--refresh-tokens-only' => true,
]);

$this->assertSame(0, $exitCode);

$output = $commandTester->getDisplay();
$this->assertStringNotContainsString('Cleared 1 expired access token.', $output);
$this->assertStringContainsString('Cleared 1 expired refresh token.', $output);
}

public function testErrorWhenBothOptionsAreUsed(): void
{
$command = $this->command();
$commandTester = new CommandTester($command);

$exitCode = $commandTester->execute([
'command' => $command->getName(),
'--access-tokens-only' => true,
'--refresh-tokens-only' => true,
]);

$this->assertSame(1, $exitCode);

$output = $commandTester->getDisplay();
$this->assertStringContainsString('Please chose only one of the following options:', $output);
}

private function command(): Command
{
return $this->application->find('trikoder:oauth2:clear-expired-tokens');
}
}
66 changes: 66 additions & 0 deletions Tests/Unit/InMemoryAccessTokenManagerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace Trikoder\Bundle\OAuth2Bundle\Tests\Unit;

use DateTime;
use PHPUnit\Framework\TestCase;
use ReflectionProperty;
use Trikoder\Bundle\OAuth2Bundle\Manager\InMemory\AccessTokenManager as InMemoryAccessTokenManager;
use Trikoder\Bundle\OAuth2Bundle\Model\AccessToken;
use Trikoder\Bundle\OAuth2Bundle\Model\Client;

final class InMemoryAccessTokenManagerTest extends TestCase
{
public function testClearExpired(): void
{
$inMemoryAccessTokenManager = new InMemoryAccessTokenManager();

timecop_freeze(new DateTime());

$testData = $this->buildClearExpiredTestData();

foreach ($testData['input'] as $token) {
$inMemoryAccessTokenManager->save($token);
}
$this->assertSame(3, $inMemoryAccessTokenManager->clearExpired());

timecop_return();

$reflectionProperty = new ReflectionProperty(InMemoryAccessTokenManager::class, 'accessTokens');
$reflectionProperty->setAccessible(true);

$this->assertSame($testData['output'], $reflectionProperty->getValue($inMemoryAccessTokenManager));
}

private function buildClearExpiredTestData(): array
{
$validAccessTokens = [
'1111' => $this->buildAccessToken('1111', '+1 day'),
'2222' => $this->buildAccessToken('2222', '+1 hour'),
'3333' => $this->buildAccessToken('3333', '+1 second'),
'4444' => $this->buildAccessToken('4444', 'now'),
];

$expiredAccessTokens = [
'5555' => $this->buildAccessToken('5555', '-1 day'),
'6666' => $this->buildAccessToken('6666', '-1 hour'),
'7777' => $this->buildAccessToken('7777', '-1 second'),
];

return [
'input' => $validAccessTokens + $expiredAccessTokens,
'output' => $validAccessTokens,
];
}

private function buildAccessToken(string $identifier, string $modify): AccessToken
{
return new AccessToken(
$identifier,
(new DateTime())->modify($modify),
new Client('client', 'secret'),
null,
[]
);
}
}
Loading

0 comments on commit 7806eda

Please sign in to comment.