diff --git a/config/services.php b/config/services.php index be416c4f..c3888188 100644 --- a/config/services.php +++ b/config/services.php @@ -133,6 +133,7 @@ ->set('league.oauth2_server.emitter', EventEmitter::class) ->call('subscribeListenersFrom', [service('league.oauth2_server.symfony_league_listener_provider')]) + // TODO remove code bloc when bundle interface and configurator will be deleted ->set('league.oauth2_server.authorization_server.grant_configurator', GrantConfigurator::class) ->args([ tagged_iterator('league.oauth2_server.authorization_server.grant'), @@ -150,6 +151,7 @@ null, ]) ->call('setEmitter', [service('league.oauth2_server.emitter')]) + // TODO remove next line when bundle interface and configurator will be deleted ->configurator(service(GrantConfigurator::class)) ->alias(AuthorizationServer::class, 'league.oauth2_server.authorization_server') diff --git a/docs/implementing-custom-grant-type.md b/docs/implementing-custom-grant-type.md index e890137b..df30e896 100644 --- a/docs/implementing-custom-grant-type.md +++ b/docs/implementing-custom-grant-type.md @@ -1,5 +1,90 @@ # Implementing custom grant type +1. Create a class that implements the `League\OAuth2\Server\Grant\GrantTypeInterface` interface. + + Example: + + ```php + foo = $foo; + } + + public function getIdentifier() + { + return 'fake_grant'; + } + + public function respondToAccessTokenRequest(ServerRequestInterface $request, ResponseTypeInterface $responseType, DateInterval $accessTokenTTL) + { + return new Response(); + } + } + ``` + +1. In order to enable the new grant type in the authorization server you must register the service in the container. +And the service must be tagged with the `league.oauth2_server.authorization_server.grant` tag: + + ```yaml + services: + + App\Grant\FakeGrant: + tags: + - {name: league.oauth2_server.authorization_server.grant} + ``` + + You could define a custom access token TTL for your grant using `accessTokenTTL` tag attribute : + + ```yaml + services: + + App\Grant\FakeGrant: + tags: + - {name: league.oauth2_server.authorization_server.grant, accessTokenTTL: PT5H} + ``` + + If you prefer php configuration, you could use `AutoconfigureTag` symfony attribute for the same result : + + ```php + grants as $grant) { - $authorizationServer->enableGrantType($grant, $grant->getAccessTokenTTL()); + if ($grant instanceof GrantTypeInterface) { + $authorizationServer->enableGrantType($grant, $grant->getAccessTokenTTL()); + } } } } diff --git a/src/AuthorizationServer/GrantTypeInterface.php b/src/AuthorizationServer/GrantTypeInterface.php index 93df05ae..8e181644 100644 --- a/src/AuthorizationServer/GrantTypeInterface.php +++ b/src/AuthorizationServer/GrantTypeInterface.php @@ -6,6 +6,9 @@ use League\OAuth2\Server\Grant\GrantTypeInterface as LeagueGrantTypeInterface; +/** + * @deprecated use League\OAuth2\Server\Grant\GrantTypeInterface with accessTokenTTL tag attribute instead + */ interface GrantTypeInterface extends LeagueGrantTypeInterface { public function getAccessTokenTTL(): ?\DateInterval; diff --git a/src/DependencyInjection/CompilerPass/GrantTypePass.php b/src/DependencyInjection/CompilerPass/GrantTypePass.php new file mode 100644 index 00000000..77cff478 --- /dev/null +++ b/src/DependencyInjection/CompilerPass/GrantTypePass.php @@ -0,0 +1,62 @@ +has(AuthorizationServer::class)) { + return; + } + + $definition = $container->findDefinition(AuthorizationServer::class); + + // find all service IDs with the league.oauth2_server.authorization_server.grant tag + $taggedServices = $container->findTaggedServiceIds('league.oauth2_server.authorization_server.grant'); + + // enable grant type for each + foreach ($taggedServices as $id => $tags) { + // skip of custom grant using \League\Bundle\OAuth2ServerBundle\AuthorizationServer\GrantTypeInterface + // since there are handled by \League\Bundle\OAuth2ServerBundle\AuthorizationServer\GrantConfigurator + // TODO remove code bloc when bundle interface and configurator will be deleted + try { + $grantDefinition = $container->findDefinition($id); + /** @var class-string|null $grantClass */ + $grantClass = $grantDefinition->getClass(); + if (null !== $grantClass) { + $refGrantClass = new \ReflectionClass($grantClass); + if ($refGrantClass->implementsInterface(GrantTypeInterface::class)) { + continue; + } + } + } catch (\ReflectionException) { + // handling of this service as native one + } + + foreach ($tags as $attributes) { + // use accessTokenTTL tag attribute if exists, otherwise use global bundle config + $accessTokenTTLValue = \array_key_exists('accessTokenTTL', $attributes) + ? $attributes['accessTokenTTL'] + : $container->getParameter('league.oauth2_server.access_token_ttl.default'); + + $definition->addMethodCall('enableGrantType', [ + new Reference($id), + (\is_string($accessTokenTTLValue)) + ? new Definition(\DateInterval::class, [$accessTokenTTLValue]) + : $accessTokenTTLValue, + ]); + } + } + } +} diff --git a/src/DependencyInjection/LeagueOAuth2ServerExtension.php b/src/DependencyInjection/LeagueOAuth2ServerExtension.php index edcb8f1a..b1427d99 100644 --- a/src/DependencyInjection/LeagueOAuth2ServerExtension.php +++ b/src/DependencyInjection/LeagueOAuth2ServerExtension.php @@ -69,6 +69,7 @@ public function load(array $configs, ContainerBuilder $container) $container->findDefinition(OAuth2Authenticator::class) ->setArgument(3, $config['role_prefix']); + // TODO remove code bloc when bundle interface and configurator will be deleted $container->registerForAutoconfiguration(GrantTypeInterface::class) ->addTag('league.oauth2_server.authorization_server.grant'); @@ -139,6 +140,7 @@ private function configureAuthorizationServer(ContainerBuilder $container, array { $container->setParameter('league.oauth2_server.encryption_key', $config['encryption_key']); $container->setParameter('league.oauth2_server.encryption_key.type', $config['encryption_key_type']); + $container->setParameter('league.oauth2_server.access_token_ttl.default', $config['access_token_ttl']); $authorizationServer = $container ->findDefinition(AuthorizationServer::class) @@ -199,6 +201,8 @@ private function configureAuthorizationServer(ContainerBuilder $container, array */ private function configureGrants(ContainerBuilder $container, array $config): void { + $container->setParameter('league.oauth2_server.refresh_token_ttl.default', $config['refresh_token_ttl']); + $container ->findDefinition(PasswordGrant::class) ->addMethodCall('setRefreshTokenTTL', [ diff --git a/src/LeagueOAuth2ServerBundle.php b/src/LeagueOAuth2ServerBundle.php index 0478860b..3146e2cf 100644 --- a/src/LeagueOAuth2ServerBundle.php +++ b/src/LeagueOAuth2ServerBundle.php @@ -6,6 +6,7 @@ use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\DoctrineOrmMappingsPass; use League\Bundle\OAuth2ServerBundle\DependencyInjection\CompilerPass\EncryptionKeyPass; +use League\Bundle\OAuth2ServerBundle\DependencyInjection\CompilerPass\GrantTypePass; use League\Bundle\OAuth2ServerBundle\DependencyInjection\LeagueOAuth2ServerExtension; use League\Bundle\OAuth2ServerBundle\DependencyInjection\Security\OAuth2Factory; use League\Bundle\OAuth2ServerBundle\Persistence\Mapping\Driver; @@ -26,6 +27,8 @@ public function build(ContainerBuilder $container) $this->configureDoctrineExtension($container); $this->configureSecurityExtension($container); + + $container->addCompilerPass(new GrantTypePass()); } public function getContainerExtension(): ExtensionInterface diff --git a/tests/Acceptance/TokenEndpointTest.php b/tests/Acceptance/TokenEndpointTest.php index 5f586714..64d5f7cf 100644 --- a/tests/Acceptance/TokenEndpointTest.php +++ b/tests/Acceptance/TokenEndpointTest.php @@ -63,7 +63,7 @@ public function testSuccessfulClientCredentialsRequest(): void $jsonResponse = json_decode($response->getContent(), true); $this->assertSame('Bearer', $jsonResponse['token_type']); - $this->assertLessThanOrEqual(3600, $jsonResponse['expires_in']); + $this->assertLessThanOrEqual(7200, $jsonResponse['expires_in']); $this->assertGreaterThan(0, $jsonResponse['expires_in']); $this->assertNotEmpty($jsonResponse['access_token']); $this->assertArrayNotHasKey('refresh_token', $jsonResponse); @@ -118,7 +118,7 @@ public function testSuccessfulPasswordRequest(): void $jsonResponse = json_decode($response->getContent(), true); $this->assertSame('Bearer', $jsonResponse['token_type']); - $this->assertLessThanOrEqual(3600, $jsonResponse['expires_in']); + $this->assertLessThanOrEqual(7200, $jsonResponse['expires_in']); $this->assertGreaterThan(0, $jsonResponse['expires_in']); $this->assertNotEmpty($jsonResponse['access_token']); $this->assertNotEmpty($jsonResponse['refresh_token']); @@ -184,7 +184,7 @@ public function testSuccessfulRefreshTokenRequest(): void $jsonResponse = json_decode($response->getContent(), true); $this->assertSame('Bearer', $jsonResponse['token_type']); - $this->assertLessThanOrEqual(3600, $jsonResponse['expires_in']); + $this->assertLessThanOrEqual(7200, $jsonResponse['expires_in']); $this->assertGreaterThan(0, $jsonResponse['expires_in']); $this->assertNotEmpty($jsonResponse['access_token']); $this->assertNotEmpty($jsonResponse['refresh_token']); @@ -228,7 +228,7 @@ public function testSuccessfulAuthorizationCodeRequest(): void $jsonResponse = json_decode($response->getContent(), true); $this->assertSame('Bearer', $jsonResponse['token_type']); - $this->assertLessThanOrEqual(3600, $jsonResponse['expires_in']); + $this->assertLessThanOrEqual(7200, $jsonResponse['expires_in']); $this->assertGreaterThan(0, $jsonResponse['expires_in']); $this->assertNotEmpty($jsonResponse['access_token']); $this->assertEmpty($response->headers->get('foo'), 'bar'); @@ -277,7 +277,7 @@ public function testSuccessfulAuthorizationCodeRequestWithPublicClient(): void $jsonResponse = json_decode($response->getContent(), true); $this->assertSame('Bearer', $jsonResponse['token_type']); - $this->assertLessThanOrEqual(3600, $jsonResponse['expires_in']); + $this->assertLessThanOrEqual(7200, $jsonResponse['expires_in']); $this->assertGreaterThan(0, $jsonResponse['expires_in']); $this->assertNotEmpty($jsonResponse['access_token']); $this->assertNotEmpty($jsonResponse['refresh_token']); diff --git a/tests/Fixtures/FakeGrant.php b/tests/Fixtures/FakeGrant.php index 04216f4a..1efe71a0 100644 --- a/tests/Fixtures/FakeGrant.php +++ b/tests/Fixtures/FakeGrant.php @@ -4,8 +4,8 @@ namespace League\Bundle\OAuth2ServerBundle\Tests\Fixtures; -use League\Bundle\OAuth2ServerBundle\AuthorizationServer\GrantTypeInterface; use League\OAuth2\Server\Grant\AbstractGrant; +use League\OAuth2\Server\Grant\GrantTypeInterface; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use Nyholm\Psr7\Response; use Psr\Http\Message\ServerRequestInterface; @@ -21,9 +21,4 @@ public function respondToAccessTokenRequest(ServerRequestInterface $request, Res { return new Response(); } - - public function getAccessTokenTTL(): ?\DateInterval - { - return new \DateInterval('PT5H'); - } } diff --git a/tests/Fixtures/FakeGrantNullAccessTokenTTL.php b/tests/Fixtures/FakeGrantNullAccessTokenTTL.php new file mode 100644 index 00000000..24fc45e0 --- /dev/null +++ b/tests/Fixtures/FakeGrantNullAccessTokenTTL.php @@ -0,0 +1,24 @@ +getProperty('enabledGrantTypes'); $reflectionProperty->setAccessible(true); + $reflectionTTLProperty = $reflectionClass->getProperty('grantTypeAccessTokenTTL'); + $reflectionTTLProperty->setAccessible(true); + $enabledGrantTypes = $reflectionProperty->getValue($authorizationServer); + $grantTypeAccessTokenTTL = $reflectionTTLProperty->getValue($authorizationServer); + + $this->assertGrantConfig('fake_grant', new \DateInterval('PT3H'), $enabledGrantTypes, $grantTypeAccessTokenTTL, FakeGrant::class); + $this->assertGrantConfig(FakeGrantNullAccessTokenTTL::class, new \DateInterval('PT1H'), $enabledGrantTypes, $grantTypeAccessTokenTTL); + $this->assertGrantConfig(FakeGrantUndefinedAccessTokenTTL::class, new \DateInterval('PT2H'), $enabledGrantTypes, $grantTypeAccessTokenTTL); + + // TODO remove code bloc when bundle interface and configurator will be deleted + $this->assertGrantConfig('fake_legacy_grant', new \DateInterval('PT5H'), $enabledGrantTypes, $grantTypeAccessTokenTTL, FakeLegacyGrant::class); + } + + private function assertGrantConfig(string $grantId, ?\DateInterval $accessTokenTTL, array $enabledGrantTypes, array $grantTypeAccessTokenTTL, ?string $grantClass = null): void + { + $grantClass ??= $grantId; - $this->assertArrayHasKey('fake_grant', $enabledGrantTypes); - $this->assertInstanceOf(FakeGrant::class, $enabledGrantTypes['fake_grant']); - $this->assertEquals(new \DateInterval('PT5H'), $enabledGrantTypes['fake_grant']->getAccessTokenTTL()); + $this->assertArrayHasKey($grantId, $enabledGrantTypes); + $this->assertInstanceOf($grantClass, $enabledGrantTypes[$grantId]); + $this->assertArrayHasKey($grantId, $grantTypeAccessTokenTTL); + $this->assertEquals($accessTokenTTL, $grantTypeAccessTokenTTL[$grantId]); } } diff --git a/tests/Integration/AuthorizationServerTest.php b/tests/Integration/AuthorizationServerTest.php index 88bae9be..e4ab57a4 100644 --- a/tests/Integration/AuthorizationServerTest.php +++ b/tests/Integration/AuthorizationServerTest.php @@ -176,7 +176,7 @@ public function testValidClientCredentialsGrant(): void // Response assertions. $this->assertSame('Bearer', $response['token_type']); - $this->assertLessThanOrEqual(3600, $response['expires_in']); + $this->assertLessThanOrEqual(7200, $response['expires_in']); $this->assertGreaterThan(0, $response['expires_in']); $this->assertInstanceOf(AccessToken::class, $accessToken); @@ -197,7 +197,7 @@ public function testValidClientCredentialsGrantWithScope(): void // Response assertions. $this->assertSame('Bearer', $response['token_type']); - $this->assertLessThanOrEqual(3600, $response['expires_in']); + $this->assertLessThanOrEqual(7200, $response['expires_in']); $this->assertGreaterThan(0, $response['expires_in']); $this->assertInstanceOf(AccessToken::class, $accessToken); @@ -224,7 +224,7 @@ public function testValidClientCredentialsGrantWithInheritedScope(): void // Response assertions. $this->assertSame('Bearer', $response['token_type']); - $this->assertLessThanOrEqual(3600, $response['expires_in']); + $this->assertLessThanOrEqual(7200, $response['expires_in']); $this->assertGreaterThan(0, $response['expires_in']); $this->assertInstanceOf(AccessToken::class, $accessToken); @@ -252,7 +252,7 @@ public function testValidClientCredentialsGrantWithRequestedScope(): void // Response assertions. $this->assertSame('Bearer', $response['token_type']); - $this->assertLessThanOrEqual(3600, $response['expires_in']); + $this->assertLessThanOrEqual(7200, $response['expires_in']); $this->assertGreaterThan(0, $response['expires_in']); $this->assertInstanceOf(AccessToken::class, $accessToken); @@ -286,7 +286,7 @@ public function testValidPasswordGrant(): void // Response assertions. $this->assertSame('Bearer', $response['token_type']); - $this->assertLessThanOrEqual(3600, $response['expires_in']); + $this->assertLessThanOrEqual(7200, $response['expires_in']); $this->assertGreaterThan(0, $response['expires_in']); $this->assertInstanceOf(AccessToken::class, $accessToken); $this->assertInstanceOf(RefreshToken::class, $refreshToken); @@ -361,7 +361,7 @@ public function testValidRefreshGrant(): void // Response assertions. $this->assertSame('Bearer', $response['token_type']); - $this->assertLessThanOrEqual(3600, $response['expires_in']); + $this->assertLessThanOrEqual(7200, $response['expires_in']); $this->assertGreaterThan(0, $response['expires_in']); $this->assertInstanceOf(AccessToken::class, $accessToken); $this->assertInstanceOf(RefreshToken::class, $refreshToken); @@ -645,7 +645,7 @@ public function testSuccessfulAuthorizationWithCode(): void $accessToken = $this->getAccessToken($response['access_token']); $this->assertSame('Bearer', $response['token_type']); - $this->assertLessThanOrEqual(3600, $response['expires_in']); + $this->assertLessThanOrEqual(7200, $response['expires_in']); $this->assertGreaterThan(0, $response['expires_in']); $this->assertInstanceOf(AccessToken::class, $accessToken); $this->assertSame('foo', $accessToken->getClient()->getIdentifier()); diff --git a/tests/TestKernel.php b/tests/TestKernel.php index 889a1030..3430e8eb 100644 --- a/tests/TestKernel.php +++ b/tests/TestKernel.php @@ -5,7 +5,6 @@ namespace League\Bundle\OAuth2ServerBundle\Tests; use Doctrine\DBAL\Platforms\SqlitePlatform; -use Doctrine\ORM\Mapping\Annotation; use League\Bundle\OAuth2ServerBundle\Manager\AccessTokenManagerInterface; use League\Bundle\OAuth2ServerBundle\Manager\AuthorizationCodeManagerInterface; use League\Bundle\OAuth2ServerBundle\Manager\ClientManagerInterface; @@ -16,6 +15,9 @@ use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FakeClientManager; use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FakeCredentialsRevoker; use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FakeGrant; +use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FakeGrantNullAccessTokenTTL; +use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FakeGrantUndefinedAccessTokenTTL; +use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FakeLegacyGrant; use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FakeRefreshTokenManager; use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\FixtureFactory; use League\Bundle\OAuth2ServerBundle\Tests\Fixtures\SecurityTestController; @@ -168,6 +170,7 @@ public function registerContainerConfiguration(LoaderInterface $loader): void 'authorization_server' => [ 'private_key' => '%env(PRIVATE_KEY_PATH)%', 'encryption_key' => '%env(ENCRYPTION_KEY)%', + 'access_token_ttl' => 'PT2H', // to have a different value as league/oauth2-server lib ], 'resource_server' => $this->resourceServiceConfig ?? ['public_key' => '%env(PUBLIC_KEY_PATH)%'], 'scopes' => [ @@ -246,7 +249,19 @@ private function configureCustomPersistenceServices(ContainerBuilder $container) private function registerFakeGrant(ContainerBuilder $container): void { - $container->register(FakeGrant::class)->setAutoconfigured(true); + $container->register(FakeGrant::class) + // tagged twice to test this case, last one win + ->addTag('league.oauth2_server.authorization_server.grant', ['accessTokenTTL' => 'PT5H']) + ->addTag('league.oauth2_server.authorization_server.grant', ['accessTokenTTL' => 'PT3H']); + + $container->register(FakeGrantNullAccessTokenTTL::class) + ->addTag('league.oauth2_server.authorization_server.grant', ['accessTokenTTL' => null]); + + $container->register(FakeGrantUndefinedAccessTokenTTL::class) + ->addTag('league.oauth2_server.authorization_server.grant'); + + // TODO remove line when bundle interface and configurator will be deleted + $container->register(FakeLegacyGrant::class)->setAutoconfigured(true); } private function initializeEnvironmentVariables(): void