Skip to content

Commit

Permalink
feature #65 Add ability to listen to League events via Symfony listen…
Browse files Browse the repository at this point in the history
…ers/subscribers (X-Coder264)

This PR was merged into the 0.1-dev branch.

Discussion
----------

Add ability to listen to League events via Symfony listeners/subscribers

This is a very simple approach to emit League OAuth server events via the Symfony event dispatcher so that users can create their own listeners in their Symfony applications.

Closes #35

Commits
-------

cf5b9fc Enable users to listen on League events via Symfony listeners
  • Loading branch information
chalasr committed Dec 4, 2021
2 parents 4e39513 + cf5b9fc commit b8c2b16
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 52 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@
"php": ">=7.2",
"doctrine/doctrine-bundle": "^2.0.8",
"doctrine/orm": "^2.7.1",
"league/oauth2-server": "^8.0",
"league/oauth2-server": "^8.3",
"nyholm/psr7": "^1.4",
"psr/http-factory": "^1.0",
"symfony/event-dispatcher": "^5.3|^6.0",
"symfony/framework-bundle": "^5.3|^6.0",
"symfony/polyfill-php81": "^1.22",
"symfony/psr-http-message-bridge": "^2.0",
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ security:
* [Token scopes](token-scopes.md)
* [Implementing custom grant type](implementing-custom-grant-type.md)
* [Using custom client](using-custom-client.md)
* [Listening to League OAuth Server events](listening-to-league-events.md)
## Contributing
Expand Down
37 changes: 37 additions & 0 deletions docs/listening-to-league-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Listening to League OAuth Server events

During the lifecycle of a request passing through the authorization server a number of events may be dispatched.
A list of those event names can be found in the constants of the `\League\OAuth2\Server\RequestEvent` class.

In order to listen to those events you need to create a standard Symfony event listener or subscriber for them.

Example:

1. Create the event listener.

```php
<?php

declare(strict_types=1);

namespace App\EventListener;

use League\OAuth2\Server\RequestAccessTokenEvent;

final class FooListener
{
public function onAccessTokenIssuedEvent(RequestAccessTokenEvent $event): void
{
// do something
}
}
```

1. Register the event listener:

```yaml
services:
App\EventListener\FooListener:
tags:
- { name: kernel.event_listener, event: access_token.issued, method: onAccessTokenIssuedEvent }
```
12 changes: 12 additions & 0 deletions src/Resources/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
use League\Bundle\OAuth2ServerBundle\Repository\UserRepository;
use League\Bundle\OAuth2ServerBundle\Security\Authenticator\OAuth2Authenticator;
use League\Bundle\OAuth2ServerBundle\Security\EventListener\CheckScopeListener;
use League\Bundle\OAuth2ServerBundle\Service\SymfonyLeagueEventListenerProvider;
use League\Event\Emitter;
use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\Grant\AuthCodeGrant;
use League\OAuth2\Server\Grant\ClientCredentialsGrant;
Expand Down Expand Up @@ -130,6 +132,15 @@
->tag('kernel.event_subscriber')
->alias(CheckScopeListener::class, 'league.oauth2_server.listener.check_scope')

->set('league.oauth2_server.symfony_league_listener_provider', SymfonyLeagueEventListenerProvider::class)
->args([
service('event_dispatcher'),
])
->alias(SymfonyLeagueEventListenerProvider::class, 'league.oauth2_server.symfony_league_listener_provider')

->set('league.oauth2_server.emitter', Emitter::class)
->call('useListenerProvider', [service('league.oauth2_server.symfony_league_listener_provider')])

->set('league.oauth2_server.authorization_server.grant_configurator', GrantConfigurator::class)
->args([
tagged_iterator('league.oauth2_server.authorization_server.grant'),
Expand All @@ -145,6 +156,7 @@
null,
null,
])
->call('setEmitter', [service('league.oauth2_server.emitter')])
->configurator(service(GrantConfigurator::class))
->alias(AuthorizationServer::class, 'league.oauth2_server.authorization_server')

Expand Down
37 changes: 37 additions & 0 deletions src/Service/SymfonyLeagueEventListenerProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace League\Bundle\OAuth2ServerBundle\Service;

use League\Event\EventInterface;
use League\Event\ListenerAcceptorInterface;
use League\Event\ListenerProviderInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

