From a1aefea0fc1f79e320ef25d751c6f71debe57e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berislav=20Balogovi=C4=87?= Date: Sat, 11 Apr 2020 04:15:27 +0200 Subject: [PATCH] Add per grant type configuration options --- DependencyInjection/Configuration.php | 183 +++++++-- .../TrikoderOAuth2Extension.php | 37 +- OAuth2Grants.php | 27 +- README.md | 96 +++-- Tests/Unit/ExtensionTest.php | 352 +++++++++++++++++- 5 files changed, 607 insertions(+), 88 deletions(-) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index ce4cb44b..707b2266 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -8,6 +8,7 @@ use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; +use Trikoder\Bundle\OAuth2Bundle\OAuth2Grants; final class Configuration implements ConfigurationInterface { @@ -27,11 +28,11 @@ public function getConfigTreeBuilder(): TreeBuilder $rootNode ->children() ->scalarNode('exception_event_listener_priority') - ->info('The priority of the event listener that converts an Exception to a Response') + ->info('The priority of the event listener that converts an Exception to a Response.') ->defaultValue(10) ->end() ->scalarNode('role_prefix') - ->info('Set a custom prefix that replaces the default \'ROLE_OAUTH2_\' role prefix') + ->info('Set a custom prefix that replaces the default "ROLE_OAUTH2_" role prefix.') ->defaultValue('ROLE_OAUTH2_') ->cannotBeEmpty() ->end() @@ -55,7 +56,7 @@ private function createAuthorizationServerNode(): NodeDefinition ->cannotBeEmpty() ->end() ->scalarNode('private_key_passphrase') - ->info('Passphrase of the private key, if any') + ->info('Passphrase of the private key, if any.') ->defaultValue(null) ->end() ->scalarNode('encryption_key') @@ -64,48 +65,174 @@ private function createAuthorizationServerNode(): NodeDefinition ->cannotBeEmpty() ->end() ->enumNode('encryption_key_type') - ->info("The type of value of 'encryption_key'") + ->info('The type of value of "encryption_key".') ->values(['plain', 'defuse']) ->defaultValue('plain') ->end() ->scalarNode('access_token_ttl') - ->info("How long the issued access token should be valid for.\nThe value should be a valid interval: http://php.net/manual/en/dateinterval.construct.php#refsect1-dateinterval.construct-parameters") + ->info("How long the issued access token should be valid for, used as a default if there is no grant type specific value set.\nThe value should be a valid interval: http://php.net/manual/en/dateinterval.construct.php#refsect1-dateinterval.construct-parameters") ->cannotBeEmpty() ->defaultValue('PT1H') ->end() ->scalarNode('refresh_token_ttl') - ->info("How long the issued refresh token should be valid for.\nThe value should be a valid interval: http://php.net/manual/en/dateinterval.construct.php#refsect1-dateinterval.construct-parameters") + ->info("How long the issued refresh token should be valid for, used as a default if there is no grant type specific value set.\nThe value should be a valid interval: http://php.net/manual/en/dateinterval.construct.php#refsect1-dateinterval.construct-parameters") ->cannotBeEmpty() ->defaultValue('P1M') ->end() + + // @TODO Remove in v4 start + ->scalarNode('auth_code_ttl') - ->info("How long the issued auth code should be valid for.\nThe value should be a valid interval: http://php.net/manual/en/dateinterval.construct.php#refsect1-dateinterval.construct-parameters") + ->info("How long the issued authorization code should be valid for.\nThe value should be a valid interval: http://php.net/manual/en/dateinterval.construct.php#refsect1-dateinterval.construct-parameters") ->cannotBeEmpty() - ->defaultValue('PT10M') - ->end() - ->booleanNode('enable_client_credentials_grant') - ->info('Whether to enable the client credentials grant') - ->defaultTrue() + ->setDeprecated('"%path%.%node%" is deprecated, use "%path%.grant_types.authorization_code.auth_code_ttl" instead.') + ->beforeNormalization() + ->ifNull() + ->thenUnset() + ->end() ->end() - ->booleanNode('enable_password_grant') - ->info('Whether to enable the password grant') - ->defaultTrue() + ->booleanNode('require_code_challenge_for_public_clients') + ->info('Whether to require code challenge for public clients for the authorization code grant.') + ->setDeprecated('"%path%.%node%" is deprecated, use "%path%.grant_types.authorization_code.require_code_challenge_for_public_clients" instead.') + ->beforeNormalization() + ->ifNull() + ->thenUnset() + ->end() ->end() - ->booleanNode('enable_refresh_token_grant') - ->info('Whether to enable the refresh token grant') - ->defaultTrue() + ->end() + ; + + foreach (OAuth2Grants::ALL as $grantType => $grantTypeName) { + $oldGrantType = 'authorization_code' === $grantType ? 'auth_code' : $grantType; + + $node + ->children() + ->booleanNode(sprintf('enable_%s_grant', $oldGrantType)) + ->info(sprintf('Whether to enable the %s grant.', $grantTypeName)) + ->setDeprecated(sprintf('"%%path%%.%%node%%" is deprecated, use "%%path%%.grant_types.%s.enable" instead.', $grantType)) + ->beforeNormalization() + ->ifNull() + ->thenUnset() + ->end() + ->end() ->end() - ->booleanNode('enable_auth_code_grant') - ->info('Whether to enable the authorization code grant') - ->defaultTrue() + ; + } + + // @TODO Remove in v4 end + + $node->append($this->createAuthorizationServerGrantTypesNode()); + + $node + ->validate() + ->always(static function ($v): array { + $grantTypesWithRefreshToken = array_flip(OAuth2Grants::WITH_REFRESH_TOKEN); + + foreach ($v['grant_types'] as $grantType => &$grantTypeConfig) { + $grantTypeConfig['access_token_ttl'] = $grantTypeConfig['access_token_ttl'] ?? $v['access_token_ttl']; + + if (isset($grantTypesWithRefreshToken[$grantType])) { + $grantTypeConfig['refresh_token_ttl'] = $grantTypeConfig['refresh_token_ttl'] ?? $v['refresh_token_ttl']; + } + + // @TODO Remove in v4 start + $oldGrantType = 'authorization_code' === $grantType ? 'auth_code' : $grantType; + + $grantTypeConfig['enable'] = $v[sprintf('enable_%s_grant', $oldGrantType)] ?? $grantTypeConfig['enable']; + + if ('authorization_code' === $grantType) { + $grantTypeConfig['auth_code_ttl'] = $v['auth_code_ttl'] ?? $grantTypeConfig['auth_code_ttl']; + $grantTypeConfig['require_code_challenge_for_public_clients'] = $v['require_code_challenge_for_public_clients'] + ?? $grantTypeConfig['require_code_challenge_for_public_clients']; + } + // @TODO Remove in v4 end + } + + unset( + $v['access_token_ttl'], + $v['refresh_token_ttl'], + // @TODO Remove in v4 start + $v['enable_auth_code_grant'], + $v['enable_client_credentials_grant'], + $v['enable_implicit_grant'], + $v['enable_password_grant'], + $v['enable_refresh_token_grant'], + $v['auth_code_ttl'], + $v['require_code_challenge_for_public_clients'] + // @TODO Remove in v4 end + ); + + return $v; + }) + ->end() + ; + + return $node; + } + + private function createAuthorizationServerGrantTypesNode(): NodeDefinition + { + $treeBuilder = new TreeBuilder('grant_types'); + $node = $treeBuilder->getRootNode(); + + $node + ->info('Enable and configure grant types.') + ->addDefaultsIfNotSet() + ; + + foreach (OAuth2Grants::ALL as $grantType => $grantTypeName) { + $node + ->children() + ->arrayNode($grantType) + ->addDefaultsIfNotSet() + ->children() + ->booleanNode('enable') + ->info(sprintf('Whether to enable the %s grant.', $grantTypeName)) + ->defaultTrue() + ->end() + ->scalarNode('access_token_ttl') + ->info(sprintf('How long the issued access token should be valid for the %s grant.', $grantTypeName)) + ->cannotBeEmpty() + ->beforeNormalization() + ->ifNull() + ->thenUnset() + ->end() + ->end() + ->end() + ->end() ->end() - ->booleanNode('require_code_challenge_for_public_clients') - ->info('Whether to require code challenge for public clients for the auth code grant') - ->defaultTrue() + ; + } + + foreach (OAuth2Grants::WITH_REFRESH_TOKEN as $grantType) { + $node + ->find($grantType) + ->children() + ->scalarNode('refresh_token_ttl') + ->info(sprintf('How long the issued refresh token should be valid for the %s grant.', OAuth2Grants::ALL[$grantType])) + ->cannotBeEmpty() + ->beforeNormalization() + ->ifNull() + ->thenUnset() + ->end() + ->end() + ->end() ->end() - ->booleanNode('enable_implicit_grant') - ->info('Whether to enable the implicit grant') - ->defaultTrue() + ; + } + + $node + ->find('authorization_code') + ->children() + ->scalarNode('auth_code_ttl') + ->info("How long the issued authorization code should be valid for.\nThe value should be a valid interval: http://php.net/manual/en/dateinterval.construct.php#refsect1-dateinterval.construct-parameters") + ->cannotBeEmpty() + ->defaultValue('PT10M') + ->end() + ->booleanNode('require_code_challenge_for_public_clients') + ->info('Whether to require code challenge for public clients for the authorization code grant.') + ->defaultTrue() + ->end() ->end() ->end() ; @@ -122,7 +249,7 @@ private function createResourceServerNode(): NodeDefinition ->isRequired() ->children() ->scalarNode('public_key') - ->info("Full path to the public key file\nHow to generate a public key: https://oauth2.thephpleague.com/installation/#generating-public-and-private-keys") + ->info("Full path to the public key file.\nHow to generate a public key: https://oauth2.thephpleague.com/installation/#generating-public-and-private-keys") ->example('/var/oauth/public.key') ->isRequired() ->cannotBeEmpty() diff --git a/DependencyInjection/TrikoderOAuth2Extension.php b/DependencyInjection/TrikoderOAuth2Extension.php index 3350867b..ec7a6009 100644 --- a/DependencyInjection/TrikoderOAuth2Extension.php +++ b/DependencyInjection/TrikoderOAuth2Extension.php @@ -150,38 +150,40 @@ private function configureAuthorizationServer(ContainerBuilder $container, array $authorizationServer->replaceArgument('$encryptionKey', new Reference('trikoder.oauth2.defuse_key')); } - if ($config['enable_client_credentials_grant']) { + $grantTypes = $config['grant_types']; + + if ($grantTypes['client_credentials']['enable']) { $authorizationServer->addMethodCall('enableGrantType', [ new Reference(ClientCredentialsGrant::class), - new Definition(DateInterval::class, [$config['access_token_ttl']]), + new Definition(DateInterval::class, [$grantTypes['client_credentials']['access_token_ttl']]), ]); } - if ($config['enable_password_grant']) { + if ($grantTypes['password']['enable']) { $authorizationServer->addMethodCall('enableGrantType', [ new Reference(PasswordGrant::class), - new Definition(DateInterval::class, [$config['access_token_ttl']]), + new Definition(DateInterval::class, [$grantTypes['password']['access_token_ttl']]), ]); } - if ($config['enable_refresh_token_grant']) { + if ($grantTypes['refresh_token']['enable']) { $authorizationServer->addMethodCall('enableGrantType', [ new Reference(RefreshTokenGrant::class), - new Definition(DateInterval::class, [$config['access_token_ttl']]), + new Definition(DateInterval::class, [$grantTypes['refresh_token']['access_token_ttl']]), ]); } - if ($config['enable_auth_code_grant']) { + if ($grantTypes['authorization_code']['enable']) { $authorizationServer->addMethodCall('enableGrantType', [ new Reference(AuthCodeGrant::class), - new Definition(DateInterval::class, [$config['access_token_ttl']]), + new Definition(DateInterval::class, [$grantTypes['authorization_code']['access_token_ttl']]), ]); } - if ($config['enable_implicit_grant']) { + if ($grantTypes['implicit']['enable']) { $authorizationServer->addMethodCall('enableGrantType', [ new Reference(ImplicitGrant::class), - new Definition(DateInterval::class, [$config['access_token_ttl']]), + new Definition(DateInterval::class, [$grantTypes['implicit']['access_token_ttl']]), ]); } @@ -190,34 +192,37 @@ private function configureAuthorizationServer(ContainerBuilder $container, array private function configureGrants(ContainerBuilder $container, array $config): void { + $grantTypes = $config['grant_types']; + $container ->getDefinition(PasswordGrant::class) ->addMethodCall('setRefreshTokenTTL', [ - new Definition(DateInterval::class, [$config['refresh_token_ttl']]), + new Definition(DateInterval::class, [$grantTypes['password']['refresh_token_ttl']]), ]) ; $container ->getDefinition(RefreshTokenGrant::class) ->addMethodCall('setRefreshTokenTTL', [ - new Definition(DateInterval::class, [$config['refresh_token_ttl']]), + new Definition(DateInterval::class, [$grantTypes['refresh_token']['refresh_token_ttl']]), ]) ; $authCodeGrantDefinition = $container->getDefinition(AuthCodeGrant::class); - $authCodeGrantDefinition->replaceArgument('$authCodeTTL', new Definition(DateInterval::class, [$config['auth_code_ttl']])) + $authCodeGrantDefinition + ->replaceArgument('$authCodeTTL', new Definition(DateInterval::class, [$grantTypes['authorization_code']['auth_code_ttl']])) ->addMethodCall('setRefreshTokenTTL', [ - new Definition(DateInterval::class, [$config['refresh_token_ttl']]), + new Definition(DateInterval::class, [$grantTypes['authorization_code']['refresh_token_ttl']]), ]) ; - if (false === $config['require_code_challenge_for_public_clients']) { + if (false === $grantTypes['authorization_code']['require_code_challenge_for_public_clients']) { $authCodeGrantDefinition->addMethodCall('disableRequireCodeChallengeForPublicClients'); } $container ->getDefinition(ImplicitGrant::class) - ->replaceArgument('$accessTokenTTL', new Definition(DateInterval::class, [$config['access_token_ttl']])) + ->replaceArgument('$accessTokenTTL', new Definition(DateInterval::class, [$grantTypes['implicit']['access_token_ttl']])) ; } diff --git a/OAuth2Grants.php b/OAuth2Grants.php index fa48c501..d823f17e 100644 --- a/OAuth2Grants.php +++ b/OAuth2Grants.php @@ -41,14 +41,27 @@ final class OAuth2Grants */ public const REFRESH_TOKEN = 'refresh_token'; + public const WITH_REFRESH_TOKEN = [ + 'authorization_code', + 'password', + 'refresh_token', + ]; + + public const ALL = [ + self::AUTHORIZATION_CODE => 'authorization code', + self::CLIENT_CREDENTIALS => 'client credentials', + self::IMPLICIT => 'implicit', + self::PASSWORD => 'password', + self::REFRESH_TOKEN => 'refresh token', + ]; + + /** + * @deprecated Will be removed in v4, use {@see OAuth2Grants::ALL} instead + * + * @TODO Remove in v4. + */ public static function has(string $grant): bool { - return \in_array($grant, [ - self::CLIENT_CREDENTIALS, - self::PASSWORD, - self::REFRESH_TOKEN, - self::AUTHORIZATION_CODE, - self::IMPLICIT, - ]); + return isset(self::ALL[$grant]); } } diff --git a/README.md b/README.md index d94c055e..160f5767 100644 --- a/README.md +++ b/README.md @@ -50,48 +50,102 @@ This package is currently in the active development. # How to generate a private key: https://oauth2.thephpleague.com/installation/#generating-public-and-private-keys private_key: ~ # Required, Example: /var/oauth/private.key - # Passphrase of the private key, if any + # Passphrase of the private key, if any. private_key_passphrase: null # The plain string or the ascii safe string used to create a Defuse\Crypto\Key to be used as an encryption key. # How to generate an encryption key: https://oauth2.thephpleague.com/installation/#string-password encryption_key: ~ # Required - # The type of value of 'encryption_key' + # The type of value of "encryption_key". encryption_key_type: plain # One of "plain"; "defuse" - # How long the issued access token should be valid for. + # How long the issued access token should be valid for, used as a default if there is no grant type specific value set. # The value should be a valid interval: http://php.net/manual/en/dateinterval.construct.php#refsect1-dateinterval.construct-parameters access_token_ttl: PT1H - # How long the issued refresh token should be valid for. + # How long the issued refresh token should be valid for, used as a default if there is no grant type specific value set. # The value should be a valid interval: http://php.net/manual/en/dateinterval.construct.php#refsect1-dateinterval.construct-parameters refresh_token_ttl: P1M - # How long the issued auth code should be valid for. + # How long the issued authorization code should be valid for. # The value should be a valid interval: http://php.net/manual/en/dateinterval.construct.php#refsect1-dateinterval.construct-parameters - auth_code_ttl: PT10M + auth_code_ttl: ~ # Deprecated ("trikoder_oauth2.authorization_server.auth_code_ttl" is deprecated, use "trikoder_oauth2.authorization_server.grant_types.authorization_code.auth_code_ttl" instead.) - # Whether to enable the client credentials grant - enable_client_credentials_grant: true + # Whether to require code challenge for public clients for the authorization code grant. + require_code_challenge_for_public_clients: ~ # Deprecated ("trikoder_oauth2.authorization_server.require_code_challenge_for_public_clients" is deprecated, use "trikoder_oauth2.authorization_server.grant_types.authorization_code.require_code_challenge_for_public_clients" instead.) - # Whether to enable the password grant - enable_password_grant: true + # Whether to enable the authorization code grant. + enable_auth_code_grant: ~ # Deprecated ("trikoder_oauth2.authorization_server.enable_auth_code_grant" is deprecated, use "trikoder_oauth2.authorization_server.grant_types.authorization_code.enable" instead.) - # Whether to enable the refresh token grant - enable_refresh_token_grant: true + # Whether to enable the client credentials grant. + enable_client_credentials_grant: ~ # Deprecated ("trikoder_oauth2.authorization_server.enable_client_credentials_grant" is deprecated, use "trikoder_oauth2.authorization_server.grant_types.client_credentials.enable" instead.) - # Whether to enable the authorization code grant - enable_auth_code_grant: true + # Whether to enable the implicit grant. + enable_implicit_grant: ~ # Deprecated ("trikoder_oauth2.authorization_server.enable_implicit_grant" is deprecated, use "trikoder_oauth2.authorization_server.grant_types.implicit.enable" instead.) - # Whether to require code challenge for public clients for the auth code grant - require_code_challenge_for_public_clients: true + # Whether to enable the password grant. + enable_password_grant: ~ # Deprecated ("trikoder_oauth2.authorization_server.enable_password_grant" is deprecated, use "trikoder_oauth2.authorization_server.grant_types.password.enable" instead.) - # Whether to enable the implicit grant - enable_implicit_grant: true + # Whether to enable the refresh token grant. + enable_refresh_token_grant: ~ # Deprecated ("trikoder_oauth2.authorization_server.enable_refresh_token_grant" is deprecated, use "trikoder_oauth2.authorization_server.grant_types.refresh_token.enable" instead.) + + # Enable and configure grant types. + grant_types: + authorization_code: + + # Whether to enable the authorization code grant. + enable: true + + # How long the issued access token should be valid for the authorization code grant. + access_token_ttl: ~ + + # How long the issued refresh token should be valid for the authorization code grant. + refresh_token_ttl: ~ + + # How long the issued authorization code should be valid for. + # The value should be a valid interval: http://php.net/manual/en/dateinterval.construct.php#refsect1-dateinterval.construct-parameters + auth_code_ttl: PT10M + + # Whether to require code challenge for public clients for the authorization code grant. + require_code_challenge_for_public_clients: true + client_credentials: + + # Whether to enable the client credentials grant. + enable: true + + # How long the issued access token should be valid for the client credentials grant. + access_token_ttl: ~ + implicit: + + # Whether to enable the implicit grant. + enable: true + + # How long the issued access token should be valid for the implicit grant. + access_token_ttl: ~ + password: + + # Whether to enable the password grant. + enable: true + + # How long the issued access token should be valid for the password grant. + access_token_ttl: ~ + + # How long the issued refresh token should be valid for the password grant. + refresh_token_ttl: ~ + refresh_token: + + # Whether to enable the refresh token grant. + enable: true + + # How long the issued access token should be valid for the refresh token grant. + access_token_ttl: ~ + + # How long the issued refresh token should be valid for the refresh token grant. + refresh_token_ttl: ~ resource_server: # Required - # Full path to the public key file + # Full path to the public key file. # How to generate a public key: https://oauth2.thephpleague.com/installation/#generating-public-and-private-keys public_key: ~ # Required, Example: /var/oauth/public.key @@ -108,10 +162,10 @@ This package is currently in the active development. entity_manager: default in_memory: ~ - # The priority of the event listener that converts an Exception to a Response + # The priority of the event listener that converts an Exception to a Response. exception_event_listener_priority: 10 - # Set a custom prefix that replaces the default 'ROLE_OAUTH2_' role prefix + # Set a custom prefix that replaces the default "ROLE_OAUTH2_" role prefix. role_prefix: ROLE_OAUTH2_ ``` diff --git a/Tests/Unit/ExtensionTest.php b/Tests/Unit/ExtensionTest.php index 65081994..146ae35f 100644 --- a/Tests/Unit/ExtensionTest.php +++ b/Tests/Unit/ExtensionTest.php @@ -7,15 +7,205 @@ use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\Grant\AuthCodeGrant; use League\OAuth2\Server\Grant\ClientCredentialsGrant; +use League\OAuth2\Server\Grant\ImplicitGrant; use League\OAuth2\Server\Grant\PasswordGrant; use League\OAuth2\Server\Grant\RefreshTokenGrant; use PHPUnit\Framework\TestCase; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Trikoder\Bundle\OAuth2Bundle\DependencyInjection\TrikoderOAuth2Extension; use Trikoder\Bundle\OAuth2Bundle\Manager\InMemory\ScopeManager; final class ExtensionTest extends TestCase { + /** + * @dataProvider accessTokenTTLProvider + */ + public function testAccessTokenTTLAndRefreshTokenTTL(array $configTTLs, array $expectedTTLs): void + { + $container = new ContainerBuilder(); + + $this->setupContainer($container); + + $extension = new TrikoderOAuth2Extension(); + + $extension->load($this->getValidConfiguration($configTTLs), $container); + + $authorizationServer = $container->getDefinition(AuthorizationServer::class); + $methodCalls = $authorizationServer->getMethodCalls(); + + foreach ($methodCalls as $methodCall) { + if ('enableGrantType' === $methodCall[0]) { + $referenceId = (string) $methodCall[1][0]; + /** @var Definition $accessTokenTTL */ + $accessTokenTTL = $methodCall[1][1]; + + $this->assertSame( + $expectedTTLs[$referenceId], + $accessTokenTTL->getArgument(0), + sprintf('Call enableGrantType with "%s".', $referenceId) + ); + } + } + + $implicitGrant = $container->getDefinition(ImplicitGrant::class); + + $this->assertSame( + $expectedTTLs[ImplicitGrant::class], + $implicitGrant->getArgument('$accessTokenTTL')->getArgument(0), + sprintf('Pass argument to "%s".', ImplicitGrant::class) + ); + } + + public function accessTokenTTLProvider(): iterable + { + yield 'Default access token TTL can be set' => [[ + 'access_token_ttl' => 'PT3H', + ], [ + AuthCodeGrant::class => 'PT3H', + ClientCredentialsGrant::class => 'PT3H', + ImplicitGrant::class => 'PT3H', + PasswordGrant::class => 'PT3H', + RefreshTokenGrant::class => 'PT3H', + ]]; + + yield 'Default & client credentials grant type access token can be set' => [[ + 'access_token_ttl' => 'PT6H', + 'client_credentials.access_token_ttl' => 'PT4H', + ], [ + AuthCodeGrant::class => 'PT6H', + ClientCredentialsGrant::class => 'PT4H', + ImplicitGrant::class => 'PT6H', + PasswordGrant::class => 'PT6H', + RefreshTokenGrant::class => 'PT6H', + ]]; + + yield 'Password grant type access token can be set' => [[ + 'password.access_token_ttl' => 'PT5H', + ], [ + AuthCodeGrant::class => 'PT1H', + ClientCredentialsGrant::class => 'PT1H', + ImplicitGrant::class => 'PT1H', + PasswordGrant::class => 'PT5H', + RefreshTokenGrant::class => 'PT1H', + ]]; + + yield 'Multiple per grant type access tokens can be set' => [[ + 'access_token_ttl' => 'PT3H', + 'authorization_code.access_token_ttl' => 'PT7H', + 'implicit.access_token_ttl' => 'PT5H', + 'refresh_token.access_token_ttl' => 'PT9H', + ], [ + AuthCodeGrant::class => 'PT7H', + ClientCredentialsGrant::class => 'PT3H', + ImplicitGrant::class => 'PT5H', + PasswordGrant::class => 'PT3H', + RefreshTokenGrant::class => 'PT9H', + ]]; + } + + public function testExceptionIsThrownForEmptyGrantTypeAccessTokenTTL(): void + { + $container = new ContainerBuilder(); + + $this->setupContainer($container); + + $extension = new TrikoderOAuth2Extension(); + + $this->expectException(InvalidConfigurationException::class); + + $extension->load($this->getValidConfiguration([ + 'password.access_token_ttl' => '', + ]), $container); + } + + /** + * @dataProvider refreshTokenTTLProvider + */ + public function testRefreshTokenTTLAndRefreshTokenTTL(array $configTTLs, array $expectedTTLs): void + { + $container = new ContainerBuilder(); + + $this->setupContainer($container); + + $extension = new TrikoderOAuth2Extension(); + + $extension->load($this->getValidConfiguration($configTTLs), $container); + + foreach ($expectedTTLs as $referenceId => $expectedTTL) { + $grant = $container->getDefinition($referenceId); + + $methodCalls = $grant->getMethodCalls(); + + foreach ($methodCalls as $methodCall) { + if ('setRefreshTokenTTL' === $methodCall[0]) { + /** @var Definition $refreshTokenTTL */ + $refreshTokenTTL = $methodCall[1][0]; + + $this->assertSame( + $expectedTTLs[$referenceId], + $refreshTokenTTL->getArgument(0), + sprintf('Call setRefreshTokenTTL with "%s".', $referenceId) + ); + } + } + } + } + + public function refreshTokenTTLProvider(): iterable + { + yield 'Default refresh token TTL can be set' => [[ + 'refresh_token_ttl' => 'P3M', + ], [ + AuthCodeGrant::class => 'P3M', + PasswordGrant::class => 'P3M', + RefreshTokenGrant::class => 'P3M', + ]]; + + yield 'Default & authorization code grant type refresh token can be set' => [[ + 'refresh_token_ttl' => 'P6M', + 'authorization_code.refresh_token_ttl' => 'P4M', + ], [ + AuthCodeGrant::class => 'P4M', + PasswordGrant::class => 'P6M', + RefreshTokenGrant::class => 'P6M', + ]]; + + yield 'Password grant type refresh token can be set' => [[ + 'password.refresh_token_ttl' => 'P5M', + ], [ + AuthCodeGrant::class => 'P1M', + PasswordGrant::class => 'P5M', + RefreshTokenGrant::class => 'P1M', + ]]; + + yield 'Multiple per grant type refresh tokens can be set' => [[ + 'refresh_token_ttl' => 'P3M', + 'authorization_code.refresh_token_ttl' => 'P7M', + 'refresh_token.refresh_token_ttl' => 'P9M', + ], [ + AuthCodeGrant::class => 'P7M', + PasswordGrant::class => 'P3M', + RefreshTokenGrant::class => 'P9M', + ]]; + } + + public function testExceptionIsThrownForEmptyGrantTypeRefreshTokenTTL(): void + { + $container = new ContainerBuilder(); + + $this->setupContainer($container); + + $extension = new TrikoderOAuth2Extension(); + + $this->expectException(InvalidConfigurationException::class); + + $extension->load($this->getValidConfiguration([ + 'password.refresh_token_ttl' => '', + ]), $container); + } + /** * @dataProvider grantsProvider */ @@ -45,22 +235,65 @@ public function testEnablingAndDisablingGrants(string $referenceId, string $gran public function grantsProvider(): iterable { + yield 'Authorization code grant can be enabled' => [ + AuthCodeGrant::class, 'authorization_code.enable', true, + ]; + yield 'Authorization code grant can be disabled' => [ + AuthCodeGrant::class, 'authorization_code.enable', false, + ]; yield 'Client credentials grant can be enabled' => [ - ClientCredentialsGrant::class, 'enable_client_credentials_grant', true, + ClientCredentialsGrant::class, 'client_credentials.enable', true, ]; yield 'Client credentials grant can be disabled' => [ - ClientCredentialsGrant::class, 'enable_client_credentials_grant', false, + ClientCredentialsGrant::class, 'client_credentials.enable', false, + ]; + yield 'Implicit grant can be enabled' => [ + ImplicitGrant::class, 'implicit.enable', true, + ]; + yield 'Implicit grant can be disabled' => [ + ImplicitGrant::class, 'implicit.enable', false, ]; yield 'Password grant can be enabled' => [ - PasswordGrant::class, 'enable_password_grant', true, + PasswordGrant::class, 'password.enable', true, ]; yield 'Password grant can be disabled' => [ - PasswordGrant::class, 'enable_password_grant', false, + PasswordGrant::class, 'password.enable', false, ]; yield 'Refresh token grant can be enabled' => [ - RefreshTokenGrant::class, 'enable_refresh_token_grant', true, + RefreshTokenGrant::class, 'refresh_token.enable', true, ]; yield 'Refresh token grant can be disabled' => [ + RefreshTokenGrant::class, 'refresh_token.enable', false, + ]; + + yield 'Legacy authorization code grant can be enabled' => [ + AuthCodeGrant::class, 'enable_auth_code_grant', true, + ]; + yield 'Legacy authorization code grant can be disabled' => [ + AuthCodeGrant::class, 'enable_auth_code_grant', false, + ]; + yield 'Legacy client credentials grant can be enabled' => [ + ClientCredentialsGrant::class, 'enable_client_credentials_grant', true, + ]; + yield 'Legacy client credentials grant can be disabled' => [ + ClientCredentialsGrant::class, 'enable_client_credentials_grant', false, + ]; + yield 'Legacy implicit grant can be enabled' => [ + ImplicitGrant::class, 'enable_implicit_grant', true, + ]; + yield 'Legacy implicit grant can be disabled' => [ + ImplicitGrant::class, 'enable_implicit_grant', false, + ]; + yield 'Legacy password grant can be enabled' => [ + PasswordGrant::class, 'enable_password_grant', true, + ]; + yield 'Legacy password grant can be disabled' => [ + PasswordGrant::class, 'enable_password_grant', false, + ]; + yield 'Legacy refresh token grant can be enabled' => [ + RefreshTokenGrant::class, 'enable_refresh_token_grant', true, + ]; + yield 'Legacy refresh token grant can be disabled' => [ RefreshTokenGrant::class, 'enable_refresh_token_grant', false, ]; } @@ -69,6 +302,7 @@ public function grantsProvider(): iterable * @dataProvider requireCodeChallengeForPublicClientsProvider */ public function testAuthCodeGrantDisableRequireCodeChallengeForPublicClientsConfig( + string $configKey, ?bool $requireCodeChallengeForPublicClients, bool $shouldTheRequirementBeDisabled ): void { @@ -78,8 +312,9 @@ public function testAuthCodeGrantDisableRequireCodeChallengeForPublicClientsConf $extension = new TrikoderOAuth2Extension(); - $configuration = $this->getValidConfiguration(); - $configuration[0]['authorization_server']['require_code_challenge_for_public_clients'] = $requireCodeChallengeForPublicClients; + $configuration = $this->getValidConfiguration([ + $configKey => $requireCodeChallengeForPublicClients, + ]); $extension->load($configuration, $container); @@ -100,14 +335,66 @@ public function testAuthCodeGrantDisableRequireCodeChallengeForPublicClientsConf public function requireCodeChallengeForPublicClientsProvider(): iterable { - yield 'when not requiring code challenge for public clients the requirement should be disabled' => [ - false, true, + yield 'When not requiring code challenge for public clients the requirement should be disabled' => [ + 'authorization_code.require_code_challenge_for_public_clients', false, true, + ]; + yield 'When code challenge for public clients is required the requirement should not be disabled' => [ + 'authorization_code.require_code_challenge_for_public_clients', true, false, + ]; + yield 'With the default value the requirement should not be disabled' => [ + 'authorization_code.require_code_challenge_for_public_clients', null, false, + ]; + + yield 'Legacy when not requiring code challenge for public clients the requirement should be disabled' => [ + 'require_code_challenge_for_public_clients', false, true, + ]; + yield 'Legacy when code challenge for public clients is required the requirement should not be disabled' => [ + 'require_code_challenge_for_public_clients', true, false, + ]; + yield 'Legacy with the default value the requirement should not be disabled' => [ + 'require_code_challenge_for_public_clients', null, false, + ]; + } + + /** + * @dataProvider authCodeTTLProvider + */ + public function testAuthCodeTTLConfig( + string $configKey, + ?string $authCodeTTL, + string $expectedAuthCodeTTL + ): void { + $container = new ContainerBuilder(); + + $this->setupContainer($container); + + $extension = new TrikoderOAuth2Extension(); + + $configuration = $this->getValidConfiguration([ + $configKey => $authCodeTTL, + ]); + + $extension->load($configuration, $container); + + $authorizationServer = $container->getDefinition(AuthCodeGrant::class); + + $this->assertSame($expectedAuthCodeTTL, $authorizationServer->getArgument('$authCodeTTL')->getArgument(0)); + } + + public function authCodeTTLProvider(): iterable + { + yield 'Authorization code TTL can be set' => [ + 'authorization_code.auth_code_ttl', 'PT20M', 'PT20M', + ]; + yield 'When no authorization code TTL is set, the default is used' => [ + 'authorization_code.auth_code_ttl', null, 'PT10M', ]; - yield 'when code challenge for public clients is required the requirement should not be disabled' => [ - true, false, + + yield 'Legacy authorization code TTL can be set' => [ + 'auth_code_ttl', 'PT20M', 'PT20M', ]; - yield 'with the default value the requirement should not be disabled' => [ - null, false, + yield 'Legacy when no authorization code TTL is set, the default is used' => [ + 'auth_code_ttl', null, 'PT10M', ]; } @@ -118,9 +405,42 @@ private function getValidConfiguration(array $options = []): array 'authorization_server' => [ 'private_key' => 'foo', 'encryption_key' => 'foo', - 'enable_client_credentials_grant' => $options['enable_client_credentials_grant'] ?? true, - 'enable_password_grant' => $options['enable_password_grant'] ?? true, - 'enable_refresh_token_grant' => $options['enable_refresh_token_grant'] ?? true, + 'enable_auth_code_grant' => $options['enable_auth_code_grant'] ?? null, + 'access_token_ttl' => $options['access_token_ttl'] ?? 'PT1H', + 'refresh_token_ttl' => $options['refresh_token_ttl'] ?? 'P1M', + 'enable_client_credentials_grant' => $options['enable_client_credentials_grant'] ?? null, + 'enable_implicit_grant' => $options['enable_implicit_grant'] ?? null, + 'enable_password_grant' => $options['enable_password_grant'] ?? null, + 'enable_refresh_token_grant' => $options['enable_refresh_token_grant'] ?? null, + 'require_code_challenge_for_public_clients' => $options['require_code_challenge_for_public_clients'] ?? null, + 'auth_code_ttl' => $options['auth_code_ttl'] ?? null, + 'grant_types' => [ + 'authorization_code' => [ + 'enable' => $options['authorization_code.enable'] ?? true, + 'access_token_ttl' => $options['authorization_code.access_token_ttl'] ?? null, + 'refresh_token_ttl' => $options['authorization_code.refresh_token_ttl'] ?? null, + 'auth_code_ttl' => $options['authorization_code.auth_code_ttl'] ?? 'PT10M', + 'require_code_challenge_for_public_clients' => $options['authorization_code.require_code_challenge_for_public_clients'] ?? true, + ], + 'client_credentials' => [ + 'enable' => $options['client_credentials.enable'] ?? true, + 'access_token_ttl' => $options['client_credentials.access_token_ttl'] ?? null, + ], + 'implicit' => [ + 'enable' => $options['implicit.enable'] ?? true, + 'access_token_ttl' => $options['implicit.access_token_ttl'] ?? null, + ], + 'password' => [ + 'enable' => $options['password.enable'] ?? true, + 'access_token_ttl' => $options['password.access_token_ttl'] ?? null, + 'refresh_token_ttl' => $options['password.refresh_token_ttl'] ?? null, + ], + 'refresh_token' => [ + 'enable' => $options['refresh_token.enable'] ?? true, + 'access_token_ttl' => $options['refresh_token.access_token_ttl'] ?? null, + 'refresh_token_ttl' => $options['refresh_token.refresh_token_ttl'] ?? null, + ], + ], ], 'resource_server' => [ 'public_key' => 'foo',