-
Notifications
You must be signed in to change notification settings - Fork 89
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
11 changed files
with
483 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
], | ||
] | ||
); | ||
} | ||
} |
Oops, something went wrong.