Skip to content

Commit

Permalink
Add configuration support for custom persistence
Browse files Browse the repository at this point in the history
  • Loading branch information
frankdekker authored and chalasr committed Mar 10, 2024
1 parent 21a5fae commit 81a32a0
Show file tree
Hide file tree
Showing 11 changed files with 483 additions and 6 deletions.
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ security:
* [Using custom client](using-custom-client.md)
* [Listening to League OAuth Server events](listening-to-league-events.md)
* [Password Grant Handling](password-grant-handling.md)
* [Using custom persistence managers](using-custom-persistence-managers.md)
## Contributing
Expand Down
110 changes: 110 additions & 0 deletions docs/using-custom-persistence-managers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Using custom persistence managers

Implement the 4 interfaces from the `League\Bundle\OAuth2ServerBundle\Manager` namespace:
- [AccessTokenManagerInterface](../src/Manager/AccessTokenManagerInterface.php)
- [AuthorizationCodeManagerInterface](../src/Manager/AuthorizationCodeManagerInterface.php)
- [ClientManagerInterface](../src/Manager/ClientManagerInterface.php)
- [RefreshTokenManagerInterface](../src/Manager/RefreshTokenManagerInterface.php)
And the interface for `CredentialsRevokerInterface`:
- [CredentialsRevokerInterface](../src/Service/CredentialsRevokerInterface.php)

```php

Example:

```php
class MyAccessTokenManager implements AccessTokenManagerInterface
{
}

class MyAuthorizationCodeManager implements AuthorizationCodeManagerInterface
{
}

class MyClientManager implements ClientManagerInterface
{
}

class MyRefreshTokenManager implements RefreshTokenManagerInterface
{
}

class MyCredentialsRevoker implements CredentialsRevokerInterface
{
}
```

Then register the services in the container:

```yaml
services:
_defaults:
autoconfigure: true

