Skip to content

Commit

Permalink
Merge pull request #53 from trikoder/clear-expired-tokens-command
Browse files Browse the repository at this point in the history
Add a console command for removing expired tokens
  • Loading branch information
X-Coder264 authored May 23, 2019
2 parents baffa92 + d8ed8de commit de3e338
Show file tree
Hide file tree
Showing 14 changed files with 581 additions and 0 deletions.
91 changes: 91 additions & 0 deletions Command/ClearExpiredTokensCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

declare(strict_types=1);

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 choose 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 @@ -11,4 +11,6 @@ interface AccessTokenManagerInterface
public function find(string $identifier): ?AccessToken;

public function save(AccessToken $accessToken): void;

public function clearExpired(): int;
}
11 changes: 11 additions & 0 deletions Manager/Doctrine/AccessTokenManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Trikoder\Bundle\OAuth2Bundle\Manager\Doctrine;

use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\AccessTokenManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Model\AccessToken;
Expand Down Expand Up @@ -36,4 +37,14 @@ 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 < :expiry')
->setParameter('expiry', new DateTime())
->getQuery()
->execute();
}
}
11 changes: 11 additions & 0 deletions Manager/Doctrine/RefreshTokenManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Trikoder\Bundle\OAuth2Bundle\Manager\Doctrine;

use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\RefreshTokenManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Model\RefreshToken;
Expand Down Expand Up @@ -36,4 +37,14 @@ 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 < :expiry')
->setParameter('expiry', new DateTime())
->getQuery()
->execute();
}
}
13 changes: 13 additions & 0 deletions Manager/InMemory/AccessTokenManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Trikoder\Bundle\OAuth2Bundle\Manager\InMemory;

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

Expand All @@ -29,4 +30,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 @@ -4,6 +4,7 @@

namespace Trikoder\Bundle\OAuth2Bundle\Manager\InMemory;

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

Expand All @@ -29,4 +30,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 @@ -11,4 +11,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
110 changes: 110 additions & 0 deletions Tests/Acceptance/ClearExpiredTokensCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

declare(strict_types=1);

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 choose only one of the following options:', $output);
}

private function command(): Command
{
return $this->application->find('trikoder:oauth2:clear-expired-tokens');
}
}
Loading

0 comments on commit de3e338

Please sign in to comment.