Skip to content
This repository was archived by the owner on Jan 29, 2020. It is now read-only.

Enable event listeners via config. Tests. Docs. #62

Merged
Merged
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
46 changes: 45 additions & 1 deletion docs/book/v1/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,50 @@ 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.

### 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

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):

Expand All @@ -152,7 +196,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**.
Expand Down
92 changes: 86 additions & 6 deletions src/AuthorizationServerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,30 @@
namespace Zend\Expressive\Authentication\OAuth2;

use DateInterval;
use League\Event\ListenerProviderInterface;

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);
Expand All @@ -46,7 +56,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;
}
Expand All @@ -57,6 +67,76 @@ 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;
}

/**
* Optionally add event listeners
*
* @param AuthorizationServer $authServer
* @param ContainerInterface $container
*/
private function addListeners(
AuthorizationServer $authServer,
ContainerInterface $container
): void {
$listeners = $this->getListenersConfig($container);

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);
}
}
}
42 changes: 42 additions & 0 deletions src/ConfigTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,46 @@ protected function getGrantsConfig(ContainerInterface $container) : array

return $config['grants'];
}

/**
* @param ContainerInterface $container
*
* @return array
*/
protected function getListenersConfig(ContainerInterface $container) : array
{
$config = $container->get('config')['authentication'] ?? [];

if (empty($config['event_listeners'])) {
return [];
}
if (! is_array($config['event_listeners'])) {
throw new InvalidConfigException(
'The event_listeners config must be an array value'
);
}

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'];
}
}
Loading