Skip to content

Commit 3718242

Browse files
committed
fix: #225: register for autoconfiguration of League\OAuth2\Server\Grant\GrantTypeInterface
1 parent b44cbb6 commit 3718242

13 files changed

+265
-38
lines changed

docs/implementing-custom-grant-type.md

+38-4
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@
4343
```
4444

4545
1. In order to enable the new grant type in the authorization server you must register the service in the container.
46-
And the service must be tagged with the `league.oauth2_server.authorization_server.grant` tag:
46+
`\League\OAuth2\Server\Grant\GrantTypeInterface` is registered for autoconfiguration
47+
48+
- but you can manually declare the service with the `league.oauth2_server.authorization_server.grant` tag:
4749

4850
```yaml
4951
services:
@@ -53,7 +55,24 @@ And the service must be tagged with the `league.oauth2_server.authorization_serv
5355
- {name: league.oauth2_server.authorization_server.grant}
5456
```
5557

56-
You could define a custom access token TTL for your grant using `accessTokenTTL` tag attribute :
58+
- If you prefer php configuration, you could use `AutoconfigureTag` symfony attribute for the same result :
59+
60+
```php
61+
<?php
62+
...
63+
64+
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
65+
66+
#[AutoconfigureTag(name: 'league.oauth2_server.authorization_server.grant')]
67+
final class FakeGrant extends AbstractGrant implements GrantTypeInterface
68+
{
69+
...
70+
}
71+
```
72+
73+
1. In order to defined access token TTL for your custom grant, you could:
74+
75+
- define a custom access token TTL for your grant using `accessTokenTTL` tag attribute :
5776

5877
```yaml
5978
services:
@@ -63,7 +82,7 @@ And the service must be tagged with the `league.oauth2_server.authorization_serv
6382
- {name: league.oauth2_server.authorization_server.grant, accessTokenTTL: PT5H}
6483
```
6584

66-
If you prefer php configuration, you could use `AutoconfigureTag` symfony attribute for the same result :
85+
- or via `AutoconfigureTag` :
6786

6887
```php
6988
<?php
@@ -78,7 +97,22 @@ And the service must be tagged with the `league.oauth2_server.authorization_serv
7897
}
7998
```
8099

81-
If `accessTokenTTL` tag attribute is not defined, then bundle config is used `league_oauth2_server.authorization_server.access_token_ttl` (same as `league.oauth2_server.access_token_ttl.default` service container parameter). \
100+
- and last option is usage of `\League\Bundle\OAuth2ServerBundle\Attribute\WithAccessTokenTTL` attribute (note: service must be registered with correct tag before, this is already done if autoconfiguration is activated):
101+
102+
```php
103+
<?php
104+
...
105+
106+
use League\Bundle\OAuth2ServerBundle\Attribute\WithAccessTokenTTL;
107+
108+
#[WithAccessTokenTTL(accessTokenTTL: 'PT5H')]
109+
final class FakeGrant extends AbstractGrant implements GrantTypeInterface
110+
{
111+
...
112+
}
113+
```
114+
115+
For all of these options, if `accessTokenTTL` is not defined, then bundle config is used `league_oauth2_server.authorization_server.access_token_ttl` (same as `league.oauth2_server.access_token_ttl.default` service container parameter). \
82116
`null` is considered as defined, to allow to unset ttl. \
83117
`league_oauth2_server.authorization_server.refresh_token_ttl` is also accessible for your implementation using `league.oauth2_server.refresh_token_ttl.default` service container parameter.
84118

src/Attribute/WithAccessTokenTTL.php

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace League\Bundle\OAuth2ServerBundle\Attribute;
4+
5+
#[\Attribute(\Attribute::TARGET_CLASS)]
6+
class WithAccessTokenTTL
7+
{
8+
// we don't allow to pass directly \DateInterval, to not break symfony container dumping which require serializable object
9+
public function __construct(public readonly string|null $accessTokenTTL)
10+
{
11+
}
12+
}

src/DependencyInjection/CompilerPass/GrantTypePass.php

+31-13
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace League\Bundle\OAuth2ServerBundle\DependencyInjection\CompilerPass;
66

