Repository: trikoder/oauth2-bundle (php)

Issue:
change access_token_ttl only for client_credentials
Is it possible to change the access_token_ttl only for client_credentials? I wanted to have different expirations for client_credentials and for password.

Tks

Pull request:
Add per grant type configuration options
Resolves #105.

Patch:
diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php
index ce4cb44..707b226 100644
--- a/DependencyInjection/Configuration.php
+++ b/DependencyInjection/Configuration.php
@@ -8,6 +8,7 @@ use Defuse\Crypto\Key;
 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 @@ final class Configuration implements ConfigurationInterface
         $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 @@ final class Configuration implements ConfigurationInterface
                     ->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 @@ final class Configuration implements ConfigurationInterface
                     ->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 @@ final class Configuration implements ConfigurationInterface
             ->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 3350867..ec7a600 100644
--- a/DependencyInjection/TrikoderOAuth2Extension.php
+++ b/DependencyInjection/TrikoderOAuth2Extension.php
@@ -150,38 +150,40 @@ final class TrikoderOAuth2Extension extends Extension implements PrependExtensio
             $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 @@ final class TrikoderOAuth2Extension extends Extension implements PrependExtensio
 
     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 fa48c50..d823f17 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 11836d4..63a53d8 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_
     ```
 


Test patch:
diff --git a/Tests/Unit/ExtensionTest.php b/Tests/Unit/ExtensionTest.php
index 6508199..146ae35 100644
--- a/Tests/Unit/ExtensionTest.php
+++ b/Tests/Unit/ExtensionTest.php
@@ -7,15 +7,205 @@ namespace Trikoder\Bundle\OAuth2Bundle\Tests\Unit;
 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 @@ final class ExtensionTest extends TestCase
 
     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 @@ final class ExtensionTest extends TestCase
      * @dataProvider requireCodeChallengeForPublicClientsProvider
      */
     public function testAuthCodeGrantDisableRequireCodeChallengeForPublicClientsConfig(
+        string $configKey,
         ?bool $requireCodeChallengeForPublicClients,
         bool $shouldTheRequirementBeDisabled
     ): void {
@@ -78,8 +312,9 @@ final class ExtensionTest extends TestCase
 
         $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 @@ final class ExtensionTest extends TestCase
 
     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 @@ final class ExtensionTest extends TestCase
                 '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',