Skip to content

Commit cf5b9fc

Browse files
X-Coder264chalasr
authored andcommitted
Enable users to listen on League events via Symfony listeners
1 parent 4e39513 commit cf5b9fc

File tree

6 files changed

+220
-52
lines changed

6 files changed

+220
-52
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@
1919
"php": ">=7.2",
2020
"doctrine/doctrine-bundle": "^2.0.8",
2121
"doctrine/orm": "^2.7.1",
22-
"league/oauth2-server": "^8.0",
22+
"league/oauth2-server": "^8.3",
2323
"nyholm/psr7": "^1.4",
2424
"psr/http-factory": "^1.0",
25+
"symfony/event-dispatcher": "^5.3|^6.0",
2526
"symfony/framework-bundle": "^5.3|^6.0",
2627
"symfony/polyfill-php81": "^1.22",
2728
"symfony/psr-http-message-bridge": "^2.0",

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ security:
148148
* [Token scopes](token-scopes.md)
149149
* [Implementing custom grant type](implementing-custom-grant-type.md)
150150
* [Using custom client](using-custom-client.md)
151+
* [Listening to League OAuth Server events](listening-to-league-events.md)
151152
152153
## Contributing
153154

docs/listening-to-league-events.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Listening to League OAuth Server events
2+
3+
During the lifecycle of a request passing through the authorization server a number of events may be dispatched.
4+
A list of those event names can be found in the constants of the `\League\OAuth2\Server\RequestEvent` class.
5+
6+
In order to listen to those events you need to create a standard Symfony event listener or subscriber for them.
7+
8+
Example:
9+
10+
1. Create the event listener.
11+
12+
```php
13+
<?php
14+
15+
declare(strict_types=1);
16+
17+
namespace App\EventListener;
18+
19+
use League\OAuth2\Server\RequestAccessTokenEvent;
20+
21+
final class FooListener
22+
{
23+
public function onAccessTokenIssuedEvent(RequestAccessTokenEvent $event): void
24+
{
25+
// do something
26+
}
27+
}
28+
```
29+
30+
1. Register the event listener:
31+
32+
```yaml
33+
services:
34+
App\EventListener\FooListener:
35+
tags:
36+
- { name: kernel.event_listener, event: access_token.issued, method: onAccessTokenIssuedEvent }
37+
```

src/Resources/config/services.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
use League\Bundle\OAuth2ServerBundle\Repository\UserRepository;
3737
use League\Bundle\OAuth2ServerBundle\Security\Authenticator\OAuth2Authenticator;
3838
use League\Bundle\OAuth2ServerBundle\Security\EventListener\CheckScopeListener;
39+
use League\Bundle\OAuth2ServerBundle\Service\SymfonyLeagueEventListenerProvider;
40+
use League\Event\Emitter;
3941
use League\OAuth2\Server\AuthorizationServer;
4042
use League\OAuth2\Server\Grant\AuthCodeGrant;
4143
use League\OAuth2\Server\Grant\ClientCredentialsGrant;
@@ -130,6 +132,15 @@
130132
->tag('kernel.event_subscriber')
131133
->alias(CheckScopeListener::class, 'league.oauth2_server.listener.check_scope')
132134