7+
use League\Bundle\OAuth2ServerBundle\Attribute\WithAccessTokenTTL;
78
use League\Bundle\OAuth2ServerBundle\AuthorizationServer\GrantTypeInterface;
89
use League\OAuth2\Server\AuthorizationServer;
910
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
@@ -27,32 +28,49 @@ public function process(ContainerBuilder $container): void
2728

2829
// enable grant type for each
2930
foreach ($taggedServices as $id => $tags) {
30-
// skip of custom grant using \League\Bundle\OAuth2ServerBundle\AuthorizationServer\GrantTypeInterface
31-
// since there are handled by \League\Bundle\OAuth2ServerBundle\AuthorizationServer\GrantConfigurator
32-
// TODO remove code bloc when bundle interface and configurator will be deleted
31+
// we only use the first tag
32+
// this allow to override autoregistration of interface to be able to set accessTokenTTL
33+
// since autoregistered tag are defined in last
34+
$attributes = array_shift($tags);
35+
3336
try {
3437
$grantDefinition = $container->findDefinition($id);
3538
/** @var class-string|null $grantClass */
3639
$grantClass = $grantDefinition->getClass();
3740
if (null !== $grantClass) {
3841
$refGrantClass = new \ReflectionClass($grantClass);
42+
43+
// skip of custom grant using \League\Bundle\OAuth2ServerBundle\AuthorizationServer\GrantTypeInterface
44+
// since there are handled by \League\Bundle\OAuth2ServerBundle\AuthorizationServer\GrantConfigurator
45+
// TODO remove code bloc when bundle interface and configurator will be deleted
3946
if ($refGrantClass->implementsInterface(GrantTypeInterface::class)) {
4047
continue;
4148
}
49+
50+
// read of accessTokenTTL from WithAccessTokenTTL attribute
51+
$withAccessTokenTTLAttributes = $refGrantClass->getAttributes(WithAccessTokenTTL::class);
52+
if (count($withAccessTokenTTLAttributes) > 0) {
53+
// we only use first attribute, because WithAccessTokenTTL is not repeatable
54+
/** @var \ReflectionAttribute<WithAccessTokenTTL> $withAccessTokenTTLAttribute */
55+
$withAccessTokenTTLAttributeArguments = array_shift($withAccessTokenTTLAttributes)->getArguments();
56+
$attributes['accessTokenTTL'] = array_shift($withAccessTokenTTLAttributeArguments);
57+
}
4258
}
4359
} catch (\ReflectionException) {
44-
// handling of this service as native one
60+
// handling of this service as native one or without attribute
4561
}
4662

47-
foreach ($tags as $attributes) {
48-
$definition->addMethodCall('enableGrantType', [
49-
new Reference($id),
50-
// use accessTokenTTL tag attribute if exists, otherwise use global bundle config
51-
new Definition(\DateInterval::class, [\array_key_exists('accessTokenTTL', $attributes)
52-
? $attributes['accessTokenTTL']
53-
: $container->getParameter('league.oauth2_server.access_token_ttl.default')]),
54-
]);
55-
}
63+
// use WithAccessTokenTTL value then accessTokenTTL tag attribute if exists, otherwise use global bundle config
64+
$accessTokenTTLValue = \array_key_exists('accessTokenTTL', $attributes)
65+
? $attributes['accessTokenTTL']
66+
: $container->getParameter('league.oauth2_server.access_token_ttl.default');
67+
68+
$definition->addMethodCall('enableGrantType', [
69+
new Reference($id),
70+
(is_string($accessTokenTTLValue))
71+
? new Definition(\DateInterval::class, [$accessTokenTTLValue])
72+
: $accessTokenTTLValue,
73+
]);
5674
}
5775
}
5876
}

src/DependencyInjection/LeagueOAuth2ServerExtension.php

+4
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
use League\OAuth2\Server\CryptKey;
3232
use League\OAuth2\Server\Grant\AuthCodeGrant;
3333
use League\OAuth2\Server\Grant\ClientCredentialsGrant;
34+
use League\OAuth2\Server\Grant\GrantTypeInterface as LeagueGrantTypeInterface;
3435
use League\OAuth2\Server\Grant\ImplicitGrant;
3536
use League\OAuth2\Server\Grant\PasswordGrant;
3637
use League\OAuth2\Server\Grant\RefreshTokenGrant;
@@ -69,6 +70,9 @@ public function load(array $configs, ContainerBuilder $container)
6970
$container->findDefinition(OAuth2Authenticator::class)
7071
->setArgument(3, $config['role_prefix']);
7172

