Skip to content

Commit

Permalink
Merge pull request #187 from toniperic/revoke-tokens-and-codes
Browse files Browse the repository at this point in the history
Revoking credentials
  • Loading branch information
spideyfusion authored Apr 9, 2020
2 parents da38b7a + a84e721 commit fee109d
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 3 deletions.
6 changes: 6 additions & 0 deletions DependencyInjection/TrikoderOAuth2Extension.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
use Trikoder\Bundle\OAuth2Bundle\Manager\ScopeManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Model\Scope as ScopeModel;
use Trikoder\Bundle\OAuth2Bundle\Security\Authentication\Token\OAuth2TokenFactory;
use Trikoder\Bundle\OAuth2Bundle\Service\CredentialsRevoker\DoctrineCredentialsRevoker;

final class TrikoderOAuth2Extension extends Extension implements PrependExtensionInterface, CompilerPassInterface
{
Expand Down Expand Up @@ -268,6 +269,11 @@ private function configureDoctrinePersistence(ContainerBuilder $container, array
->replaceArgument('$entityManager', $entityManager)
;

$container
->getDefinition(DoctrineCredentialsRevoker::class)
->replaceArgument('$entityManager', $entityManager)
;

$container->setParameter('trikoder.oauth2.persistence.doctrine.enabled', true);
$container->setParameter('trikoder.oauth2.persistence.doctrine.manager', $entityManagerName);
}
Expand Down
5 changes: 2 additions & 3 deletions Manager/ClientManagerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@
namespace Trikoder\Bundle\OAuth2Bundle\Manager;

use Trikoder\Bundle\OAuth2Bundle\Model\Client;
use Trikoder\Bundle\OAuth2Bundle\Service\ClientFinderInterface;

interface ClientManagerInterface
interface ClientManagerInterface extends ClientFinderInterface
{
public function find(string $identifier): ?Client;

public function save(Client $client): void;

public function remove(Client $client): void;
Expand Down
3 changes: 3 additions & 0 deletions Resources/config/doctrine/model/AccessToken.orm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,8 @@
<many-to-one field="client" target-entity="Trikoder\Bundle\OAuth2Bundle\Model\Client">
<join-column name="client" referenced-column-name="identifier" nullable="false" on-delete="CASCADE" />
</many-to-one>
<indexes>
<index columns="userIdentifier" />
</indexes>
</entity>
</doctrine-mapping>
3 changes: 3 additions & 0 deletions Resources/config/doctrine/model/AuthorizationCode.orm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,8 @@
<many-to-one field="client" target-entity="Trikoder\Bundle\OAuth2Bundle\Model\Client">
<join-column name="client" referenced-column-name="identifier" nullable="false" />
</many-to-one>
<indexes>
<index columns="userIdentifier" />
</indexes>
</entity>
</doctrine-mapping>
2 changes: 2 additions & 0 deletions Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="Trikoder\Bundle\OAuth2Bundle\Service\ClientFinderInterface" alias="Trikoder\Bundle\OAuth2Bundle\Manager\ClientManagerInterface" />

<!-- The league repositories -->
<service id="Trikoder\Bundle\OAuth2Bundle\League\Repository\ClientRepository">
<argument type="service" id="Trikoder\Bundle\OAuth2Bundle\Manager\ClientManagerInterface" />
Expand Down
5 changes: 5 additions & 0 deletions Resources/config/storage/doctrine.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
<service id="Trikoder\Bundle\OAuth2Bundle\Manager\AuthorizationCodeManagerInterface" alias="Trikoder\Bundle\OAuth2Bundle\Manager\Doctrine\AuthorizationCodeManager" />

<!-- Services -->
<service id="Trikoder\Bundle\OAuth2Bundle\Service\CredentialsRevokerInterface" alias="Trikoder\Bundle\OAuth2Bundle\Service\CredentialsRevoker\DoctrineCredentialsRevoker" />
<service id="Trikoder\Bundle\OAuth2Bundle\Service\CredentialsRevoker\DoctrineCredentialsRevoker">
<argument key="$entityManager" />
</service>

<service id="Trikoder\Bundle\OAuth2Bundle\Manager\Doctrine\ClientManager">
<argument key="$entityManager" />
</service>
Expand Down
15 changes: 15 additions & 0 deletions Service/ClientFinderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Trikoder\Bundle\OAuth2Bundle\Service;

use Trikoder\Bundle\OAuth2Bundle\Model\Client;

/**
* @api
*/
interface ClientFinderInterface
{
public function find(string $identifier): ?Client;
}
101 changes: 101 additions & 0 deletions Service/CredentialsRevoker/DoctrineCredentialsRevoker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

declare(strict_types=1);

namespace Trikoder\Bundle\OAuth2Bundle\Service\CredentialsRevoker;

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Trikoder\Bundle\OAuth2Bundle\Model\AccessToken;
use Trikoder\Bundle\OAuth2Bundle\Model\AuthorizationCode;
use Trikoder\Bundle\OAuth2Bundle\Model\Client;
use Trikoder\Bundle\OAuth2Bundle\Model\RefreshToken;
use Trikoder\Bundle\OAuth2Bundle\Service\CredentialsRevokerInterface;

final class DoctrineCredentialsRevoker implements CredentialsRevokerInterface
{
/**
* @var EntityManagerInterface
*/
private $entityManager;

public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}

public function revokeCredentialsForUser(UserInterface $user): void
{
$userIdentifier = $user->getUsername();

$this->entityManager->createQueryBuilder()
->update(AccessToken::class, 'at')
->set('at.revoked', true)
->where('at.userIdentifier = :userIdentifier')
->setParameter('userIdentifier', $userIdentifier)
->getQuery()
->execute();

$queryBuilder = $this->entityManager->createQueryBuilder();
$queryBuilder
->update(RefreshToken::class, 'rt')
->set('rt.revoked', true)
->where($queryBuilder->expr()->in(
'rt.accessToken',
$this->entityManager->createQueryBuilder()
->select('at.identifier')
->from(AccessToken::class, 'at')
->where('at.userIdentifier = :userIdentifier')
->getDQL()
))
->setParameter('userIdentifier', $userIdentifier)
->getQuery()
->execute();

$this->entityManager->createQueryBuilder()
->update(AuthorizationCode::class, 'ac')
->set('ac.revoked', true)
->where('ac.userIdentifier = :userIdentifier')
->setParameter('userIdentifier', $userIdentifier)
->getQuery()
->execute();
}

public function revokeCredentialsForClient(Client $client): void
{
$doctrineClient = $this->entityManager
->getRepository(Client::class)
->findOneBy(['identifier' => $client->getIdentifier()]);

$this->entityManager->createQueryBuilder()
->update(AccessToken::class, 'at')
->set('at.revoked', true)
->where('at.client = :client')
->setParameter('client', $doctrineClient)
->getQuery()
->execute();

$queryBuilder = $this->entityManager->createQueryBuilder();
$queryBuilder->update(RefreshToken::class, 'rt')
->set('rt.revoked', true)
->where($queryBuilder->expr()->in(
'rt.accessToken',
$this->entityManager->createQueryBuilder()
->select('at.identifier')
->from(AccessToken::class, 'at')
->where('at.client = :client')
->getDQL()
))
->setParameter('client', $doctrineClient)
->getQuery()
->execute();

$this->entityManager->createQueryBuilder()
->update(AuthorizationCode::class, 'ac')
->set('ac.revoked', true)
->where('ac.client = :client')
->setParameter('client', $doctrineClient)
->getQuery()
->execute();
}
}
21 changes: 21 additions & 0 deletions Service/CredentialsRevokerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Trikoder\Bundle\OAuth2Bundle\Service;

use Symfony\Component\Security\Core\User\UserInterface;
use Trikoder\Bundle\OAuth2Bundle\Model\Client;

/**
* Service responsible for revoking credentials on client-level and user-level.
* Credentials = access tokens, refresh tokens and authorization codes.
*
* @api
*/
interface CredentialsRevokerInterface
{
public function revokeCredentialsForUser(UserInterface $user): void;

public function revokeCredentialsForClient(Client $client): void;
}
112 changes: 112 additions & 0 deletions Tests/Acceptance/DoctrineCredentialsRevokerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

declare(strict_types=1);

namespace Trikoder\Bundle\OAuth2Bundle\Tests\Acceptance;

use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Model\AccessToken;
use Trikoder\Bundle\OAuth2Bundle\Model\AuthorizationCode;
use Trikoder\Bundle\OAuth2Bundle\Model\Client;
use Trikoder\Bundle\OAuth2Bundle\Model\RefreshToken;
use Trikoder\Bundle\OAuth2Bundle\Service\CredentialsRevoker\DoctrineCredentialsRevoker;
use Trikoder\Bundle\OAuth2Bundle\Tests\Fixtures\FixtureFactory;

/**
* @TODO This should be in the Integration tests folder but the current tests infrastructure would need improvements first.
* @covers \Trikoder\Bundle\OAuth2Bundle\Service\CredentialsRevoker\DoctrineCredentialsRevoker
*/
final class DoctrineCredentialsRevokerTest extends AbstractAcceptanceTest
{
public function testRevokesAllCredentialsForUser(): void
{
$userIdentifier = FixtureFactory::FIXTURE_USER;

/** @var EntityManagerInterface $em */
$em = $this->client->getContainer()->get('doctrine.orm.entity_manager');

$em->persist($client = new Client('client', 'secret'));

$authCode = $this->buildAuthCode('foo', '+1 minute', $client, $userIdentifier);
$accessToken = $this->buildAccessToken('bar', '+1 minute', $client, $userIdentifier);
$refreshToken = $this->buildRefreshToken('baz', '+1 minute', $accessToken);

$em->persist($authCode);
$em->persist($accessToken);
$em->persist($refreshToken);
$em->flush();

$revoker = new DoctrineCredentialsRevoker($em);

$revoker->revokeCredentialsForUser(FixtureFactory::createUser());

$em->refresh($authCode);
$em->refresh($accessToken);
$em->refresh($refreshToken);

$this->assertTrue($authCode->isRevoked());
$this->assertTrue($accessToken->isRevoked());
$this->assertTrue($refreshToken->isRevoked());
}

public function testRevokesAllCredentialsForClient(): void
{
/** @var EntityManagerInterface $em */
$em = $this->client->getContainer()->get('doctrine.orm.entity_manager');

$em->persist($client = new Client('acme', 'secret'));

$authCode = $this->buildAuthCode('foo', '+1 minute', $client, 'john');
$accessToken = $this->buildAccessToken('bar', '+1 minute', $client);
$refreshToken = $this->buildRefreshToken('baz', '+1 minute', $accessToken);

$em->persist($authCode);
$em->persist($accessToken);
$em->persist($refreshToken);
$em->flush();

$revoker = new DoctrineCredentialsRevoker($em);

$revoker->revokeCredentialsForClient($client);

$em->refresh($authCode);
$em->refresh($accessToken);
$em->refresh($refreshToken);

$this->assertTrue($authCode->isRevoked());
$this->assertTrue($accessToken->isRevoked());
$this->assertTrue($refreshToken->isRevoked());
}

private function buildRefreshToken(string $identifier, string $modify, AccessToken $accessToken): RefreshToken
{
return new RefreshToken(
$identifier,
new DateTimeImmutable($modify),
$accessToken
);
}

private function buildAccessToken(string $identifier, string $modify, Client $client, ?string $userIdentifier = null): AccessToken
{
return new AccessToken(
$identifier,
new DateTimeImmutable($modify),
$client,
$userIdentifier,
[]
);
}

private function buildAuthCode(string $identifier, string $modify, Client $client, ?string $userIdentifier = null): AuthorizationCode
{
return new AuthorizationCode(
$identifier,
new DateTimeImmutable($modify),
$client,
$userIdentifier,
[]
);
}
}

0 comments on commit fee109d

Please sign in to comment.