Skip to content

Commit e4d5221

Browse files
committed
feature #171 Allow custom persistence managers (frankdekker)
This PR was merged into the 0.4-dev branch. Discussion ---------- Allow custom persistence managers Added support for configuring custom persistence manager. As improvement implementation on #145 **Configuration:** ```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 ``` **Code changes:** - Extended the Configuration class. - Added support in the Extension class. - Skipped the doctrine extension check if `in_memory` or `custom` persistence is chosen. - Added acceptance test, testing the different auth flows. - Updated the `readme.md` to include instructions how to set the custom persistence managers. **Implementation notes** I would've really liked to make `doctrine/orm` _not_ a mandatory dependency but move it to the suggests section in composer.json. We have a project where we can't use Doctrine, and need to implement our own persistence manager, but now 6-7 doctrine packages will be included in the project. Could we move `doctrine/orm` to suggests section in a future version? Commits ------- 81a32a0 Add configuration support for custom persistence
2 parents 21a5fae + 81a32a0 commit e4d5221

11 files changed

+483
-6
lines changed

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ security:
157157
* [Using custom client](using-custom-client.md)
158158
* [Listening to League OAuth Server events](listening-to-league-events.md)
159159
* [Password Grant Handling](password-grant-handling.md)
160+
* [Using custom persistence managers](using-custom-persistence-managers.md)
160161
161162
## Contributing
162163
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Using custom persistence managers
2+
3+
Implement the 4 interfaces from the `League\Bundle\OAuth2ServerBundle\Manager` namespace:
4+
- [AccessTokenManagerInterface](../src/Manager/AccessTokenManagerInterface.php)
5+
- [AuthorizationCodeManagerInterface](../src/Manager/AuthorizationCodeManagerInterface.php)
6+
- [ClientManagerInterface](../src/Manager/ClientManagerInterface.php)
7+
- [RefreshTokenManagerInterface](../src/Manager/RefreshTokenManagerInterface.php)
8+
And the interface for `CredentialsRevokerInterface`:
9+
- [CredentialsRevokerInterface](../src/Service/CredentialsRevokerInterface.php)
10+
11+
```php
12+
13+
Example:
14+
15+
```php
16+
class MyAccessTokenManager implements AccessTokenManagerInterface
17+
{
18+
}
19+
20+
class MyAuthorizationCodeManager implements AuthorizationCodeManagerInterface
21+
{
22+
}
23+
24+
class MyClientManager implements ClientManagerInterface
25+
{
26+
}
27+
28+
class MyRefreshTokenManager implements RefreshTokenManagerInterface
29+
{
30+
}
31+
32+
class MyCredentialsRevoker implements CredentialsRevokerInterface
33+
{
34+
}
35+
```
36+
37+
Then register the services in the container:
38+
39+
```yaml
40+
services:
41+
_defaults:
42+
autoconfigure: true
43+
44+
App\Manager\MyAccessTokenManager: ~
45+
App\Manager\MyAuthorizationCodeManager: ~
46+
App\Manager\MyClientManager: ~
47+
App\Manager\MyRefreshTokenManager: ~
48+
App\Service\MyCredentialsRevoker: ~
49+
```
50+
51+
Finally, configure the bundle to use the new managers:
52+
53+
```yaml
54+
league_oauth2_server:
55+
persistence:
56+
custom:
57+
access_token_manager: App\Manager\MyAccessTokenManager
58+
authorization_code_manager: App\Manager\MyAuthorizationCodeManager
59+
client_manager: App\Manager\MyClientManager
60+
refresh_token_manager: App\Manager\MyRefreshTokenManager
61+
credentials_revoker: App\Service\MyCredentialsRevoker
62+
```
63+
64+
## Optional
65+
66+
Example MySql table schema for custom persistence managers implementation:
67+
```sql
68+
CREATE TABLE `oauth2_access_token` (
69+
`identifier` char(80) NOT NULL,
70+
`client` varchar(32) NOT NULL,
71+
`expiry` datetime NOT NULL,
72+
`userIdentifier` varchar(128) DEFAULT NULL,
73+
`scopes` text,
74+
`revoked` tinyint(1) NOT NULL,
75+
PRIMARY KEY (`identifier`),
76+
KEY `client` (`client`)
77+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
78+
79+
CREATE TABLE `oauth2_authorization_code` (
80+
`identifier` char(80) NOT NULL,
81+
`client` varchar(32) NOT NULL,
82+
`expiry` datetime NOT NULL,
83+
`userIdentifier` varchar(128) DEFAULT NULL,
84+
`scopes` text,
85+
`revoked` tinyint(1) NOT NULL,
86+
PRIMARY KEY (`identifier`),
87+
KEY `client` (`client`)
88+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
89+
90+
CREATE TABLE `oauth2_client` (
91+
`identifier` varchar(32) NOT NULL,
92+
`name` varchar(128) NOT NULL,
93+
`secret` varchar(128) DEFAULT NULL,
94+
`redirectUris` text,
95+
`grants` text,
96+
`scopes` text,
97+
`active` tinyint(1) NOT NULL,
98+
`allowPlainTextPkce` tinyint(1) NOT NULL DEFAULT '0',
99+
PRIMARY KEY (`identifier`)
100+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
101+
102+
CREATE TABLE `oauth2_refresh_token` (
103+
`identifier` char(80) NOT NULL,
104+
`access_token` char(80) DEFAULT NULL,
105+
`expiry` datetime NOT NULL,
106+
`revoked` tinyint(1) NOT NULL,
107+
PRIMARY KEY (`identifier`),
108+
KEY `access_token` (`access_token`)
109+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
110+
```

src/DependencyInjection/Configuration.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,36 @@ private function createPersistenceNode(): NodeDefinition
195195
// In-memory persistence
196196
->scalarNode('in_memory')
197197
->end()
198+
// Custom persistence
199+
->arrayNode('custom')
200+
->children()
201+
->scalarNode('access_token_manager')
202+
->info('Service id of the custom access token manager')
203+
->cannotBeEmpty()
204+
->isRequired()
205+
->end()
206+
->scalarNode('authorization_code_manager')
207+
->info('Service id of the custom authorization code manager')
208+
->cannotBeEmpty()
209+
->isRequired()
210+
->end()
211+
->scalarNode('client_manager')
212+
->info('Service id of the custom client manager')
213+
->cannotBeEmpty()
214+
->isRequired()
215+
->end()
216+
->scalarNode('refresh_token_manager')
217+
->info('Service id of the custom refresh token manager')
218+
->cannotBeEmpty()
219+
->isRequired()
220+
->end()
221+
->scalarNode('credentials_revoker')
222+
->info('Service id of the custom credentials revoker')
223+
->cannotBeEmpty()
224+
->isRequired()
225+
->end()
226+
->end()
227+
->end()
198228
->end()
199229
;
200230

src/DependencyInjection/LeagueOAuth2ServerExtension.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,20 @@
1010
use League\Bundle\OAuth2ServerBundle\DBAL\Type\Grant as GrantType;
1111
use League\Bundle\OAuth2ServerBundle\DBAL\Type\RedirectUri as RedirectUriType;
1212
use League\Bundle\OAuth2ServerBundle\DBAL\Type\Scope as ScopeType;
13+
use League\Bundle\OAuth2ServerBundle\Manager\AccessTokenManagerInterface;
14+
use League\Bundle\OAuth2ServerBundle\Manager\AuthorizationCodeManagerInterface;
15+
use League\Bundle\OAuth2ServerBundle\Manager\ClientManagerInterface;
1316
use League\Bundle\OAuth2ServerBundle\Manager\Doctrine\AccessTokenManager;
1417
use League\Bundle\OAuth2ServerBundle\Manager\Doctrine\AuthorizationCodeManager;
1518
use League\Bundle\OAuth2ServerBundle\Manager\Doctrine\ClientManager;
1619
use League\Bundle\OAuth2ServerBundle\Manager\Doctrine\RefreshTokenManager;
1720
use League\Bundle\OAuth2ServerBundle\Manager\InMemory\AccessTokenManager as InMemoryAccessTokenManager;
21+
use League\Bundle\OAuth2ServerBundle\Manager\RefreshTokenManagerInterface;
1822
use League\Bundle\OAuth2ServerBundle\Manager\ScopeManagerInterface;
1923
use League\Bundle\OAuth2ServerBundle\Persistence\Mapping\Driver;
2024
use League\Bundle\OAuth2ServerBundle\Security\Authenticator\OAuth2Authenticator;
2125
use League\Bundle\OAuth2ServerBundle\Service\CredentialsRevoker\DoctrineCredentialsRevoker;
26+
use League\Bundle\OAuth2ServerBundle\Service\CredentialsRevokerInterface;
2227
use League\Bundle\OAuth2ServerBundle\ValueObject\Scope as ScopeModel;
2328
use League\OAuth2\Server\AuthorizationServer;
2429
use League\OAuth2\Server\CryptKey;
@@ -104,10 +109,13 @@ public function process(ContainerBuilder $container)
104109
private function assertRequiredBundlesAreEnabled(ContainerBuilder $container): void
105110
{
106111
$requiredBundles = [
107-
'doctrine' => DoctrineBundle::class,
108112
'security' => SecurityBundle::class,
109113
];
110114

115+
if ($container->hasParameter('league.oauth2_server.persistence.doctrine.enabled')) {
116+
$requiredBundles['doctrine'] = DoctrineBundle::class;
117+
}
118+
111119
foreach ($requiredBundles as $bundleAlias => $requiredBundle) {
112120
if (!$container->hasExtension($bundleAlias)) {
113121
throw new \LogicException(sprintf('Bundle \'%s\' needs to be enabled in your application kernel.', $requiredBundle));
@@ -233,6 +241,9 @@ private function configurePersistence(LoaderInterface $loader, ContainerBuilder
233241
$loader->load('storage/doctrine.php');
234242
$this->configureDoctrinePersistence($container, $config, $persistenceConfig);
235243
break;
244+
case 'custom':
245+
$this->configureCustomPersistence($container, $persistenceConfig);
246+
break;
236247
}
237248
}
238249

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

305+
private function configureCustomPersistence(ContainerBuilder $container, array $persistenceConfig): void
306+
{
307+
$container->setAlias(ClientManagerInterface::class, $persistenceConfig['client_manager']);
308+
$container->setAlias(AccessTokenManagerInterface::class, $persistenceConfig['access_token_manager']);
309+
$container->setAlias(RefreshTokenManagerInterface::class, $persistenceConfig['refresh_token_manager']);
310+
$container->setAlias(AuthorizationCodeManagerInterface::class, $persistenceConfig['authorization_code_manager']);
311+
$container->setAlias(CredentialsRevokerInterface::class, $persistenceConfig['credentials_revoker']);
312+
313+
$container->setParameter('league.oauth2_server.persistence.custom.enabled', true);
314+
}
315+
294316
private function configureResourceServer(ContainerBuilder $container, array $config): void
295317
{
296318
$container
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace League\Bundle\OAuth2ServerBundle\Tests\Acceptance;
6+
7+
use League\Bundle\OAuth2ServerBundle\Event\UserResolveEvent;
8+
use League\Bundle\OAuth2ServerBundle\Manager\AccessTokenManagerInterface;
9+
use League\Bundle\OAuth2ServerBundle\Manager\AuthorizationCodeManagerInterface;
10+
use League\Bundle\OAuth2ServerBundle\Manager\ClientManagerInterface;
11+
use League\Bundle\OAuth2ServerBundle\Manager\RefreshTokenManagerInterface;
12+
use League\Bundle\OAuth2ServerBundle\Model\AccessToken;
13+
use League\Bundle\OAuth2ServerBundle\Model\AuthorizationCode;
14+
use League\Bundle\OAuth2ServerBundle\Model\Client;
15+
use League\Bundle\OAuth2ServerBundle\Model\RefreshToken;
16+
use League\Bundle\OAuth2ServerBundle\OAuth2Events;
17+
use League\Bundle\OAuth2ServerBundle\Service\CredentialsRevokerInterface;
18+
use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FakeAccessTokenManager;
19+
use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FakeAuthorizationCodeManager;
20+
use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FakeClientManager;
21+
use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FakeCredentialsRevoker;
22+
use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FakeRefreshTokenManager;
23+
use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FixtureFactory;
24+
use League\Bundle\OAuth2ServerBundle\Tests\TestHelper;
25+
use League\Bundle\OAuth2ServerBundle\Tests\TestKernel;
26+
use League\Bundle\OAuth2ServerBundle\ValueObject\RedirectUri;
27+
use PHPUnit\Framework\MockObject\MockObject;
28+
use Symfony\Bundle\FrameworkBundle\Console\Application;
29+
use Symfony\Component\HttpKernel\KernelInterface;
30+
31+
class CustomPersistenceManagerTest extends AbstractAcceptanceTest
32+
{
33+
private AccessTokenManagerInterface&MockObject $accessTokenManager;
34+
private ClientManagerInterface&MockObject $clientManager;
35+
private RefreshTokenManagerInterface&MockObject $refreshTokenManager;
36+
private AuthorizationCodeManagerInterface&MockObject $authCodeManager;
37+
38+
protected function setUp(): void
39+
{
40+
$this->client = self::createClient();
41+
$this->accessTokenManager = $this->createMock(AccessTokenManagerInterface::class);
42+
$this->clientManager = $this->createMock(ClientManagerInterface::class);
43+
$this->refreshTokenManager = $this->createMock(RefreshTokenManagerInterface::class);
44+
$this->authCodeManager = $this->createMock(AuthorizationCodeManagerInterface::class);
45+
$this->application = new Application($this->client->getKernel());
46+
}
47+
48+
public function testRegisteredServices(): void
49+
{
50+
static::assertInstanceOf(FakeAccessTokenManager::class, $this->client->getContainer()->get(AccessTokenManagerInterface::class));
51+
static::assertInstanceOf(FakeAuthorizationCodeManager::class, $this->client->getContainer()->get(AuthorizationCodeManagerInterface::class));
52+
static::assertInstanceOf(FakeClientManager::class, $this->client->getContainer()->get(ClientManagerInterface::class));
53+
static::assertInstanceOf(FakeRefreshTokenManager::class, $this->client->getContainer()->get(RefreshTokenManagerInterface::class));
54+
static::assertInstanceOf(FakeCredentialsRevoker::class, $this->client->getContainer()->get(CredentialsRevokerInterface::class));
55+
}
56+
57+
public function testSuccessfulClientCredentialsRequest(): void
58+
{
59+
$this->accessTokenManager->expects(self::atLeastOnce())->method('find')->willReturn(null);
60+
$this->accessTokenManager->expects(self::atLeastOnce())->method('save');
61+
$this->client->getContainer()->set('test.access_token_manager', $this->accessTokenManager);
62+
63+
$this->clientManager->expects(self::atLeastOnce())->method('find')->with('foo')->willReturn(new Client('name', 'foo', 'secret'));
64+
$this->client->getContainer()->set('test.client_manager', $this->clientManager);
65+
66+
$this->client->request('POST', '/token', [
67+
'client_id' => 'foo',
68+
'client_secret' => 'secret',
69+
'grant_type' => 'client_credentials',
70+
]);
71+
72+
$this->client->getResponse();
73+
static::assertResponseIsSuccessful();
74+
}
75+
76+
public function testSuccessfulPasswordRequest(): void
77+
{
78+
$this->accessTokenManager->expects(self::atLeastOnce())->method('find')->willReturn(null);
79+
$this->accessTokenManager->expects(self::atLeastOnce())->method('save');
80+
$this->client->getContainer()->set('test.access_token_manager', $this->accessTokenManager);
81+
82+
$this->clientManager->expects(self::atLeastOnce())->method('find')->with('foo')->willReturn(new Client('name', 'foo', 'secret'));
83+
$this->client->getContainer()->set('test.client_manager', $this->clientManager);
84+
85+
$eventDispatcher = $this->client->getContainer()->get('event_dispatcher');
86+
$eventDispatcher->addListener(OAuth2Events::USER_RESOLVE, static function (UserResolveEvent $event): void {
87+
$event->setUser(FixtureFactory::createUser());
88+
});
89+
90+
$this->client->request('POST', '/token', [
91+
'client_id' => 'foo',
92+
'client_secret' => 'secret',
93+
'grant_type' => 'password',
94+
'username' => 'user',
95+
'password' => 'pass',
96+
]);
97+
98+
$this->client->getResponse();
99+
static::assertResponseIsSuccessful();
100+
}
101+
102+
public function testSuccessfulRefreshTokenRequest(): void
103+
{
104+
$client = new Client('name', 'foo', 'secret');
105+
$accessToken = new AccessToken('access_token', new \DateTimeImmutable('+1 hour'), $client, 'user', []);
106+
$refreshToken = new RefreshToken('refresh_token', new \DateTimeImmutable('+1 month'), $accessToken);
107+
108+
$this->refreshTokenManager->expects(self::atLeastOnce())->method('find')->willReturn($refreshToken, null);
109+
$this->client->getContainer()->set('test.refresh_token_manager', $this->refreshTokenManager);
110+
111+
$this->accessTokenManager->expects(self::atLeastOnce())->method('find')->willReturn($accessToken, null);
112+
$this->accessTokenManager->expects(self::atLeastOnce())->method('save');
113+
$this->client->getContainer()->set('test.access_token_manager', $this->accessTokenManager);
114+
115+
$this->clientManager->expects(self::atLeastOnce())->method('find')->with('foo')->willReturn($client);
116+
$this->client->getContainer()->set('test.client_manager', $this->clientManager);
117+
118+
$this->client->request('POST', '/token', [
119+
'client_id' => 'foo',
120+
'client_secret' => 'secret',
121+
'grant_type' => 'refresh_token',
122+
'refresh_token' => TestHelper::generateEncryptedPayload($refreshToken),
123+
]);
124+
125+
$this->client->getResponse();
126+
static::assertResponseIsSuccessful();
127+
}
128+
129+
public function testSuccessfulAuthorizationCodeRequest(): void
130+
{
131+
$client = new Client('name', 'foo', 'secret');
132+
$client->setRedirectUris(new RedirectUri('https://example.org/oauth2/redirect-uri'));
133+
$authCode = new AuthorizationCode('authorization_code', new \DateTimeImmutable('+2 minute'), $client, 'user', []);
134+
135+
$this->authCodeManager->expects(self::atLeastOnce())->method('find')->willReturn($authCode, null);
136+
$this->client->getContainer()->set('test.authorization_code_manager', $this->authCodeManager);
137+
138+
$this->accessTokenManager->expects(self::atLeastOnce())->method('find')->willReturn(null);
139+
$this->accessTokenManager->expects(self::atLeastOnce())->method('save');
140+
$this->client->getContainer()->set('test.access_token_manager', $this->accessTokenManager);
141+
142+
$this->clientManager->expects(self::atLeastOnce())->method('find')->with('foo')->willReturn($client);
143+
$this->client->getContainer()->set('test.client_manager', $this->clientManager);
144+
145+
$this->client->request('POST', '/token', [
146+
'client_id' => 'foo',
147+
'client_secret' => 'secret',
148+
'grant_type' => 'authorization_code',
149+
'redirect_uri' => 'https://example.org/oauth2/redirect-uri',
150+
'code' => TestHelper::generateEncryptedAuthCodePayload($authCode),
151+
]);
152+
153+
$this->client->getResponse();
154+
static::assertResponseIsSuccessful();
155+
}
156+
157+
protected static function createKernel(array $options = []): KernelInterface
158+
{
159+
return new TestKernel(
160+
'test',
161+
false,
162+
[
163+
'custom' => [
164+
'access_token_manager' => 'test.access_token_manager',
165+
'authorization_code_manager' => 'test.authorization_code_manager',
166+
'client_manager' => 'test.client_manager',
167+
'refresh_token_manager' => 'test.refresh_token_manager',
168+
'credentials_revoker' => 'test.credentials_revoker',
169+
],
170+
]
171+
);
172+
}
173+
}

0 commit comments

Comments
 (0)