73+
$container->registerForAutoconfiguration(LeagueGrantTypeInterface::class)
74+
->addTag('league.oauth2_server.authorization_server.grant');
75+
7276
// TODO remove code bloc when bundle interface and configurator will be deleted
7377
$container->registerForAutoconfiguration(GrantTypeInterface::class)
7478
->addTag('league.oauth2_server.authorization_server.grant');

tests/Acceptance/TokenEndpointTest.php

+5-5
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public function testSuccessfulClientCredentialsRequest(): void
6363
$jsonResponse = json_decode($response->getContent(), true);
6464

6565
$this->assertSame('Bearer', $jsonResponse['token_type']);
66-
$this->assertLessThanOrEqual(3600, $jsonResponse['expires_in']);
66+
$this->assertLessThanOrEqual(7200, $jsonResponse['expires_in']);
6767
$this->assertGreaterThan(0, $jsonResponse['expires_in']);
6868
$this->assertNotEmpty($jsonResponse['access_token']);
6969
$this->assertArrayNotHasKey('refresh_token', $jsonResponse);
@@ -118,7 +118,7 @@ public function testSuccessfulPasswordRequest(): void
118118
$jsonResponse = json_decode($response->getContent(), true);
119119

120120
$this->assertSame('Bearer', $jsonResponse['token_type']);
121-
$this->assertLessThanOrEqual(3600, $jsonResponse['expires_in']);
121+
$this->assertLessThanOrEqual(7200, $jsonResponse['expires_in']);
122122
$this->assertGreaterThan(0, $jsonResponse['expires_in']);
123123
$this->assertNotEmpty($jsonResponse['access_token']);
124124
$this->assertNotEmpty($jsonResponse['refresh_token']);
@@ -184,7 +184,7 @@ public function testSuccessfulRefreshTokenRequest(): void
184184
$jsonResponse = json_decode($response->getContent(), true);
185185

186186
$this->assertSame('Bearer', $jsonResponse['token_type']);
187-
$this->assertLessThanOrEqual(3600, $jsonResponse['expires_in']);
187+
$this->assertLessThanOrEqual(7200, $jsonResponse['expires_in']);
188188
$this->assertGreaterThan(0, $jsonResponse['expires_in']);
189189
$this->assertNotEmpty($jsonResponse['access_token']);
190190
$this->assertNotEmpty($jsonResponse['refresh_token']);
@@ -228,7 +228,7 @@ public function testSuccessfulAuthorizationCodeRequest(): void
228228
$jsonResponse = json_decode($response->getContent(), true);
229229

230230
$this->assertSame('Bearer', $jsonResponse['token_type']);
231-
$this->assertLessThanOrEqual(3600, $jsonResponse['expires_in']);
231+
$this->assertLessThanOrEqual(7200, $jsonResponse['expires_in']);
232232
$this->assertGreaterThan(0, $jsonResponse['expires_in']);
233233
$this->assertNotEmpty($jsonResponse['access_token']);
234234
$this->assertEmpty($response->headers->get('foo'), 'bar');
@@ -277,7 +277,7 @@ public function testSuccessfulAuthorizationCodeRequestWithPublicClient(): void
277277
$jsonResponse = json_decode($response->getContent(), true);
278278