135+
->set('league.oauth2_server.symfony_league_listener_provider', SymfonyLeagueEventListenerProvider::class)
136+
->args([
137+
service('event_dispatcher'),
138+
])
139+
->alias(SymfonyLeagueEventListenerProvider::class, 'league.oauth2_server.symfony_league_listener_provider')
140+
141+
->set('league.oauth2_server.emitter', Emitter::class)
142+
->call('useListenerProvider', [service('league.oauth2_server.symfony_league_listener_provider')])
143+
133144
->set('league.oauth2_server.authorization_server.grant_configurator', GrantConfigurator::class)
134145
->args([
135146
tagged_iterator('league.oauth2_server.authorization_server.grant'),
@@ -145,6 +156,7 @@
145156
null,
146157
null,
147158
])
159+
->call('setEmitter', [service('league.oauth2_server.emitter')])
148160
->configurator(service(GrantConfigurator::class))
149161
->alias(AuthorizationServer::class, 'league.oauth2_server.authorization_server')
150162

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace League\Bundle\OAuth2ServerBundle\Service;
6+
7+
use League\Event\EventInterface;
8+
use League\Event\ListenerAcceptorInterface;
9+
use League\Event\ListenerProviderInterface;
10+
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
11+
12+
final class SymfonyLeagueEventListenerProvider implements ListenerProviderInterface
13+
{
14+
/**
15+
* @var EventDispatcherInterface
16+
*/
17+
private $eventDispatcher;
18+
19+
public function __construct(EventDispatcherInterface $eventDispatcher)
20+
{
21+
$this->eventDispatcher = $eventDispatcher;
22+
}
23+
24+
public function provideListeners(ListenerAcceptorInterface $listenerAcceptor)
25+
{
26+
$listener = \Closure::fromCallable([$this, 'dispatchLeagueEventWithSymfonyEventDispatcher']);
27+
28+
$listenerAcceptor->addListener('*', $listener);
29+
30+
return $this;
31+
}
32+
33+
private function dispatchLeagueEventWithSymfonyEventDispatcher(EventInterface $event): void
34+
{
35+
$this->eventDispatcher->dispatch($event, $event->getName());
36+
}
37+
}

tests/Acceptance/TokenEndpointTest.php

Lines changed: 131 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
use League\Bundle\OAuth2ServerBundle\OAuth2Events;
1515
use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FixtureFactory;
1616
use League\Bundle\OAuth2ServerBundle\Tests\TestHelper;
17+
use League\OAuth2\Server\RequestAccessTokenEvent;
18+
use League\OAuth2\Server\RequestEvent;
19+
use League\OAuth2\Server\RequestRefreshTokenEvent;
1720

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

3336
public function testSuccessfulClientCredentialsRequest(): void
3437
{
38+
$eventDispatcher = $this->client->getContainer()->get('event_dispatcher');
39+
40+
$eventDispatcher->addListener(OAuth2Events::TOKEN_REQUEST_RESOLVE, static function (TokenRequestResolveEvent $event): void {
41+
$event->getResponse()->headers->set('foo', 'bar');
42+
});
43+
44+
$wasRequestAccessTokenEventDispatched = false;
45+
$accessToken = null;
46+
47+
$eventDispatcher->addListener(RequestEvent::ACCESS_TOKEN_ISSUED, static function (RequestAccessTokenEvent $event) use (&$wasRequestAccessTokenEventDispatched, &$accessToken): void {
48+
$wasRequestAccessTokenEventDispatched = true;
49+
$accessToken = $event->getAccessToken();
50+
});
51+
3552
$this->client->request('POST', '/token', [
3653
'client_id' => 'foo',
3754
'client_secret' => 'secret',
3855
'grant_type' => 'client_credentials',
3956
]);
4057

41-
$this->client
42-
->getContainer()
43-
->get('event_dispatcher')
44-
->addListener(OAuth2Events::TOKEN_REQUEST_RESOLVE, static function (TokenRequestResolveEvent $event): void {
45-
$event->getResponse()->headers->set('foo', 'bar');
46-
});
47-
4858
$response = $this->client->getResponse();
4959

5060
$this->assertSame(200, $response->getStatusCode());
@@ -56,24 +66,41 @@ public function testSuccessfulClientCredentialsRequest(): void
5666
$this->assertLessThanOrEqual(3600, $jsonResponse['expires_in']);
5767
$this->assertGreaterThan(0, $jsonResponse['expires_in']);
5868
$this->assertNotEmpty($jsonResponse['access_token']);
59-
$this->assertEmpty($response->headers->get('foo'), 'bar');
69+
$this->assertArrayNotHasKey('refresh_token', $jsonResponse);
70+
$this->assertSame('bar', $response->headers->get('foo'));
71+
72+
$this->assertTrue($wasRequestAccessTokenEventDispatched);
73+
74+
$this->assertSame('foo', $accessToken->getClient()->getIdentifier());
75+
$this->assertNull($accessToken->getUserIdentifier());
6076
}
6177