App\Manager\MyAccessTokenManager: ~
App\Manager\MyAuthorizationCodeManager: ~
App\Manager\MyClientManager: ~
App\Manager\MyRefreshTokenManager: ~
App\Service\MyCredentialsRevoker: ~
```
Finally, configure the bundle to use the new managers:
```yaml
league_oauth2_server:
persistence:
custom:
access_token_manager: App\Manager\MyAccessTokenManager
authorization_code_manager: App\Manager\MyAuthorizationCodeManager
client_manager: App\Manager\MyClientManager
refresh_token_manager: App\Manager\MyRefreshTokenManager
credentials_revoker: App\Service\MyCredentialsRevoker
```
## Optional
Example MySql table schema for custom persistence managers implementation:
```sql
CREATE TABLE `oauth2_access_token` (
`identifier` char(80) NOT NULL,
`client` varchar(32) NOT NULL,
`expiry` datetime NOT NULL,
`userIdentifier` varchar(128) DEFAULT NULL,
`scopes` text,
`revoked` tinyint(1) NOT NULL,
PRIMARY KEY (`identifier`),
KEY `client` (`client`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `oauth2_authorization_code` (
`identifier` char(80) NOT NULL,
`client` varchar(32) NOT NULL,
`expiry` datetime NOT NULL,
`userIdentifier` varchar(128) DEFAULT NULL,
`scopes` text,
`revoked` tinyint(1) NOT NULL,
PRIMARY KEY (`identifier`),
KEY `client` (`client`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `oauth2_client` (
`identifier` varchar(32) NOT NULL,
`name` varchar(128) NOT NULL,
`secret` varchar(128) DEFAULT NULL,
`redirectUris` text,
`grants` text,
`scopes` text,
`active` tinyint(1) NOT NULL,
`allowPlainTextPkce` tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`identifier`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `oauth2_refresh_token` (
`identifier` char(80) NOT NULL,
`access_token` char(80) DEFAULT NULL,
`expiry` datetime NOT NULL,
`revoked` tinyint(1) NOT NULL,
PRIMARY KEY (`identifier`),
KEY `access_token` (`access_token`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
30 changes: 30 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,36 @@ private function createPersistenceNode(): NodeDefinition
// In-memory persistence
->scalarNode('in_memory')
->end()
// Custom persistence
->arrayNode('custom')
->children()
->scalarNode('access_token_manager')
->info('Service id of the custom access token manager')
->cannotBeEmpty()
->isRequired()
->end()
->scalarNode('authorization_code_manager')
->info('Service id of the custom authorization code manager')
->cannotBeEmpty()
->isRequired()
->end()
->scalarNode('client_manager')
->info('Service id of the custom client manager')
->cannotBeEmpty()
->isRequired()
->end()
->scalarNode('refresh_token_manager')
->info('Service id of the custom refresh token manager')
->cannotBeEmpty()
->isRequired()
->end()
->scalarNode('credentials_revoker')
->info('Service id of the custom credentials revoker')
->cannotBeEmpty()
->isRequired()
->end()
->end()
->end()
->end()
;

Expand Down
24 changes: 23 additions & 1 deletion src/DependencyInjection/LeagueOAuth2ServerExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,20 @@
use League\Bundle\OAuth2ServerBundle\DBAL\Type\Grant as GrantType;
use League\Bundle\OAuth2ServerBundle\DBAL\Type\RedirectUri as RedirectUriType;
use League\Bundle\OAuth2ServerBundle\DBAL\Type\Scope as ScopeType;
use League\Bundle\OAuth2ServerBundle\Manager\AccessTokenManagerInterface;
use League\Bundle\OAuth2ServerBundle\Manager\AuthorizationCodeManagerInterface;
use League\Bundle\OAuth2ServerBundle\Manager\ClientManagerInterface;
use League\Bundle\OAuth2ServerBundle\Manager\Doctrine\AccessTokenManager;
use League\Bundle\OAuth2ServerBundle\Manager\Doctrine\AuthorizationCodeManager;
use League\Bundle\OAuth2ServerBundle\Manager\Doctrine\ClientManager;
use League\Bundle\OAuth2ServerBundle\Manager\Doctrine\RefreshTokenManager;
use League\Bundle\OAuth2ServerBundle\Manager\InMemory\AccessTokenManager as InMemoryAccessTokenManager;
use League\Bundle\OAuth2ServerBundle\Manager\RefreshTokenManagerInterface;
use League\Bundle\OAuth2ServerBundle\Manager\ScopeManagerInterface;
use League\Bundle\OAuth2ServerBundle\Persistence\Mapping\Driver;
use League\Bundle\OAuth2ServerBundle\Security\Authenticator\OAuth2Authenticator;
use League\Bundle\OAuth2ServerBundle\Service\CredentialsRevoker\DoctrineCredentialsRevoker;
use League\Bundle\OAuth2ServerBundle\Service\CredentialsRevokerInterface;
use League\Bundle\OAuth2ServerBundle\ValueObject\Scope as ScopeModel;
use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\CryptKey;
Expand Down Expand Up @@ -104,10 +109,13 @@ public function process(ContainerBuilder $container)
private function assertRequiredBundlesAreEnabled(ContainerBuilder $container): void
{
$requiredBundles = [
'doctrine' => DoctrineBundle::class,
'security' => SecurityBundle::class,
];

if ($container->hasParameter('league.oauth2_server.persistence.doctrine.enabled')) {
$requiredBundles['doctrine'] = DoctrineBundle::class;
}

foreach ($requiredBundles as $bundleAlias => $requiredBundle) {
if (!$container->hasExtension($bundleAlias)) {
throw new \LogicException(sprintf('Bundle \'%s\' needs to be enabled in your application kernel.', $requiredBundle));
Expand Down Expand Up @@ -233,6 +241,9 @@ private function configurePersistence(LoaderInterface $loader, ContainerBuilder
$loader->load('storage/doctrine.php');
$this->configureDoctrinePersistence($container, $config, $persistenceConfig);
break;
case 'custom':
$this->configureCustomPersistence($container, $persistenceConfig);
break;
}
}

Expand Down Expand Up @@ -291,6 +302,17 @@ private function configureInMemoryPersistence(ContainerBuilder $container, array
$container->setParameter('league.oauth2_server.persistence.in_memory.enabled', true);
}

private function configureCustomPersistence(ContainerBuilder $container, array $persistenceConfig): void
{
$container->setAlias(ClientManagerInterface::class, $persistenceConfig['client_manager']);
$container->setAlias(AccessTokenManagerInterface::class, $persistenceConfig['access_token_manager']);
$container->setAlias(RefreshTokenManagerInterface::class, $persistenceConfig['refresh_token_manager']);
$container->setAlias(AuthorizationCodeManagerInterface::class, $persistenceConfig['authorization_code_manager']);
$container->setAlias(CredentialsRevokerInterface::class, $persistenceConfig['credentials_revoker']);

$container->setParameter('league.oauth2_server.persistence.custom.enabled', true);
}

private function configureResourceServer(ContainerBuilder $container, array $config): void
{
$container
Expand Down
173 changes: 173 additions & 0 deletions tests/Acceptance/CustomPersistenceManagerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
<?php

declare(strict_types=1);

namespace League\Bundle\OAuth2ServerBundle\Tests\Acceptance;

use League\Bundle\OAuth2ServerBundle\Event\UserResolveEvent;
use League\Bundle\OAuth2ServerBundle\Manager\AccessTokenManagerInterface;
use League\Bundle\OAuth2ServerBundle\Manager\AuthorizationCodeManagerInterface;
use League\Bundle\OAuth2ServerBundle\Manager\ClientManagerInterface;
use League\Bundle\OAuth2ServerBundle\Manager\RefreshTokenManagerInterface;
use League\Bundle\OAuth2ServerBundle\Model\AccessToken;
use League\Bundle\OAuth2ServerBundle\Model\AuthorizationCode;
use League\Bundle\OAuth2ServerBundle\Model\Client;
use League\Bundle\OAuth2ServerBundle\Model\RefreshToken;
use League\Bundle\OAuth2ServerBundle\OAuth2Events;
use League\Bundle\OAuth2ServerBundle\Service\CredentialsRevokerInterface;
use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FakeAccessTokenManager;
use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FakeAuthorizationCodeManager;
use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FakeClientManager;
use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FakeCredentialsRevoker;
use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FakeRefreshTokenManager;
use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FixtureFactory;
use League\Bundle\OAuth2ServerBundle\Tests\TestHelper;
use League\Bundle\OAuth2ServerBundle\Tests\TestKernel;
use League\Bundle\OAuth2ServerBundle\ValueObject\RedirectUri;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\HttpKernel\KernelInterface;

class CustomPersistenceManagerTest extends AbstractAcceptanceTest
{
private AccessTokenManagerInterface&MockObject $accessTokenManager;
private ClientManagerInterface&MockObject $clientManager;
private RefreshTokenManagerInterface&MockObject $refreshTokenManager;
private AuthorizationCodeManagerInterface&MockObject $authCodeManager;

protected function setUp(): void
{
$this->client = self::createClient();
$this->accessTokenManager = $this->createMock(AccessTokenManagerInterface::class);
$this->clientManager = $this->createMock(ClientManagerInterface::class);
$this->refreshTokenManager = $this->createMock(RefreshTokenManagerInterface::class);
$this->authCodeManager = $this->createMock(AuthorizationCodeManagerInterface::class);
$this->application = new Application($this->client->getKernel());
}

public function testRegisteredServices(): void
{
static::assertInstanceOf(FakeAccessTokenManager::class, $this->client->getContainer()->get(AccessTokenManagerInterface::class));
static::assertInstanceOf(FakeAuthorizationCodeManager::class, $this->client->getContainer()->get(AuthorizationCodeManagerInterface::class));
static::assertInstanceOf(FakeClientManager::class, $this->client->getContainer()->get(ClientManagerInterface::class));
static::assertInstanceOf(FakeRefreshTokenManager::class, $this->client->getContainer()->get(RefreshTokenManagerInterface::class));
static::assertInstanceOf(FakeCredentialsRevoker::class, $this->client->getContainer()->get(CredentialsRevokerInterface::class));
}

public function testSuccessfulClientCredentialsRequest(): void
{
$this->accessTokenManager->expects(self::atLeastOnce())->method('find')->willReturn(null);
$this->accessTokenManager->expects(self::atLeastOnce())->method('save');
$this->client->getContainer()->set('test.access_token_manager', $this->accessTokenManager);

$this->clientManager->expects(self::atLeastOnce())->method('find')->with('foo')->willReturn(new Client('name', 'foo', 'secret'));
$this->client->getContainer()->set('test.client_manager', $this->clientManager);

$this->client->request('POST', '/token', [
'client_id' => 'foo',
'client_secret' => 'secret',
'grant_type' => 'client_credentials',
]);

$this->client->getResponse();
static::assertResponseIsSuccessful();
}

public function testSuccessfulPasswordRequest(): void
{
$this->accessTokenManager->expects(self::atLeastOnce())->method('find')->willReturn(null);
$this->accessTokenManager->expects(self::atLeastOnce())->method('save');
$this->client->getContainer()->set('test.access_token_manager', $this->accessTokenManager);

$this->clientManager->expects(self::atLeastOnce())->method('find')->with('foo')->willReturn(new Client('name', 'foo', 'secret'));
$this->client->getContainer()->set('test.client_manager', $this->clientManager);

$eventDispatcher = $this->client->getContainer()->get('event_dispatcher');
$eventDispatcher->addListener(OAuth2Events::USER_RESOLVE, static function (UserResolveEvent $event): void {
$event->setUser(FixtureFactory::createUser());
});

$this->client->request('POST', '/token', [
'client_id' => 'foo',
'client_secret' => 'secret',
'grant_type' => 'password',
'username' => 'user',
'password' => 'pass',
]);

$this->client->getResponse();
static::assertResponseIsSuccessful();
}

public function testSuccessfulRefreshTokenRequest(): void
{
$client = new Client('name', 'foo', 'secret');
$accessToken = new AccessToken('access_token', new \DateTimeImmutable('+1 hour'), $client, 'user', []);
$refreshToken = new RefreshToken('refresh_token', new \DateTimeImmutable('+1 month'), $accessToken);

$this->refreshTokenManager->expects(self::atLeastOnce())->method('find')->willReturn($refreshToken, null);
$this->client->getContainer()->set('test.refresh_token_manager', $this->refreshTokenManager);

$this->accessTokenManager->expects(self::atLeastOnce())->method('find')->willReturn($accessToken, null);
$this->accessTokenManager->expects(self::atLeastOnce())->method('save');
$this->client->getContainer()->set('test.access_token_manager', $this->accessTokenManager);

$this->clientManager->expects(self::atLeastOnce())->method('find')->with('foo')->willReturn($client);
$this->client->getContainer()->set('test.client_manager', $this->clientManager);

$this->client->request('POST', '/token', [
'client_id' => 'foo',
'client_secret' => 'secret',
'grant_type' => 'refresh_token',
'refresh_token' => TestHelper::generateEncryptedPayload($refreshToken),
]);

$this->client->getResponse();
static::assertResponseIsSuccessful();
}

public function testSuccessfulAuthorizationCodeRequest(): void
{
$client = new Client('name', 'foo', 'secret');
$client->setRedirectUris(new RedirectUri('https://example.org/oauth2/redirect-uri'));
$authCode = new AuthorizationCode('authorization_code', new \DateTimeImmutable('+2 minute'), $client, 'user', []);

$this->authCodeManager->expects(self::atLeastOnce())->method('find')->willReturn($authCode, null);
$this->client->getContainer()->set('test.authorization_code_manager', $this->authCodeManager);

$this->accessTokenManager->expects(self::atLeastOnce())->method('find')->willReturn(null);
$this->accessTokenManager->expects(self::atLeastOnce())->method('save');
$this->client->getContainer()->set('test.access_token_manager', $this->accessTokenManager);

$this->clientManager->expects(self::atLeastOnce())->method('find')->with('foo')->willReturn($client);
$this->client->getContainer()->set('test.client_manager', $this->clientManager);

$this->client->request('POST', '/token', [
'client_id' => 'foo',
'client_secret' => 'secret',
'grant_type' => 'authorization_code',
'redirect_uri' => 'https://example.org/oauth2/redirect-uri',
'code' => TestHelper::generateEncryptedAuthCodePayload($authCode),
]);

$this->client->getResponse();
static::assertResponseIsSuccessful();
}

protected static function createKernel(array $options = []): KernelInterface
{
return new TestKernel(
'test',
false,
[
'custom' => [
'access_token_manager' => 'test.access_token_manager',
'authorization_code_manager' => 'test.authorization_code_manager',
'client_manager' => 'test.client_manager',
'refresh_token_manager' => 'test.refresh_token_manager',
'credentials_revoker' => 'test.credentials_revoker',
],
]
);
}
}
Loading

0 comments on commit 81a32a0

Please sign in to comment.