279279
$this->assertSame('Bearer', $jsonResponse['token_type']);
280-
$this->assertLessThanOrEqual(3600, $jsonResponse['expires_in']);
280+
$this->assertLessThanOrEqual(7200, $jsonResponse['expires_in']);
281281
$this->assertGreaterThan(0, $jsonResponse['expires_in']);
282282
$this->assertNotEmpty($jsonResponse['access_token']);
283283
$this->assertNotEmpty($jsonResponse['refresh_token']);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace League\Bundle\OAuth2ServerBundle\Tests\Fixtures;
6+
7+
use League\OAuth2\Server\Grant\AbstractGrant;
8+
use League\OAuth2\Server\Grant\GrantTypeInterface;
9+
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
10+
use Nyholm\Psr7\Response;
11+
use Psr\Http\Message\ServerRequestInterface;
12+
13+
final class FakeGrantNullAccessTokenTTL extends AbstractGrant implements GrantTypeInterface
14+
{
15+
public function getIdentifier(): string
16+
{
17+
return self::class;
18+
}
19+
20+
public function respondToAccessTokenRequest(ServerRequestInterface $request, ResponseTypeInterface $responseType, \DateInterval $accessTokenTTL): ResponseTypeInterface
21+
{
22+
return new Response();
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace League\Bundle\OAuth2ServerBundle\Tests\Fixtures;
6+
7+
use League\Bundle\OAuth2ServerBundle\Attribute\WithAccessTokenTTL;
8+
use League\OAuth2\Server\Grant\AbstractGrant;
9+
use League\OAuth2\Server\Grant\GrantTypeInterface;
10+
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
11+
use Nyholm\Psr7\Response;
12+
use Psr\Http\Message\ServerRequestInterface;
13+
14+
#[WithAccessTokenTTL(accessTokenTTL: null)]
15+
final class FakeGrantNullAccessTokenTTLWithAttribute extends AbstractGrant implements GrantTypeInterface
16+
{
17+
public function getIdentifier(): string
18+
{
19+
return self::class;
20+
}
21+
22+
public function respondToAccessTokenRequest(ServerRequestInterface $request, ResponseTypeInterface $responseType, \DateInterval $accessTokenTTL): ResponseTypeInterface
23+
{
24+
return new Response();
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace League\Bundle\OAuth2ServerBundle\Tests\Fixtures;
6+
7+
use League\OAuth2\Server\Grant\AbstractGrant;
8+
use League\OAuth2\Server\Grant\GrantTypeInterface;
9+
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
10+
use Nyholm\Psr7\Response;
11+
use Psr\Http\Message\ServerRequestInterface;
12+
13+
final class FakeGrantUndefinedAccessTokenTTL extends AbstractGrant implements GrantTypeInterface
14+
{
15+
public function getIdentifier(): string
16+
{
17+
return self::class;
18+
}
19+
20+
public function respondToAccessTokenRequest(ServerRequestInterface $request, ResponseTypeInterface $responseType, \DateInterval $accessTokenTTL): ResponseTypeInterface
21+
{
22+
return new Response();
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace League\Bundle\OAuth2ServerBundle\Tests\Fixtures;
6+
7+
use League\OAuth2\Server\Grant\AbstractGrant;
8+
use League\OAuth2\Server\Grant\GrantTypeInterface;
9+
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
10+
use Nyholm\Psr7\Response;
11+
use Psr\Http\Message\ServerRequestInterface;
12+
13+
final class FakeGrantUndefinedAccessTokenTTLOnlyAutoconfigured extends AbstractGrant implements GrantTypeInterface
14+
{
15+
public function getIdentifier(): string
16+
{
17+
return self::class;
18+
}
19+
20+
public function respondToAccessTokenRequest(ServerRequestInterface $request, ResponseTypeInterface $responseType, \DateInterval $accessTokenTTL): ResponseTypeInterface
21+
{
22+
return new Response();
23+
}
24+
}
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace League\Bundle\OAuth2ServerBundle\Tests\Fixtures;
6+
7+
use League\Bundle\OAuth2ServerBundle\Attribute\WithAccessTokenTTL;
8+
use League\OAuth2\Server\Grant\AbstractGrant;
9+
use League\OAuth2\Server\Grant\GrantTypeInterface;
10+
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
11+
use Nyholm\Psr7\Response;
12+
use Psr\Http\Message\ServerRequestInterface;
13+
14+
#[WithAccessTokenTTL(accessTokenTTL: 'PT5H')]
15+
final class FakeGrantWithAttribute extends AbstractGrant implements GrantTypeInterface
16+
{
17+
public function getIdentifier(): string
18+
{
19+
return self::class;
20+
}
21+
22+
public function respondToAccessTokenRequest(ServerRequestInterface $request, ResponseTypeInterface $responseType, \DateInterval $accessTokenTTL): ResponseTypeInterface
23+
{
24+
return new Response();
25+
}
26+
}

tests/Integration/AuthorizationServerCustomGrantTest.php

+23-7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
namespace League\Bundle\OAuth2ServerBundle\Tests\Integration;
66

77
use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FakeGrant;
8+
use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FakeGrantNullAccessTokenTTL;
9+
use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FakeGrantNullAccessTokenTTLWithAttribute;
10+
use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FakeGrantUndefinedAccessTokenTTL;
11+
use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FakeGrantUndefinedAccessTokenTTLOnlyAutoconfigured;
12+
use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FakeGrantWithAttribute;
813
use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FakeLegacyGrant;
914
use League\OAuth2\Server\AuthorizationServer;
1015
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
@@ -28,14 +33,25 @@ public function testAuthorizationServerHasOurCustomGrantEnabled(): void
2833
$enabledGrantTypes = $reflectionProperty->getValue($authorizationServer);
2934
$grantTypeAccessTokenTTL = $reflectionTTLProperty->getValue($authorizationServer);
3035

31-
$this->assertArrayHasKey('fake_grant', $enabledGrantTypes);
32-
$this->assertInstanceOf(FakeGrant::class, $enabledGrantTypes['fake_grant']);
33-
$this->assertArrayHasKey('fake_grant', $grantTypeAccessTokenTTL);
34-
$this->assertEquals(new \DateInterval('PT5H'), $grantTypeAccessTokenTTL['fake_grant']);
36+
$this->assertGrantConfig('fake_grant', new \DateInterval('PT5H'), $enabledGrantTypes, $grantTypeAccessTokenTTL, FakeGrant::class);
37+
$this->assertGrantConfig(FakeGrantNullAccessTokenTTL::class, new \DateInterval('PT1H'), $enabledGrantTypes, $grantTypeAccessTokenTTL);
38+
$this->assertGrantConfig(FakeGrantUndefinedAccessTokenTTL::class, new \DateInterval('PT2H'), $enabledGrantTypes, $grantTypeAccessTokenTTL);
39+
40+
$this->assertGrantConfig(FakeGrantWithAttribute::class, new \DateInterval('PT5H'), $enabledGrantTypes, $grantTypeAccessTokenTTL);
41+
$this->assertGrantConfig(FakeGrantNullAccessTokenTTLWithAttribute::class, new \DateInterval('PT1H'), $enabledGrantTypes, $grantTypeAccessTokenTTL);
42+
$this->assertGrantConfig(FakeGrantUndefinedAccessTokenTTLOnlyAutoconfigured::class, new \DateInterval('PT2H'), $enabledGrantTypes, $grantTypeAccessTokenTTL);
3543

3644
// TODO remove code bloc when bundle interface and configurator will be deleted
37-
$this->assertArrayHasKey('fake_legacy_grant', $enabledGrantTypes);
38-
$this->assertInstanceOf(FakeLegacyGrant::class, $enabledGrantTypes['fake_legacy_grant']);
39-
$this->assertEquals(new \DateInterval('PT5H'), $enabledGrantTypes['fake_legacy_grant']->getAccessTokenTTL());
45+
$this->assertGrantConfig('fake_legacy_grant', new \DateInterval('PT5H'), $enabledGrantTypes, $grantTypeAccessTokenTTL, FakeLegacyGrant::class);
46+
}
47+
48+
private function assertGrantConfig(string $grantId, \DateInterval|null $accessTokenTTL, array $enabledGrantTypes, array $grantTypeAccessTokenTTL, string|null $grantClass = null): void
49+
{
50+
$grantClass ??= $grantId;
51+
52+
$this->assertArrayHasKey($grantId, $enabledGrantTypes);
53+
$this->assertInstanceOf($grantClass, $enabledGrantTypes[$grantId]);
54+
$this->assertArrayHasKey($grantId, $grantTypeAccessTokenTTL);
55+
$this->assertEquals($accessTokenTTL, $grantTypeAccessTokenTTL[$grantId]);
4056
}
4157
}

0 commit comments

Comments
 (0)