final class SymfonyLeagueEventListenerProvider implements ListenerProviderInterface
{
/**
* @var EventDispatcherInterface
*/
private $eventDispatcher;

public function __construct(EventDispatcherInterface $eventDispatcher)
{
$this->eventDispatcher = $eventDispatcher;
}

public function provideListeners(ListenerAcceptorInterface $listenerAcceptor)
{
$listener = \Closure::fromCallable([$this, 'dispatchLeagueEventWithSymfonyEventDispatcher']);

$listenerAcceptor->addListener('*', $listener);

return $this;
}

private function dispatchLeagueEventWithSymfonyEventDispatcher(EventInterface $event): void
{
$this->eventDispatcher->dispatch($event, $event->getName());
}
}
182 changes: 131 additions & 51 deletions tests/Acceptance/TokenEndpointTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
use League\Bundle\OAuth2ServerBundle\OAuth2Events;
use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FixtureFactory;
use League\Bundle\OAuth2ServerBundle\Tests\TestHelper;
use League\OAuth2\Server\RequestAccessTokenEvent;
use League\OAuth2\Server\RequestEvent;
use League\OAuth2\Server\RequestRefreshTokenEvent;

final class TokenEndpointTest extends AbstractAcceptanceTest
{
Expand All @@ -32,19 +35,26 @@ protected function setUp(): void

public function testSuccessfulClientCredentialsRequest(): void
{
$eventDispatcher = $this->client->getContainer()->get('event_dispatcher');

$eventDispatcher->addListener(OAuth2Events::TOKEN_REQUEST_RESOLVE, static function (TokenRequestResolveEvent $event): void {
$event->getResponse()->headers->set('foo', 'bar');
});

$wasRequestAccessTokenEventDispatched = false;
$accessToken = null;

$eventDispatcher->addListener(RequestEvent::ACCESS_TOKEN_ISSUED, static function (RequestAccessTokenEvent $event) use (&$wasRequestAccessTokenEventDispatched, &$accessToken): void {
$wasRequestAccessTokenEventDispatched = true;
$accessToken = $event->getAccessToken();
});

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

$this->client
->getContainer()
->get('event_dispatcher')
->addListener(OAuth2Events::TOKEN_REQUEST_RESOLVE, static function (TokenRequestResolveEvent $event): void {
$event->getResponse()->headers->set('foo', 'bar');
});

$response = $this->client->getResponse();

$this->assertSame(200, $response->getStatusCode());
Expand All @@ -56,24 +66,41 @@ public function testSuccessfulClientCredentialsRequest(): void
$this->assertLessThanOrEqual(3600, $jsonResponse['expires_in']);
$this->assertGreaterThan(0, $jsonResponse['expires_in']);
$this->assertNotEmpty($jsonResponse['access_token']);
$this->assertEmpty($response->headers->get('foo'), 'bar');
$this->assertArrayNotHasKey('refresh_token', $jsonResponse);
$this->assertSame('bar', $response->headers->get('foo'));

$this->assertTrue($wasRequestAccessTokenEventDispatched);

$this->assertSame('foo', $accessToken->getClient()->getIdentifier());
$this->assertNull($accessToken->getUserIdentifier());
}

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

$this->client
->getContainer()
->get('event_dispatcher')
->addListener(OAuth2Events::TOKEN_REQUEST_RESOLVE, static function (TokenRequestResolveEvent $event): void {
$event->getResponse()->headers->set('foo', 'bar');
});
$eventDispatcher->addListener(OAuth2Events::USER_RESOLVE, static function (UserResolveEvent $event): void {
$event->setUser(FixtureFactory::createUser());
});

$eventDispatcher->addListener(OAuth2Events::TOKEN_REQUEST_RESOLVE, static function (TokenRequestResolveEvent $event): void {
$event->getResponse()->headers->set('foo', 'bar');
});

$wasRequestAccessTokenEventDispatched = false;
$wasRequestRefreshTokenEventDispatched = false;
$accessToken = null;
$refreshToken = null;

$eventDispatcher->addListener(RequestEvent::ACCESS_TOKEN_ISSUED, static function (RequestAccessTokenEvent $event) use (&$wasRequestAccessTokenEventDispatched, &$accessToken): void {
$wasRequestAccessTokenEventDispatched = true;
$accessToken = $event->getAccessToken();
});

$eventDispatcher->addListener(RequestEvent::REFRESH_TOKEN_ISSUED, static function (RequestRefreshTokenEvent $event) use (&$wasRequestRefreshTokenEventDispatched, &$refreshToken): void {
$wasRequestRefreshTokenEventDispatched = true;
$refreshToken = $event->getRefreshToken();
});

$this->client->request('POST', '/token', [
'client_id' => 'foo',
Expand All @@ -96,6 +123,13 @@ public function testSuccessfulPasswordRequest(): void
$this->assertNotEmpty($jsonResponse['access_token']);
$this->assertNotEmpty($jsonResponse['refresh_token']);
$this->assertSame($response->headers->get('foo'), 'bar');

$this->assertTrue($wasRequestAccessTokenEventDispatched);
$this->assertTrue($wasRequestRefreshTokenEventDispatched);

$this->assertSame('foo', $accessToken->getClient()->getIdentifier());
$this->assertSame('user', $accessToken->getUserIdentifier());
$this->assertSame($accessToken->getIdentifier(), $refreshToken->getAccessToken()->getIdentifier());
}

public function testSuccessfulRefreshTokenRequest(): void
Expand All @@ -105,24 +139,35 @@ public function testSuccessfulRefreshTokenRequest(): void
->get(RefreshTokenManagerInterface::class)
->find(FixtureFactory::FIXTURE_REFRESH_TOKEN);

$this->client
->getContainer()
->get('event_dispatcher')
->addListener(OAuth2Events::TOKEN_REQUEST_RESOLVE, static function (TokenRequestResolveEvent $event): void {
$event->getResponse()->headers->set('foo', 'bar');
});
$eventDispatcher = $this->client->getContainer()->get('event_dispatcher');

$this->client
->getContainer()
->get('event_dispatcher')
->addListener(OAuth2Events::TOKEN_REQUEST_RESOLVE, static function (TokenRequestResolveEvent $event): void {
if ('bar' === $event->getResponse()->headers->get('foo')) {
$newResponse = clone $event->getResponse();
$newResponse->headers->remove('foo');
$newResponse->headers->set('baz', 'qux');
$event->setResponse($newResponse);
}
}, -1);
$eventDispatcher->addListener(OAuth2Events::TOKEN_REQUEST_RESOLVE, static function (TokenRequestResolveEvent $event): void {
$event->getResponse()->headers->set('foo', 'bar');
});

$eventDispatcher->addListener(OAuth2Events::TOKEN_REQUEST_RESOLVE, static function (TokenRequestResolveEvent $event): void {
if ('bar' === $event->getResponse()->headers->get('foo')) {
$newResponse = clone $event->getResponse();
$newResponse->headers->remove('foo');
$newResponse->headers->set('baz', 'qux');
$event->setResponse($newResponse);
}
}, -1);

$wasRequestAccessTokenEventDispatched = false;
$wasRequestRefreshTokenEventDispatched = false;
$accessToken = null;
$refreshTokenEntity = null;

$eventDispatcher->addListener(RequestEvent::ACCESS_TOKEN_ISSUED, static function (RequestAccessTokenEvent $event) use (&$wasRequestAccessTokenEventDispatched, &$accessToken): void {
$wasRequestAccessTokenEventDispatched = true;
$accessToken = $event->getAccessToken();
});

$eventDispatcher->addListener(RequestEvent::REFRESH_TOKEN_ISSUED, static function (RequestRefreshTokenEvent $event) use (&$wasRequestRefreshTokenEventDispatched, &$refreshTokenEntity): void {
$wasRequestRefreshTokenEventDispatched = true;
$refreshTokenEntity = $event->getRefreshToken();
});

$this->client->request('POST', '/token', [
'client_id' => 'foo',
Expand All @@ -145,6 +190,12 @@ public function testSuccessfulRefreshTokenRequest(): void
$this->assertNotEmpty($jsonResponse['refresh_token']);
$this->assertFalse($response->headers->has('foo'));
$this->assertSame($response->headers->get('baz'), 'qux');

$this->assertTrue($wasRequestAccessTokenEventDispatched);
$this->assertTrue($wasRequestRefreshTokenEventDispatched);

$this->assertSame($refreshToken->getAccessToken()->getClient()->getIdentifier(), $accessToken->getClient()->getIdentifier());
$this->assertSame($accessToken->getIdentifier(), $refreshTokenEntity->getAccessToken()->getIdentifier());
}

public function testSuccessfulAuthorizationCodeRequest(): void
Expand Down Expand Up @@ -190,12 +241,26 @@ public function testSuccessfulAuthorizationCodeRequestWithPublicClient(): void
->get(AuthorizationCodeManagerInterface::class)
->find(FixtureFactory::FIXTURE_AUTH_CODE_PUBLIC_CLIENT);

$this->client
->getContainer()
->get('event_dispatcher')
->addListener(OAuth2Events::TOKEN_REQUEST_RESOLVE, static function (TokenRequestResolveEvent $event): void {
$event->getResponse()->headers->set('foo', 'bar');
});
$eventDispatcher = $this->client->getContainer()->get('event_dispatcher');

$eventDispatcher->addListener(OAuth2Events::TOKEN_REQUEST_RESOLVE, static function (TokenRequestResolveEvent $event): void {
$event->getResponse()->headers->set('foo', 'bar');
});

$wasRequestAccessTokenEventDispatched = false;
$wasRequestRefreshTokenEventDispatched = false;
$accessToken = null;
$refreshToken = null;

$eventDispatcher->addListener(RequestEvent::ACCESS_TOKEN_ISSUED, static function (RequestAccessTokenEvent $event) use (&$wasRequestAccessTokenEventDispatched, &$accessToken): void {
$wasRequestAccessTokenEventDispatched = true;
$accessToken = $event->getAccessToken();
});

$eventDispatcher->addListener(RequestEvent::REFRESH_TOKEN_ISSUED, static function (RequestRefreshTokenEvent $event) use (&$wasRequestRefreshTokenEventDispatched, &$refreshToken): void {
$wasRequestRefreshTokenEventDispatched = true;
$refreshToken = $event->getRefreshToken();
});

$this->client->request('POST', '/token', [
'client_id' => FixtureFactory::FIXTURE_PUBLIC_CLIENT,
Expand All @@ -215,7 +280,15 @@ public function testSuccessfulAuthorizationCodeRequestWithPublicClient(): void
$this->assertLessThanOrEqual(3600, $jsonResponse['expires_in']);
$this->assertGreaterThan(0, $jsonResponse['expires_in']);
$this->assertNotEmpty($jsonResponse['access_token']);
$this->assertNotEmpty($jsonResponse['refresh_token']);
$this->assertSame($response->headers->get('foo'), 'bar');

$this->assertTrue($wasRequestAccessTokenEventDispatched);
$this->assertTrue($wasRequestRefreshTokenEventDispatched);

$this->assertSame($authCode->getClient()->getIdentifier(), $accessToken->getClient()->getIdentifier());
$this->assertSame($authCode->getUserIdentifier(), $accessToken->getUserIdentifier());
$this->assertSame($accessToken->getIdentifier(), $refreshToken->getAccessToken()->getIdentifier());
}

public function testFailedTokenRequest(): void
Expand All @@ -236,19 +309,24 @@ public function testFailedTokenRequest(): void

public function testFailedClientCredentialsTokenRequest(): void
{
$eventDispatcher = $this->client->getContainer()->get('event_dispatcher');

$eventDispatcher->addListener(OAuth2Events::TOKEN_REQUEST_RESOLVE, static function (TokenRequestResolveEvent $event): void {
$event->getResponse()->headers->set('foo', 'bar');
});

$wasClientAuthenticationEventDispatched = false;

$eventDispatcher->addListener(RequestEvent::CLIENT_AUTHENTICATION_FAILED, static function (RequestEvent $event) use (&$wasClientAuthenticationEventDispatched, &$accessToken): void {
$wasClientAuthenticationEventDispatched = true;
});

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

$this->client
->getContainer()
->get('event_dispatcher')
->addListener(OAuth2Events::TOKEN_REQUEST_RESOLVE, static function (TokenRequestResolveEvent $event): void {
$event->getResponse()->headers->set('foo', 'bar');
});

$response = $this->client->getResponse();

$this->assertSame(401, $response->getStatusCode());
Expand All @@ -258,6 +336,8 @@ public function testFailedClientCredentialsTokenRequest(): void

$this->assertSame('invalid_client', $jsonResponse['error']);
$this->assertSame('Client authentication failed', $jsonResponse['message']);
$this->assertEmpty($response->headers->get('foo'), 'bar');
$this->assertSame('bar', $response->headers->get('foo'));

$this->assertTrue($wasClientAuthenticationEventDispatched);
}
}

0 comments on commit b8c2b16

Please sign in to comment.