From 242406ed07038222504d54e94842df43921e6c77 Mon Sep 17 00:00:00 2001 From: Marc Guyer Date: Tue, 30 Jul 2019 16:54:57 -0400 Subject: [PATCH 1/8] Enable event listeners via config. Tests. Docs. --- config/oauth2.php | 19 +++++++++ docs/book/v1/intro.md | 16 +++++++ src/AuthorizationServerFactory.php | 49 +++++++++++++++++++--- src/ConfigTrait.php | 21 ++++++++++ test/AuthorizationServerFactoryTest.php | 55 +++++++++++++++++++++++++ test/ConfigTraitTest.php | 39 ++++++++++++++++++ 6 files changed, 193 insertions(+), 6 deletions(-) diff --git a/config/oauth2.php b/config/oauth2.php index 9abb090..70ef994 100644 --- a/config/oauth2.php +++ b/config/oauth2.php @@ -35,6 +35,25 @@ \League\OAuth2\Server\Grant\RefreshTokenGrant::class => \League\OAuth2\Server\Grant\RefreshTokenGrant::class ], + + // optionally add listener config + // 'listeners' => [ + // [ + // // event name + // \League\OAuth2\Server\RequestEvent::CLIENT_AUTHENTICATION_FAILED, + // // listener defined as a service key + // \My\League\Event\Listener\For\Auth\Failure::class, + // ], [ + // \League\OAuth2\Server\RequestEvent::CLIENT_AUTHENTICATION_FAILED, + // // listener defined as an anonymous function + // function (\League\OAuth2\Server\RequestEvent $event) { + // // do something + // }, + // // priority NORMAL (0) is the default but you may set an + // // int val of your choosing + // // League\Event\ListenerAcceptorInterface::P_HIGH, + // ], + // ], ]; // Conditionally include the encryption_key config setting, based on presence of file. diff --git a/docs/book/v1/intro.md b/docs/book/v1/intro.md index 9759f54..27e23d2 100644 --- a/docs/book/v1/intro.md +++ b/docs/book/v1/intro.md @@ -74,6 +74,20 @@ return [ Grant\ImplicitGrant::class => Grant\ImplicitGrant::class, Grant\RefreshTokenGrant::class => Grant\RefreshTokenGrant::class ], + + // optionally configure event listeners + // 'listeners' => [ + // [ + // \League\OAuth2\Server\RequestEvent::CLIENT_AUTHENTICATION_FAILED, + // \My\Event\Listener\Service::class, + // ], + // [ + // \League\OAuth2\Server\RequestEvent::ACCESS_TOKEN_ISSUED, + // function (\League\OAuth2\Server\RequestEvent $event) { + // // do something + // }, + // ], + // ], ]; ``` @@ -126,6 +140,8 @@ grants are configured to be available. If you would like to disable any of the supplied grants, change the value for the grant to `null`. Additionally, you can extend this array to add your own custom grants. +The `listeners` array is for enabling event listeners. Listeners are not required. This is an array of arrays. Each array in the list must contain at least 2 elements. The first element must be a string that corresponds to the name of the event to listen for. The second element must be either a string or an anonymous function. If it's a string it's assumed to be a container service key that points to your listener. There may be a third element of the array -- it must be an integer. The third element is the `$priority` argument when registering the listener. See the [Authorization Server Domain Events documentation](https://oauth2.thephpleague.com/authorization-server/events/). + You need to provide an OAuth2 database yourself, or generate a [SQLite](https://www.sqlite.org) database with the following command (using `sqlite3` for GNU/Linux): diff --git a/src/AuthorizationServerFactory.php b/src/AuthorizationServerFactory.php index 1ecbc76..9d4ded8 100644 --- a/src/AuthorizationServerFactory.php +++ b/src/AuthorizationServerFactory.php @@ -12,19 +12,27 @@ use DateInterval; use League\OAuth2\Server\AuthorizationServer; -use League\OAuth2\Server\Grant; -use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface; -use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; -use League\OAuth2\Server\Repositories\UserRepositoryInterface; use Psr\Container\ContainerInterface; -use Zend\Expressive\Authentication\OAuth2\Exception\InvalidConfigException; +/** + * Factory for OAuth AuthorizationServer + * + * Initializes a new AuthorizationServer with required params from config. + * Then configured grant types are enabled with configured access token + * expiry. Then any optionally configured event listeners are attached to the + * AuthorizationServer. + */ class AuthorizationServerFactory { use ConfigTrait; use CryptKeyTrait; use RepositoryTrait; + /** + * @param ContainerInterface $container + * + * @return AuthorizationServer + */ public function __invoke(ContainerInterface $container) : AuthorizationServer { $clientRepository = $this->getClientRepository($container); @@ -46,7 +54,7 @@ public function __invoke(ContainerInterface $container) : AuthorizationServer $accessTokenInterval = new DateInterval($this->getAccessTokenExpire($container)); foreach ($grants as $grant) { - // Config may set this grant to null. Continue on if grant has been disabled + // Config may set this grant to null. Continue on if grant has been disabled if (empty($grant)) { continue; } @@ -57,6 +65,35 @@ public function __invoke(ContainerInterface $container) : AuthorizationServer ); } + // add listeners if configured + $this->addListeners($authServer, $container); + return $authServer; } + + /** + * Optionally add event listeners + * + * @param AuthorizationServer $authServer + * @param ContainerInterface $container + */ + private function addListeners( + AuthorizationServer $authServer, + ContainerInterface $container + ): void { + $listeners = $this->getListenersConfig($container); + if (null === $listeners) { + return; + } + foreach ($listeners as $listenerConfig) { + $event = $listenerConfig[0]; + $listener = $listenerConfig[1]; + $priority = $listenerConfig[2] ?? null; + if (is_string($listener)) { + $listener = $container->get($listener); + } + $authServer->getEmitter() + ->addListener($event, $listener, $priority); + } + } } diff --git a/src/ConfigTrait.php b/src/ConfigTrait.php index 163e9bf..7822406 100644 --- a/src/ConfigTrait.php +++ b/src/ConfigTrait.php @@ -103,4 +103,25 @@ protected function getGrantsConfig(ContainerInterface $container) : array return $config['grants']; } + + /** + * @param ContainerInterface $container + * + * @return array|null + */ + protected function getListenersConfig(ContainerInterface $container) : ?array + { + $config = $container->get('config')['authentication'] ?? []; + + if (empty($config['listeners'])) { + return null; + } + if (! is_array($config['listeners'])) { + throw new InvalidConfigException( + 'The listeners must be an array value' + ); + } + + return $config['listeners']; + } } diff --git a/test/AuthorizationServerFactoryTest.php b/test/AuthorizationServerFactoryTest.php index ef1df13..ae3a9bd 100644 --- a/test/AuthorizationServerFactoryTest.php +++ b/test/AuthorizationServerFactoryTest.php @@ -19,6 +19,8 @@ use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; use Zend\Expressive\Authentication\OAuth2\AuthorizationServerFactory; +use League\OAuth2\Server\RequestEvent; +use League\Event\ListenerInterface; use function array_merge; use function array_slice; @@ -66,4 +68,57 @@ public function testInvoke() $this->assertInstanceOf(AuthorizationServer::class, $result); } + + public function testInvokeWithListenerConfig() + { + $mockContainer = $this->prophesize(ContainerInterface::class); + $mockClientRepo = $this->prophesize(ClientRepositoryInterface::class); + $mockAccessTokenRepo = $this->prophesize(AccessTokenRepositoryInterface::class); + $mockScopeRepo = $this->prophesize(ScopeRepositoryInterface::class); + $mockClientGrant = $this->prophesize(GrantTypeInterface::class); + $mockPasswordGrant = $this->prophesize(GrantTypeInterface::class); + + $mockListener = $this->prophesize(ListenerInterface::class); + $mockContainer->get(ListenerInterface::class) + ->will([$mockListener, 'reveal']); + + $config = [ + 'authentication' => [ + 'private_key' => __DIR__ . '/TestAsset/private.key', + 'encryption_key' => 'iALlwJ1sH77dmFCJFo+pMdM6Af4bF/hCca1EDDx7MwE=', + 'access_token_expire' => 'P1D', + 'grants' => [ + ClientCredentialsGrant::class + => ClientCredentialsGrant::class, + ], + 'listeners' => [ + [ + RequestEvent::CLIENT_AUTHENTICATION_FAILED, + function (RequestEvent $event) { + // do something + } + ], [ + RequestEvent::CLIENT_AUTHENTICATION_FAILED, + ListenerInterface::class + ] + ] + ] + ]; + + $mockContainer->has(ClientRepositoryInterface::class)->willReturn(true); + $mockContainer->has(AccessTokenRepositoryInterface::class)->willReturn(true); + $mockContainer->has(ScopeRepositoryInterface::class)->willReturn(true); + + $mockContainer->get(ClientRepositoryInterface::class)->willReturn($mockClientRepo->reveal()); + $mockContainer->get(AccessTokenRepositoryInterface::class)->willReturn($mockAccessTokenRepo->reveal()); + $mockContainer->get(ScopeRepositoryInterface::class)->willReturn($mockScopeRepo->reveal()); + $mockContainer->get(ClientCredentialsGrant::class)->willReturn($mockClientGrant->reveal()); + $mockContainer->get('config')->willReturn($config); + + $factory = new AuthorizationServerFactory(); + + $result = $factory($mockContainer->reveal()); + + $this->assertInstanceOf(AuthorizationServer::class, $result); + } } diff --git a/test/ConfigTraitTest.php b/test/ConfigTraitTest.php index 52c71aa..4460349 100644 --- a/test/ConfigTraitTest.php +++ b/test/ConfigTraitTest.php @@ -152,4 +152,43 @@ public function testGetGrantsConfig() $result = $this->trait->proxy('getGrantsConfig', $this->container->reveal()); $this->assertEquals($this->config['authentication']['grants'], $result); } + + public function testGetListenersConfigNoConfig() + { + $this->container + ->get('config') + ->willReturn([]); + $result = $this->trait + ->proxy('getListenersConfig', $this->container->reveal()); + $this->assertNull($result); + } + + /** + * @expectedException Zend\Expressive\Authentication\OAuth2\Exception\InvalidConfigException + */ + public function testGetListenersConfigNoArrayValue() + { + $this->container + ->get('config') + ->willReturn([ + 'authentication' => [ + 'listeners' => 'xxx', + ], + ]); + + $this->trait->proxy('getListenersConfig', $this->container->reveal()); + } + + public function testGetListenersConfig() + { + $this->container->get('config') + ->willReturn([ + 'authentication' => [ + 'listeners' => $expected = [['xxx']], + ], + ]); + $result = $this->trait + ->proxy('getListenersConfig', $this->container->reveal()); + $this->assertEquals($expected, $result); + } } From 49a0cf739bca89ad355f64cf2a0960b64f1afac6 Mon Sep 17 00:00:00 2001 From: Marc Guyer Date: Sun, 11 Aug 2019 22:09:47 -0400 Subject: [PATCH 2/8] Add support for listener providers config * Remove commented out example listeners config * Rewrite docs with subheadings and better example configs * Empty config returns empty array instead of null * Add some basic error handling for missing container services * Tests --- docs/book/v1/intro.md | 58 ++++++--- src/AuthorizationServerFactory.php | 51 +++++++- src/ConfigTrait.php | 35 ++++- test/AuthorizationServerFactoryTest.php | 164 ++++++++++++++++++++++-- test/ConfigTraitTest.php | 45 ++++++- 5 files changed, 311 insertions(+), 42 deletions(-) diff --git a/docs/book/v1/intro.md b/docs/book/v1/intro.md index 27e23d2..eda3fb1 100644 --- a/docs/book/v1/intro.md +++ b/docs/book/v1/intro.md @@ -74,20 +74,6 @@ return [ Grant\ImplicitGrant::class => Grant\ImplicitGrant::class, Grant\RefreshTokenGrant::class => Grant\RefreshTokenGrant::class ], - - // optionally configure event listeners - // 'listeners' => [ - // [ - // \League\OAuth2\Server\RequestEvent::CLIENT_AUTHENTICATION_FAILED, - // \My\Event\Listener\Service::class, - // ], - // [ - // \League\OAuth2\Server\RequestEvent::ACCESS_TOKEN_ISSUED, - // function (\League\OAuth2\Server\RequestEvent $event) { - // // do something - // }, - // ], - // ], ]; ``` @@ -140,7 +126,47 @@ grants are configured to be available. If you would like to disable any of the supplied grants, change the value for the grant to `null`. Additionally, you can extend this array to add your own custom grants. -The `listeners` array is for enabling event listeners. Listeners are not required. This is an array of arrays. Each array in the list must contain at least 2 elements. The first element must be a string that corresponds to the name of the event to listen for. The second element must be either a string or an anonymous function. If it's a string it's assumed to be a container service key that points to your listener. There may be a third element of the array -- it must be an integer. The third element is the `$priority` argument when registering the listener. See the [Authorization Server Domain Events documentation](https://oauth2.thephpleague.com/authorization-server/events/). +### Configure Event Listeners + +_Optional_ The `event-listeners` and `event-listener-providers` arrays may be used to enable event listeners for events published by `league\oauth2-server`. See the [Authorization Server Domain Events documentation](https://oauth2.thephpleague.com/authorization-server/events/). The possible event names can be found [in `League\OAuth2\Server\RequestEvent`](https://github.com/thephpleague/oauth2-server/blob/0b0b43d43342c0909b3b32fb7a09d502c368d2ec/src/RequestEvent.php#L17-L22). + +#### Event Listeners + +The `event-listeners` key must contain an array of arrays. Each array element must contain at least 2 elements and may include a 3rd element. These roughly correspond to the arguments passed to [`League\Event\ListenerAcceptorInterface::addListener()`](https://github.com/thephpleague/event/blob/d2cc124cf9a3fab2bb4ff963307f60361ce4d119/src/ListenerAcceptorInterface.php#L43). The first element must be a string -- either the [wildcard (`*`)](https://event.thephpleague.com/2.0/listeners/wildcard/) or a [single event name](https://event.thephpleague.com/2.0/events/named/). The second element must be either a callable, a concrete instance of `League\Event\ListenerInterface`, or a string pointing to your listener service instance in the container. The third element is optional, and must be an integer if provided. + +See the [documentation for callable listeners](https://event.thephpleague.com/2.0/listeners/callables/). + +#### Event Listener Providers + +The `event-listener-providers` key must contain an array. Each array element must contain either a concrete instance of `League\Event\ListenerProviderInterface` or a string pointing to your container service instance of a listener provider. + +See the [documentation for listener providers](https://event.thephpleague.com/2.0/listeners/providers/). + +Example config: + +```php +return [ + 'event-listeners' => [ + // using a container service + [ + \League\OAuth2\Server\RequestEvent::CLIENT_AUTHENTICATION_FAILED, + \My\Event\Listener\Service::class, + ], + // using a callable + [ + \League\OAuth2\Server\RequestEvent::ACCESS_TOKEN_ISSUED, + function (\League\OAuth2\Server\RequestEvent $event) { + // do something + }, + ], + ], + 'event-listener-providers' => [ + \My\Event\ListenerProvider\Service::class, + ], +]; +``` + +## OAuth2 Database You need to provide an OAuth2 database yourself, or generate a [SQLite](https://www.sqlite.org) database with the following command (using `sqlite3` for GNU/Linux): @@ -168,7 +194,7 @@ For security reason, the client `secret` and the user `password` are stored using the `bcrypt` algorithm as used by the [password_hash](http://php.net/manual/en/function.password-hash.php) function. -## Configure OAuth2 routes +## Configure OAuth2 Routes As the final step, in order to use the OAuth2 server you need to configure the routes for the **token endpoint** and **authorization**. diff --git a/src/AuthorizationServerFactory.php b/src/AuthorizationServerFactory.php index 9d4ded8..6eb1106 100644 --- a/src/AuthorizationServerFactory.php +++ b/src/AuthorizationServerFactory.php @@ -11,6 +11,8 @@ namespace Zend\Expressive\Authentication\OAuth2; use DateInterval; +use League\Event\ListenerProviderInterface; + use League\OAuth2\Server\AuthorizationServer; use Psr\Container\ContainerInterface; @@ -68,6 +70,9 @@ public function __invoke(ContainerInterface $container) : AuthorizationServer // add listeners if configured $this->addListeners($authServer, $container); + // add listener providers if configured + $this->addListenerProviders($authServer, $container); + return $authServer; } @@ -82,18 +87,56 @@ private function addListeners( ContainerInterface $container ): void { $listeners = $this->getListenersConfig($container); - if (null === $listeners) { - return; - } - foreach ($listeners as $listenerConfig) { + + foreach ($listeners as $idx => $listenerConfig) { $event = $listenerConfig[0]; $listener = $listenerConfig[1]; $priority = $listenerConfig[2] ?? null; if (is_string($listener)) { + if (!$container->has($listener)) { + throw new Exception\InvalidConfigException(sprintf( + 'The second element of event-listeners config at ' . + 'index "%s" is a string and therefore expected to ' . + 'be available as a service key in the container. ' . + 'A service named "%s" was not found.', + $idx, + $listener + )); + } $listener = $container->get($listener); } $authServer->getEmitter() ->addListener($event, $listener, $priority); } } + + /** + * Optionally add event listener providers + * + * @param AuthorizationServer $authServer + * @param ContainerInterface $container + */ + private function addListenerProviders( + AuthorizationServer $authServer, + ContainerInterface $container + ): void { + $providers = $this->getListenerProvidersConfig($container); + + foreach ($providers as $idx => $provider) { + if (is_string($provider)) { + if (!$container->has($provider)) { + throw new Exception\InvalidConfigException(sprintf( + 'The event-listener-providers config at ' . + 'index "%s" is a string and therefore expected to ' . + 'be available as a service key in the container. ' . + 'A service named "%s" was not found.', + $idx, + $provider + )); + } + $provider = $container->get($provider); + } + $authServer->getEmitter()->useListenerProvider($provider); + } + } } diff --git a/src/ConfigTrait.php b/src/ConfigTrait.php index 7822406..b424412 100644 --- a/src/ConfigTrait.php +++ b/src/ConfigTrait.php @@ -107,21 +107,42 @@ protected function getGrantsConfig(ContainerInterface $container) : array /** * @param ContainerInterface $container * - * @return array|null + * @return array */ - protected function getListenersConfig(ContainerInterface $container) : ?array + protected function getListenersConfig(ContainerInterface $container) : array { $config = $container->get('config')['authentication'] ?? []; - if (empty($config['listeners'])) { - return null; + if (empty($config['event-listeners'])) { + return []; } - if (! is_array($config['listeners'])) { + if (! is_array($config['event-listeners'])) { throw new InvalidConfigException( - 'The listeners must be an array value' + 'The event-listeners config must be an array value' ); } - return $config['listeners']; + return $config['event-listeners']; + } + + /** + * @param ContainerInterface $container + * + * @return array + */ + protected function getListenerProvidersConfig(ContainerInterface $container) : array + { + $config = $container->get('config')['authentication'] ?? []; + + if (empty($config['event-listener-providers'])) { + return []; + } + if (! is_array($config['event-listener-providers'])) { + throw new InvalidConfigException( + 'The event-listener-providers config must be an array value' + ); + } + + return $config['event-listener-providers']; } } diff --git a/test/AuthorizationServerFactoryTest.php b/test/AuthorizationServerFactoryTest.php index ae3a9bd..94c5990 100644 --- a/test/AuthorizationServerFactoryTest.php +++ b/test/AuthorizationServerFactoryTest.php @@ -10,13 +10,18 @@ namespace ZendTest\Expressive\Authentication\OAuth2; +use League\Event\ListenerProviderInterface; + use League\OAuth2\Server\AuthorizationServer; +use League\OAuth2\Server\Grant\PasswordGrant; use League\OAuth2\Server\Grant\ClientCredentialsGrant; use League\OAuth2\Server\Grant\GrantTypeInterface; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use League\OAuth2\Server\Repositories\ClientRepositoryInterface; use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; use PHPUnit\Framework\TestCase; +use Prophecy\Prophecy\ObjectProphecy; + use Psr\Container\ContainerInterface; use Zend\Expressive\Authentication\OAuth2\AuthorizationServerFactory; use League\OAuth2\Server\RequestEvent; @@ -26,8 +31,11 @@ use function array_slice; use function in_array; +use Zend\Expressive\Authentication\OAuth2\Exception\InvalidConfigException; + class AuthorizationServerFactoryTest extends TestCase { + public function testInvoke() { $mockContainer = $this->prophesize(ContainerInterface::class); @@ -69,8 +77,10 @@ public function testInvoke() $this->assertInstanceOf(AuthorizationServer::class, $result); } - public function testInvokeWithListenerConfig() - { + /** + * @return ObjectProphecy + */ + private function getContainerMock(): ObjectProphecy { $mockContainer = $this->prophesize(ContainerInterface::class); $mockClientRepo = $this->prophesize(ClientRepositoryInterface::class); $mockAccessTokenRepo = $this->prophesize(AccessTokenRepositoryInterface::class); @@ -78,9 +88,52 @@ public function testInvokeWithListenerConfig() $mockClientGrant = $this->prophesize(GrantTypeInterface::class); $mockPasswordGrant = $this->prophesize(GrantTypeInterface::class); + $mockContainer->has(ClientRepositoryInterface::class)->willReturn(true); + $mockContainer->has(AccessTokenRepositoryInterface::class)->willReturn(true); + $mockContainer->has(ScopeRepositoryInterface::class)->willReturn(true); + + $mockContainer->get(ClientRepositoryInterface::class)->willReturn($mockClientRepo->reveal()); + $mockContainer->get(AccessTokenRepositoryInterface::class)->willReturn($mockAccessTokenRepo->reveal()); + $mockContainer->get(ScopeRepositoryInterface::class)->willReturn($mockScopeRepo->reveal()); + $mockContainer->get(ClientCredentialsGrant::class)->willReturn($mockClientGrant->reveal()); + $mockContainer->get(PasswordGrant::class)->willReturn($mockPasswordGrant->reveal()); + + return $mockContainer; + } + + public function testInvokeWithNullGrant() + { + $mockContainer = $this->getContainerMock(); + + $config = [ + 'authentication' => [ + 'private_key' => __DIR__ . '/TestAsset/private.key', + 'encryption_key' => 'iALlwJ1sH77dmFCJFo+pMdM6Af4bF/hCca1EDDx7MwE=', + 'access_token_expire' => 'P1D', + 'grants' => [ + ClientCredentialsGrant::class + => null, + PasswordGrant::class + => PasswordGrant::class, + ], + ] + ]; + + $mockContainer->get('config')->willReturn($config); + + $factory = new AuthorizationServerFactory(); + + $result = $factory($mockContainer->reveal()); + + $this->assertInstanceOf(AuthorizationServer::class, $result); + } + + public function testInvokeWithListenerConfig() + { + $mockContainer = $this->getContainerMock(); $mockListener = $this->prophesize(ListenerInterface::class); - $mockContainer->get(ListenerInterface::class) - ->will([$mockListener, 'reveal']); + $mockContainer->has(ListenerInterface::class)->willReturn(true); + $mockContainer->get(ListenerInterface::class)->willReturn($mockListener->reveal()); $config = [ 'authentication' => [ @@ -91,7 +144,7 @@ public function testInvokeWithListenerConfig() ClientCredentialsGrant::class => ClientCredentialsGrant::class, ], - 'listeners' => [ + 'event-listeners' => [ [ RequestEvent::CLIENT_AUTHENTICATION_FAILED, function (RequestEvent $event) { @@ -105,14 +158,70 @@ function (RequestEvent $event) { ] ]; - $mockContainer->has(ClientRepositoryInterface::class)->willReturn(true); - $mockContainer->has(AccessTokenRepositoryInterface::class)->willReturn(true); - $mockContainer->has(ScopeRepositoryInterface::class)->willReturn(true); + $mockContainer->get('config')->willReturn($config); + + $factory = new AuthorizationServerFactory(); + + $result = $factory($mockContainer->reveal()); + + $this->assertInstanceOf(AuthorizationServer::class, $result); + } + + public function testInvokeWithListenerConfigMissingServiceThrowsException() + { + $mockContainer = $this->getContainerMock(); + $mockListener = $this->prophesize(ListenerInterface::class); + $mockContainer->has(ListenerInterface::class)->willReturn(false); + + $config = [ + 'authentication' => [ + 'private_key' => __DIR__ . '/TestAsset/private.key', + 'encryption_key' => 'iALlwJ1sH77dmFCJFo+pMdM6Af4bF/hCca1EDDx7MwE=', + 'access_token_expire' => 'P1D', + 'grants' => [ + ClientCredentialsGrant::class + => ClientCredentialsGrant::class, + ], + 'event-listeners' => [ + [ + RequestEvent::CLIENT_AUTHENTICATION_FAILED, + ListenerInterface::class + ] + ] + ] + ]; + + $mockContainer->get('config')->willReturn($config); + + $factory = new AuthorizationServerFactory(); + + $this->expectException(InvalidConfigException::class); + + $result = $factory($mockContainer->reveal()); + } + + public function testInvokeWithListenerProviderConfig() + { + $mockContainer = $this->getContainerMock(); + $mockProvider = $this->prophesize(ListenerProviderInterface::class); + $mockContainer->has(ListenerProviderInterface::class)->willReturn(true); + $mockContainer->get(ListenerProviderInterface::class)->willReturn($mockProvider->reveal()); + + $config = [ + 'authentication' => [ + 'private_key' => __DIR__ . '/TestAsset/private.key', + 'encryption_key' => 'iALlwJ1sH77dmFCJFo+pMdM6Af4bF/hCca1EDDx7MwE=', + 'access_token_expire' => 'P1D', + 'grants' => [ + ClientCredentialsGrant::class + => ClientCredentialsGrant::class, + ], + 'event-listener-providers' => [ + ListenerProviderInterface::class + ] + ] + ]; - $mockContainer->get(ClientRepositoryInterface::class)->willReturn($mockClientRepo->reveal()); - $mockContainer->get(AccessTokenRepositoryInterface::class)->willReturn($mockAccessTokenRepo->reveal()); - $mockContainer->get(ScopeRepositoryInterface::class)->willReturn($mockScopeRepo->reveal()); - $mockContainer->get(ClientCredentialsGrant::class)->willReturn($mockClientGrant->reveal()); $mockContainer->get('config')->willReturn($config); $factory = new AuthorizationServerFactory(); @@ -121,4 +230,35 @@ function (RequestEvent $event) { $this->assertInstanceOf(AuthorizationServer::class, $result); } + + public function testInvokeWithListenerProviderConfigMissingServiceThrowsException() + { + $mockContainer = $this->getContainerMock(); + $mockProvider = $this->prophesize(ListenerProviderInterface::class); + $mockContainer->has(ListenerProviderInterface::class)->willReturn(false); + + $config = [ + 'authentication' => [ + 'private_key' => __DIR__ . '/TestAsset/private.key', + 'encryption_key' => 'iALlwJ1sH77dmFCJFo+pMdM6Af4bF/hCca1EDDx7MwE=', + 'access_token_expire' => 'P1D', + 'grants' => [ + ClientCredentialsGrant::class + => ClientCredentialsGrant::class, + ], + 'event-listener-providers' => [ + ListenerProviderInterface::class + ] + ] + ]; + + $mockContainer->get('config')->willReturn($config); + + $factory = new AuthorizationServerFactory(); + + $this->expectException(InvalidConfigException::class); + + $result = $factory($mockContainer->reveal()); + + } } diff --git a/test/ConfigTraitTest.php b/test/ConfigTraitTest.php index 4460349..6e615f0 100644 --- a/test/ConfigTraitTest.php +++ b/test/ConfigTraitTest.php @@ -160,7 +160,7 @@ public function testGetListenersConfigNoConfig() ->willReturn([]); $result = $this->trait ->proxy('getListenersConfig', $this->container->reveal()); - $this->assertNull($result); + $this->assertInternalType('array', $result); } /** @@ -172,7 +172,7 @@ public function testGetListenersConfigNoArrayValue() ->get('config') ->willReturn([ 'authentication' => [ - 'listeners' => 'xxx', + 'event-listeners' => 'xxx', ], ]); @@ -184,11 +184,50 @@ public function testGetListenersConfig() $this->container->get('config') ->willReturn([ 'authentication' => [ - 'listeners' => $expected = [['xxx']], + 'event-listeners' => $expected = [['xxx']], ], ]); $result = $this->trait ->proxy('getListenersConfig', $this->container->reveal()); $this->assertEquals($expected, $result); } + + public function testGetListenerProvidersConfigNoConfig() + { + $this->container + ->get('config') + ->willReturn([]); + $result = $this->trait + ->proxy('getListenerProvidersConfig', $this->container->reveal()); + $this->assertInternalType('array', $result); + } + + /** + * @expectedException Zend\Expressive\Authentication\OAuth2\Exception\InvalidConfigException + */ + public function testGetListenerProvidersConfigNoArrayValue() + { + $this->container + ->get('config') + ->willReturn([ + 'authentication' => [ + 'event-listener-providers' => 'xxx', + ], + ]); + + $this->trait->proxy('getListenerProvidersConfig', $this->container->reveal()); + } + + public function testGetListenerProvidersConfig() + { + $this->container->get('config') + ->willReturn([ + 'authentication' => [ + 'event-listener-providers' => $expected = ['xxx'], + ], + ]); + $result = $this->trait + ->proxy('getListenerProvidersConfig', $this->container->reveal()); + $this->assertEquals($expected, $result); + } } From 9361dd22857ebc35c06aed2b99aa3109955d84e9 Mon Sep 17 00:00:00 2001 From: Marc Guyer Date: Sun, 11 Aug 2019 22:16:39 -0400 Subject: [PATCH 3/8] Fix cs errors --- src/AuthorizationServerFactory.php | 4 ++-- test/AuthorizationServerFactoryTest.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/AuthorizationServerFactory.php b/src/AuthorizationServerFactory.php index 6eb1106..e41d399 100644 --- a/src/AuthorizationServerFactory.php +++ b/src/AuthorizationServerFactory.php @@ -93,7 +93,7 @@ private function addListeners( $listener = $listenerConfig[1]; $priority = $listenerConfig[2] ?? null; if (is_string($listener)) { - if (!$container->has($listener)) { + if (! $container->has($listener)) { throw new Exception\InvalidConfigException(sprintf( 'The second element of event-listeners config at ' . 'index "%s" is a string and therefore expected to ' . @@ -124,7 +124,7 @@ private function addListenerProviders( foreach ($providers as $idx => $provider) { if (is_string($provider)) { - if (!$container->has($provider)) { + if (! $container->has($provider)) { throw new Exception\InvalidConfigException(sprintf( 'The event-listener-providers config at ' . 'index "%s" is a string and therefore expected to ' . diff --git a/test/AuthorizationServerFactoryTest.php b/test/AuthorizationServerFactoryTest.php index 94c5990..9edfb49 100644 --- a/test/AuthorizationServerFactoryTest.php +++ b/test/AuthorizationServerFactoryTest.php @@ -80,7 +80,8 @@ public function testInvoke() /** * @return ObjectProphecy */ - private function getContainerMock(): ObjectProphecy { + private function getContainerMock(): ObjectProphecy + { $mockContainer = $this->prophesize(ContainerInterface::class); $mockClientRepo = $this->prophesize(ClientRepositoryInterface::class); $mockAccessTokenRepo = $this->prophesize(AccessTokenRepositoryInterface::class); @@ -259,6 +260,5 @@ public function testInvokeWithListenerProviderConfigMissingServiceThrowsExceptio $this->expectException(InvalidConfigException::class); $result = $factory($mockContainer->reveal()); - } } From 43311f018b42233de05f5091ea57a3efc6096910 Mon Sep 17 00:00:00 2001 From: Marc Guyer Date: Wed, 14 Aug 2019 13:47:24 -0400 Subject: [PATCH 4/8] Remove commented-out listener config example --- config/oauth2.php | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/config/oauth2.php b/config/oauth2.php index 70ef994..9abb090 100644 --- a/config/oauth2.php +++ b/config/oauth2.php @@ -35,25 +35,6 @@ \League\OAuth2\Server\Grant\RefreshTokenGrant::class => \League\OAuth2\Server\Grant\RefreshTokenGrant::class ], - - // optionally add listener config - // 'listeners' => [ - // [ - // // event name - // \League\OAuth2\Server\RequestEvent::CLIENT_AUTHENTICATION_FAILED, - // // listener defined as a service key - // \My\League\Event\Listener\For\Auth\Failure::class, - // ], [ - // \League\OAuth2\Server\RequestEvent::CLIENT_AUTHENTICATION_FAILED, - // // listener defined as an anonymous function - // function (\League\OAuth2\Server\RequestEvent $event) { - // // do something - // }, - // // priority NORMAL (0) is the default but you may set an - // // int val of your choosing - // // League\Event\ListenerAcceptorInterface::P_HIGH, - // ], - // ], ]; // Conditionally include the encryption_key config setting, based on presence of file. From 68c8c2c174b1ffc9607a1a1881fe073d963015bf Mon Sep 17 00:00:00 2001 From: Marc Guyer Date: Wed, 14 Aug 2019 14:48:04 -0400 Subject: [PATCH 5/8] Use underscores for config keys --- docs/book/v1/intro.md | 10 +++++----- src/AuthorizationServerFactory.php | 4 ++-- src/ConfigTrait.php | 16 ++++++++-------- test/AuthorizationServerFactoryTest.php | 8 ++++---- test/ConfigTraitTest.php | 8 ++++---- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/book/v1/intro.md b/docs/book/v1/intro.md index eda3fb1..86e43e2 100644 --- a/docs/book/v1/intro.md +++ b/docs/book/v1/intro.md @@ -128,17 +128,17 @@ you can extend this array to add your own custom grants. ### Configure Event Listeners -_Optional_ The `event-listeners` and `event-listener-providers` arrays may be used to enable event listeners for events published by `league\oauth2-server`. See the [Authorization Server Domain Events documentation](https://oauth2.thephpleague.com/authorization-server/events/). The possible event names can be found [in `League\OAuth2\Server\RequestEvent`](https://github.com/thephpleague/oauth2-server/blob/0b0b43d43342c0909b3b32fb7a09d502c368d2ec/src/RequestEvent.php#L17-L22). +_Optional_ The `event_listeners` and `event_listener_providers` arrays may be used to enable event listeners for events published by `league\oauth2-server`. See the [Authorization Server Domain Events documentation](https://oauth2.thephpleague.com/authorization-server/events/). The possible event names can be found [in `League\OAuth2\Server\RequestEvent`](https://github.com/thephpleague/oauth2-server/blob/0b0b43d43342c0909b3b32fb7a09d502c368d2ec/src/RequestEvent.php#L17-L22). #### Event Listeners -The `event-listeners` key must contain an array of arrays. Each array element must contain at least 2 elements and may include a 3rd element. These roughly correspond to the arguments passed to [`League\Event\ListenerAcceptorInterface::addListener()`](https://github.com/thephpleague/event/blob/d2cc124cf9a3fab2bb4ff963307f60361ce4d119/src/ListenerAcceptorInterface.php#L43). The first element must be a string -- either the [wildcard (`*`)](https://event.thephpleague.com/2.0/listeners/wildcard/) or a [single event name](https://event.thephpleague.com/2.0/events/named/). The second element must be either a callable, a concrete instance of `League\Event\ListenerInterface`, or a string pointing to your listener service instance in the container. The third element is optional, and must be an integer if provided. +The `event_listeners` key must contain an array of arrays. Each array element must contain at least 2 elements and may include a 3rd element. These roughly correspond to the arguments passed to [`League\Event\ListenerAcceptorInterface::addListener()`](https://github.com/thephpleague/event/blob/d2cc124cf9a3fab2bb4ff963307f60361ce4d119/src/ListenerAcceptorInterface.php#L43). The first element must be a string -- either the [wildcard (`*`)](https://event.thephpleague.com/2.0/listeners/wildcard/) or a [single event name](https://event.thephpleague.com/2.0/events/named/). The second element must be either a callable, a concrete instance of `League\Event\ListenerInterface`, or a string pointing to your listener service instance in the container. The third element is optional, and must be an integer if provided. See the [documentation for callable listeners](https://event.thephpleague.com/2.0/listeners/callables/). #### Event Listener Providers -The `event-listener-providers` key must contain an array. Each array element must contain either a concrete instance of `League\Event\ListenerProviderInterface` or a string pointing to your container service instance of a listener provider. +The `event_listener_providers` key must contain an array. Each array element must contain either a concrete instance of `League\Event\ListenerProviderInterface` or a string pointing to your container service instance of a listener provider. See the [documentation for listener providers](https://event.thephpleague.com/2.0/listeners/providers/). @@ -146,7 +146,7 @@ Example config: ```php return [ - 'event-listeners' => [ + 'event_listeners' => [ // using a container service [ \League\OAuth2\Server\RequestEvent::CLIENT_AUTHENTICATION_FAILED, @@ -160,7 +160,7 @@ return [ }, ], ], - 'event-listener-providers' => [ + 'event_listener_providers' => [ \My\Event\ListenerProvider\Service::class, ], ]; diff --git a/src/AuthorizationServerFactory.php b/src/AuthorizationServerFactory.php index e41d399..b4ad06f 100644 --- a/src/AuthorizationServerFactory.php +++ b/src/AuthorizationServerFactory.php @@ -95,7 +95,7 @@ private function addListeners( if (is_string($listener)) { if (! $container->has($listener)) { throw new Exception\InvalidConfigException(sprintf( - 'The second element of event-listeners config at ' . + 'The second element of event_listeners config at ' . 'index "%s" is a string and therefore expected to ' . 'be available as a service key in the container. ' . 'A service named "%s" was not found.', @@ -126,7 +126,7 @@ private function addListenerProviders( if (is_string($provider)) { if (! $container->has($provider)) { throw new Exception\InvalidConfigException(sprintf( - 'The event-listener-providers config at ' . + 'The event_listener_providers config at ' . 'index "%s" is a string and therefore expected to ' . 'be available as a service key in the container. ' . 'A service named "%s" was not found.', diff --git a/src/ConfigTrait.php b/src/ConfigTrait.php index b424412..1b75670 100644 --- a/src/ConfigTrait.php +++ b/src/ConfigTrait.php @@ -113,16 +113,16 @@ protected function getListenersConfig(ContainerInterface $container) : array { $config = $container->get('config')['authentication'] ?? []; - if (empty($config['event-listeners'])) { + if (empty($config['event_listeners'])) { return []; } - if (! is_array($config['event-listeners'])) { + if (! is_array($config['event_listeners'])) { throw new InvalidConfigException( - 'The event-listeners config must be an array value' + 'The event_listeners config must be an array value' ); } - return $config['event-listeners']; + return $config['event_listeners']; } /** @@ -134,15 +134,15 @@ protected function getListenerProvidersConfig(ContainerInterface $container) : a { $config = $container->get('config')['authentication'] ?? []; - if (empty($config['event-listener-providers'])) { + if (empty($config['event_listener_providers'])) { return []; } - if (! is_array($config['event-listener-providers'])) { + if (! is_array($config['event_listener_providers'])) { throw new InvalidConfigException( - 'The event-listener-providers config must be an array value' + 'The event_listener_providers config must be an array value' ); } - return $config['event-listener-providers']; + return $config['event_listener_providers']; } } diff --git a/test/AuthorizationServerFactoryTest.php b/test/AuthorizationServerFactoryTest.php index 9edfb49..e84df8e 100644 --- a/test/AuthorizationServerFactoryTest.php +++ b/test/AuthorizationServerFactoryTest.php @@ -145,7 +145,7 @@ public function testInvokeWithListenerConfig() ClientCredentialsGrant::class => ClientCredentialsGrant::class, ], - 'event-listeners' => [ + 'event_listeners' => [ [ RequestEvent::CLIENT_AUTHENTICATION_FAILED, function (RequestEvent $event) { @@ -183,7 +183,7 @@ public function testInvokeWithListenerConfigMissingServiceThrowsException() ClientCredentialsGrant::class => ClientCredentialsGrant::class, ], - 'event-listeners' => [ + 'event_listeners' => [ [ RequestEvent::CLIENT_AUTHENTICATION_FAILED, ListenerInterface::class @@ -217,7 +217,7 @@ public function testInvokeWithListenerProviderConfig() ClientCredentialsGrant::class => ClientCredentialsGrant::class, ], - 'event-listener-providers' => [ + 'event_listener_providers' => [ ListenerProviderInterface::class ] ] @@ -247,7 +247,7 @@ public function testInvokeWithListenerProviderConfigMissingServiceThrowsExceptio ClientCredentialsGrant::class => ClientCredentialsGrant::class, ], - 'event-listener-providers' => [ + 'event_listener_providers' => [ ListenerProviderInterface::class ] ] diff --git a/test/ConfigTraitTest.php b/test/ConfigTraitTest.php index 6e615f0..2808660 100644 --- a/test/ConfigTraitTest.php +++ b/test/ConfigTraitTest.php @@ -172,7 +172,7 @@ public function testGetListenersConfigNoArrayValue() ->get('config') ->willReturn([ 'authentication' => [ - 'event-listeners' => 'xxx', + 'event_listeners' => 'xxx', ], ]); @@ -184,7 +184,7 @@ public function testGetListenersConfig() $this->container->get('config') ->willReturn([ 'authentication' => [ - 'event-listeners' => $expected = [['xxx']], + 'event_listeners' => $expected = [['xxx']], ], ]); $result = $this->trait @@ -211,7 +211,7 @@ public function testGetListenerProvidersConfigNoArrayValue() ->get('config') ->willReturn([ 'authentication' => [ - 'event-listener-providers' => 'xxx', + 'event_listener_providers' => 'xxx', ], ]); @@ -223,7 +223,7 @@ public function testGetListenerProvidersConfig() $this->container->get('config') ->willReturn([ 'authentication' => [ - 'event-listener-providers' => $expected = ['xxx'], + 'event_listener_providers' => $expected = ['xxx'], ], ]); $result = $this->trait From db8cc1ba8e9f5e5e89784aef6ed54c981a71c801 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sat, 28 Dec 2019 14:01:54 -0600 Subject: [PATCH 6/8] docs: note that configurable event listeners are from 1.3.0 forward --- docs/book/v1/intro.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/book/v1/intro.md b/docs/book/v1/intro.md index 86e43e2..16dbf3b 100644 --- a/docs/book/v1/intro.md +++ b/docs/book/v1/intro.md @@ -128,6 +128,8 @@ you can extend this array to add your own custom grants. ### Configure Event Listeners +- **Since 1.3.0** + _Optional_ The `event_listeners` and `event_listener_providers` arrays may be used to enable event listeners for events published by `league\oauth2-server`. See the [Authorization Server Domain Events documentation](https://oauth2.thephpleague.com/authorization-server/events/). The possible event names can be found [in `League\OAuth2\Server\RequestEvent`](https://github.com/thephpleague/oauth2-server/blob/0b0b43d43342c0909b3b32fb7a09d502c368d2ec/src/RequestEvent.php#L17-L22). #### Event Listeners From 08e5e5f29e42ff210b9408aa1ff81820f6c61fdb Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sat, 28 Dec 2019 14:05:25 -0600 Subject: [PATCH 7/8] qa: CS fixes for AuthorizationServerFactoryTest - trailing commas in array entries - consistent whitespace --- test/AuthorizationServerFactoryTest.php | 60 +++++++++++-------------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/test/AuthorizationServerFactoryTest.php b/test/AuthorizationServerFactoryTest.php index e84df8e..05d9898 100644 --- a/test/AuthorizationServerFactoryTest.php +++ b/test/AuthorizationServerFactoryTest.php @@ -51,10 +51,8 @@ public function testInvoke() 'encryption_key' => 'iALlwJ1sH77dmFCJFo+pMdM6Af4bF/hCca1EDDx7MwE=', 'access_token_expire' => 'P1D', 'grants' => [ - ClientCredentialsGrant::class - => ClientCredentialsGrant::class, - PasswordGrant::class - => PasswordGrant::class, + ClientCredentialsGrant::class => ClientCredentialsGrant::class, + PasswordGrant::class => PasswordGrant::class, ], ] ]; @@ -112,12 +110,10 @@ public function testInvokeWithNullGrant() 'encryption_key' => 'iALlwJ1sH77dmFCJFo+pMdM6Af4bF/hCca1EDDx7MwE=', 'access_token_expire' => 'P1D', 'grants' => [ - ClientCredentialsGrant::class - => null, - PasswordGrant::class - => PasswordGrant::class, + ClientCredentialsGrant::class => null, + PasswordGrant::class => PasswordGrant::class, ], - ] + ], ]; $mockContainer->get('config')->willReturn($config); @@ -142,21 +138,21 @@ public function testInvokeWithListenerConfig() 'encryption_key' => 'iALlwJ1sH77dmFCJFo+pMdM6Af4bF/hCca1EDDx7MwE=', 'access_token_expire' => 'P1D', 'grants' => [ - ClientCredentialsGrant::class - => ClientCredentialsGrant::class, + ClientCredentialsGrant::class => ClientCredentialsGrant::class, ], 'event_listeners' => [ [ RequestEvent::CLIENT_AUTHENTICATION_FAILED, function (RequestEvent $event) { // do something - } - ], [ + }, + ], + [ RequestEvent::CLIENT_AUTHENTICATION_FAILED, - ListenerInterface::class - ] - ] - ] + ListenerInterface::class, + ], + ], + ], ]; $mockContainer->get('config')->willReturn($config); @@ -180,16 +176,15 @@ public function testInvokeWithListenerConfigMissingServiceThrowsException() 'encryption_key' => 'iALlwJ1sH77dmFCJFo+pMdM6Af4bF/hCca1EDDx7MwE=', 'access_token_expire' => 'P1D', 'grants' => [ - ClientCredentialsGrant::class - => ClientCredentialsGrant::class, + ClientCredentialsGrant::class => ClientCredentialsGrant::class, ], 'event_listeners' => [ [ RequestEvent::CLIENT_AUTHENTICATION_FAILED, - ListenerInterface::class - ] - ] - ] + ListenerInterface::class, + ], + ], + ], ]; $mockContainer->get('config')->willReturn($config); @@ -214,13 +209,12 @@ public function testInvokeWithListenerProviderConfig() 'encryption_key' => 'iALlwJ1sH77dmFCJFo+pMdM6Af4bF/hCca1EDDx7MwE=', 'access_token_expire' => 'P1D', 'grants' => [ - ClientCredentialsGrant::class - => ClientCredentialsGrant::class, + ClientCredentialsGrant::class => ClientCredentialsGrant::class, ], 'event_listener_providers' => [ ListenerProviderInterface::class - ] - ] + ], + ], ]; $mockContainer->get('config')->willReturn($config); @@ -244,13 +238,12 @@ public function testInvokeWithListenerProviderConfigMissingServiceThrowsExceptio 'encryption_key' => 'iALlwJ1sH77dmFCJFo+pMdM6Af4bF/hCca1EDDx7MwE=', 'access_token_expire' => 'P1D', 'grants' => [ - ClientCredentialsGrant::class - => ClientCredentialsGrant::class, + ClientCredentialsGrant::class => ClientCredentialsGrant::class, ], 'event_listener_providers' => [ - ListenerProviderInterface::class - ] - ] + ListenerProviderInterface::class, + ], + ], ]; $mockContainer->get('config')->willReturn($config); @@ -258,7 +251,6 @@ public function testInvokeWithListenerProviderConfigMissingServiceThrowsExceptio $factory = new AuthorizationServerFactory(); $this->expectException(InvalidConfigException::class); - - $result = $factory($mockContainer->reveal()); + $factory($mockContainer->reveal()); } } From 93ddf21e717d2563952cdaee9f5c8b2b22645cb5 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sat, 28 Dec 2019 14:08:14 -0600 Subject: [PATCH 8/8] docs: adds CHANGELOG entry for #62 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5760a6..bb34f4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file, in reverse ### Added -- Nothing. +- [#62](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/62) adds the ability to configure and add event listeners for the underlying league/oauth2 implementation. See the [event listeners configuration documentation](https://docs.zendframework.com/zend-expressive-authentication-oauth2/intro/#configure-event-listeners) for more information. ### Changed