6278
public function testSuccessfulPasswordRequest(): void
6379
{
64-
$this->client
65-
->getContainer()
66-
->get('event_dispatcher')
67-
->addListener(OAuth2Events::USER_RESOLVE, static function (UserResolveEvent $event): void {
68-
$event->setUser(FixtureFactory::createUser());
69-
});
80+
$eventDispatcher = $this->client->getContainer()->get('event_dispatcher');
7081

71-
$this->client
72-
->getContainer()
73-
->get('event_dispatcher')
74-
->addListener(OAuth2Events::TOKEN_REQUEST_RESOLVE, static function (TokenRequestResolveEvent $event): void {
75-
$event->getResponse()->headers->set('foo', 'bar');
76-
});
82+
$eventDispatcher->addListener(OAuth2Events::USER_RESOLVE, static function (UserResolveEvent $event): void {
83+
$event->setUser(FixtureFactory::createUser());
84+
});
85+
86+
$eventDispatcher->addListener(OAuth2Events::TOKEN_REQUEST_RESOLVE, static function (TokenRequestResolveEvent $event): void {
87+
$event->getResponse()->headers->set('foo', 'bar');
88+
});
89+
90+
$wasRequestAccessTokenEventDispatched = false;
91+
$wasRequestRefreshTokenEventDispatched = false;
92+
$accessToken = null;
93+
$refreshToken = null;
94+
95+
$eventDispatcher->addListener(RequestEvent::ACCESS_TOKEN_ISSUED, static function (RequestAccessTokenEvent $event) use (&$wasRequestAccessTokenEventDispatched, &$accessToken): void {
96+
$wasRequestAccessTokenEventDispatched = true;
97+
$accessToken = $event->getAccessToken();
98+
});
99+
100+
$eventDispatcher->addListener(RequestEvent::REFRESH_TOKEN_ISSUED, static function (RequestRefreshTokenEvent $event) use (&$wasRequestRefreshTokenEventDispatched, &$refreshToken): void {
101+
$wasRequestRefreshTokenEventDispatched = true;
102+
$refreshToken = $event->getRefreshToken();
103+
});
77104

78105
$this->client->request('POST', '/token', [
79106
'client_id' => 'foo',
@@ -96,6 +123,13 @@ public function testSuccessfulPasswordRequest(): void
96123
$this->assertNotEmpty($jsonResponse['access_token']);
97124
$this->assertNotEmpty($jsonResponse['refresh_token']);
98125
$this->assertSame($response->headers->get('foo'), 'bar');
126+
127+
$this->assertTrue($wasRequestAccessTokenEventDispatched);
128+
$this->assertTrue($wasRequestRefreshTokenEventDispatched);
129+
130+
$this->assertSame('foo', $accessToken->getClient()->getIdentifier());
131+
$this->assertSame('user', $accessToken->getUserIdentifier());
132+
$this->assertSame($accessToken->getIdentifier(), $refreshToken->getAccessToken()->getIdentifier());
99133
}
100134

101135
public function testSuccessfulRefreshTokenRequest(): void
@@ -105,24 +139,35 @@ public function testSuccessfulRefreshTokenRequest(): void
105139
->get(RefreshTokenManagerInterface::class)
106140
->find(FixtureFactory::FIXTURE_REFRESH_TOKEN);
107141

108-
$this->client
109-
->getContainer()
110-
->get('event_dispatcher')
111-
->addListener(OAuth2Events::TOKEN_REQUEST_RESOLVE, static function (TokenRequestResolveEvent $event): void {
112-
$event->getResponse()->headers->set('foo', 'bar');
113-
});
142+
$eventDispatcher = $this->client->getContainer()->get('event_dispatcher');
114143

115-
$this->client
116-
->getContainer()
117-
->get('event_dispatcher')
118-
->addListener(OAuth2Events::TOKEN_REQUEST_RESOLVE, static function (TokenRequestResolveEvent $event): void {
119-
if ('bar' === $event->getResponse()->headers->get('foo')) {
120-
$newResponse = clone $event->getResponse();
121-
$newResponse->headers->remove('foo');
122-
$newResponse->headers->set('baz', 'qux');
123-
$event->setResponse($newResponse);
124-
}
125-
}, -1);
144+
$eventDispatcher->addListener(OAuth2Events::TOKEN_REQUEST_RESOLVE, static function (TokenRequestResolveEvent $event): void {
145+
$event->getResponse()->headers->set('foo', 'bar');
146+
});
147+
148+
$eventDispatcher->addListener(OAuth2Events::TOKEN_REQUEST_RESOLVE, static function (TokenRequestResolveEvent $event): void {
149+
if ('bar' === $event->getResponse()->headers->get('foo')) {
150+
$newResponse = clone $event->getResponse();
151+
$newResponse->headers->remove('foo');
152+
$newResponse->headers->set('baz', 'qux');
153+
$event->setResponse($newResponse);
154+
}
155+
}, -1);
156+
157+
$wasRequestAccessTokenEventDispatched = false;
158+
$wasRequestRefreshTokenEventDispatched = false;
159+
$accessToken = null;
160+
$refreshTokenEntity = null;
161+
162+
$eventDispatcher->addListener(RequestEvent::ACCESS_TOKEN_ISSUED, static function (RequestAccessTokenEvent $event) use (&$wasRequestAccessTokenEventDispatched, &$accessToken): void {
163+
$wasRequestAccessTokenEventDispatched = true;
164+
$accessToken = $event->getAccessToken();
165+
});
166+
167+
$eventDispatcher->addListener(RequestEvent::REFRESH_TOKEN_ISSUED, static function (RequestRefreshTokenEvent $event) use (&$wasRequestRefreshTokenEventDispatched, &$refreshTokenEntity): void {
168+
$wasRequestRefreshTokenEventDispatched = true;
169+
$refreshTokenEntity = $event->getRefreshToken();
170+
});
126171

127172
$this->client->request('POST', '/token', [
128173
'client_id' => 'foo',
@@ -145,6 +190,12 @@ public function testSuccessfulRefreshTokenRequest(): void
145190
$this->assertNotEmpty($jsonResponse['refresh_token']);
146191
$this->assertFalse($response->headers->has('foo'));
147192
$this->assertSame($response->headers->get('baz'), 'qux');
193+
194+
$this->assertTrue($wasRequestAccessTokenEventDispatched);
195+
$this->assertTrue($wasRequestRefreshTokenEventDispatched);
196+
197+
$this->assertSame($refreshToken->getAccessToken()->getClient()->getIdentifier(), $accessToken->getClient()->getIdentifier());
198+
$this->assertSame($accessToken->getIdentifier(), $refreshTokenEntity->getAccessToken()->getIdentifier());
148199
}
149200

150201
public function testSuccessfulAuthorizationCodeRequest(): void
@@ -190,12 +241,26 @@ public function testSuccessfulAuthorizationCodeRequestWithPublicClient(): void
190241
->get(AuthorizationCodeManagerInterface::class)
191242
->find(FixtureFactory::FIXTURE_AUTH_CODE_PUBLIC_CLIENT);
192243

193-
$this->client
194-
->getContainer()
195-
->get('event_dispatcher')
196-
->addListener(OAuth2Events::TOKEN_REQUEST_RESOLVE, static function (TokenRequestResolveEvent $event): void {
197-
$event->getResponse()->headers->set('foo', 'bar');
198-
});
244+
$eventDispatcher = $this->client->getContainer()->get('event_dispatcher');
245+
246+
$eventDispatcher->addListener(OAuth2Events::TOKEN_REQUEST_RESOLVE, static function (TokenRequestResolveEvent $event): void {
247+
$event->getResponse()->headers->set('foo', 'bar');
248+
});
249+
250+
$wasRequestAccessTokenEventDispatched = false;
251+
$wasRequestRefreshTokenEventDispatched = false;
252+
$accessToken = null;
253+
$refreshToken = null;
254+
255+
$eventDispatcher->addListener(RequestEvent::ACCESS_TOKEN_ISSUED, static function (RequestAccessTokenEvent $event) use (&$wasRequestAccessTokenEventDispatched, &$accessToken): void {
256+
$wasRequestAccessTokenEventDispatched = true;
257+
$accessToken = $event->getAccessToken();
258+
});
259+
260+
$eventDispatcher->addListener(RequestEvent::REFRESH_TOKEN_ISSUED, static function (RequestRefreshTokenEvent $event) use (&$wasRequestRefreshTokenEventDispatched, &$refreshToken): void {
261+
$wasRequestRefreshTokenEventDispatched = true;
262+
$refreshToken = $event->getRefreshToken();
263+
});
199264

200265
$this->client->request('POST', '/token', [
201266
'client_id' => FixtureFactory::FIXTURE_PUBLIC_CLIENT,
@@ -215,7 +280,15 @@ public function testSuccessfulAuthorizationCodeRequestWithPublicClient(): void
215280
$this->assertLessThanOrEqual(3600, $jsonResponse['expires_in']);
216281
$this->assertGreaterThan(0, $jsonResponse['expires_in']);
217282
$this->assertNotEmpty($jsonResponse['access_token']);
283+
$this->assertNotEmpty($jsonResponse['refresh_token']);
218284
$this->assertSame($response->headers->get('foo'), 'bar');
285+
286+
$this->assertTrue($wasRequestAccessTokenEventDispatched);
287+
$this->assertTrue($wasRequestRefreshTokenEventDispatched);
288+
289+
$this->assertSame($authCode->getClient()->getIdentifier(), $accessToken->getClient()->getIdentifier());
290+
$this->assertSame($authCode->getUserIdentifier(), $accessToken->getUserIdentifier());
291+
$this->assertSame($accessToken->getIdentifier(), $refreshToken->getAccessToken()->getIdentifier());
219292
}
220293

221294
public function testFailedTokenRequest(): void
@@ -236,19 +309,24 @@ public function testFailedTokenRequest(): void
236309

237310
public function testFailedClientCredentialsTokenRequest(): void
238311
{
312+
$eventDispatcher = $this->client->getContainer()->get('event_dispatcher');
313+
314+
$eventDispatcher->addListener(OAuth2Events::TOKEN_REQUEST_RESOLVE, static function (TokenRequestResolveEvent $event): void {
315+
$event->getResponse()->headers->set('foo', 'bar');
316+
});
317+
318+
$wasClientAuthenticationEventDispatched = false;
319+
320+
$eventDispatcher->addListener(RequestEvent::CLIENT_AUTHENTICATION_FAILED, static function (RequestEvent $event) use (&$wasClientAuthenticationEventDispatched, &$accessToken): void {
321+
$wasClientAuthenticationEventDispatched = true;
322+
});
323+
239324
$this->client->request('POST', '/token', [
240325
'client_id' => 'foo',
241326
'client_secret' => 'wrong',
242327
'grant_type' => 'client_credentials',
243328
]);
244329

245-
$this->client
246-
->getContainer()
247-
->get('event_dispatcher')
248-
->addListener(OAuth2Events::TOKEN_REQUEST_RESOLVE, static function (TokenRequestResolveEvent $event): void {
249-
$event->getResponse()->headers->set('foo', 'bar');
250-
});
251-
252330
$response = $this->client->getResponse();
253331

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

259337
$this->assertSame('invalid_client', $jsonResponse['error']);
260338
$this->assertSame('Client authentication failed', $jsonResponse['message']);
261-
$this->assertEmpty($response->headers->get('foo'), 'bar');
339+
$this->assertSame('bar', $response->headers->get('foo'));
340+
341+
$this->assertTrue($wasClientAuthenticationEventDispatched);
262342
}
263343
}

0 commit comments

Comments
 (0)