Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: #225: GrantConfigurator should accept League\OAuth2\Server\Grant… #227

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand All @@ -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')

Expand Down
85 changes: 85 additions & 0 deletions docs/implementing-custom-grant-type.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,90 @@
# Implementing custom grant type

1. Create a class that implements the `League\OAuth2\Server\Grant\GrantTypeInterface` interface.

Example:

```php
<?php

declare(strict_types=1);

namespace App\Grant;

use DateInterval;
use League\OAuth2\Server\Grant\AbstractGrant;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use Nyholm\Psr7\Response;
use Psr\Http\Message\ServerRequestInterface;
use League\OAuth2\Server\Grant\GrantTypeInterface;

final class FakeGrant extends AbstractGrant implements GrantTypeInterface
{
/**
* @var SomeDependency
*/
private $foo;

public function __construct(SomeDependency $foo)
{
$this->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
<?php
...

use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

#[AutoconfigureTag(name: 'league.oauth2_server.authorization_server.grant', attributes: [accessTokenTTL: 'PT5H'])]
final class FakeGrant extends AbstractGrant implements GrantTypeInterface
{
...
}
```

If `accessTokenTTL` tag attribute is not defined, then bundle config is used `league_oauth2_server.authorization_server.access_token_ttl` (same as `league.oauth2_server.access_token_ttl.default` service container parameter). \
`null` is considered as defined, to allow to unset ttl. \
`league_oauth2_server.authorization_server.refresh_token_ttl` is also accessible for your implementation using `league.oauth2_server.refresh_token_ttl.default` service container parameter.


# Implementing custom grant type (deprecated method)

1. Create a class that implements the `\League\Bundle\OAuth2ServerBundle\League\AuthorizationServer\GrantTypeInterface` interface.

Example:
Expand Down
7 changes: 6 additions & 1 deletion src/AuthorizationServer/GrantConfigurator.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

use League\OAuth2\Server\AuthorizationServer;

/**
* @deprecated
*/
final class GrantConfigurator
{
/**
Expand All @@ -24,7 +27,9 @@ public function __construct(iterable $grants)
public function __invoke(AuthorizationServer $authorizationServer): void
{
foreach ($this->grants as $grant) {
$authorizationServer->enableGrantType($grant, $grant->getAccessTokenTTL());
if ($grant instanceof GrantTypeInterface) {
$authorizationServer->enableGrantType($grant, $grant->getAccessTokenTTL());
}
}
}
}
3 changes: 3 additions & 0 deletions src/AuthorizationServer/GrantTypeInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
62 changes: 62 additions & 0 deletions src/DependencyInjection/CompilerPass/GrantTypePass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace League\Bundle\OAuth2ServerBundle\DependencyInjection\CompilerPass;

use League\Bundle\OAuth2ServerBundle\AuthorizationServer\GrantTypeInterface;
use League\OAuth2\Server\AuthorizationServer;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

class GrantTypePass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
// check if AuthorizationServer service is defined
if (!$container->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,
]);
}
}
}
}
4 changes: 4 additions & 0 deletions src/DependencyInjection/LeagueOAuth2ServerExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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', [
Expand Down
3 changes: 3 additions & 0 deletions src/LeagueOAuth2ServerBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,6 +27,8 @@ public function build(ContainerBuilder $container)

$this->configureDoctrineExtension($container);
$this->configureSecurityExtension($container);

$container->addCompilerPass(new GrantTypePass());
}

public function getContainerExtension(): ExtensionInterface
Expand Down
10 changes: 5 additions & 5 deletions tests/Acceptance/TokenEndpointTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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']);
Expand Down Expand Up @@ -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']);
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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']);
Expand Down
7 changes: 1 addition & 6 deletions tests/Fixtures/FakeGrant.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,9 +21,4 @@ public function respondToAccessTokenRequest(ServerRequestInterface $request, Res
{
return new Response();
}

public function getAccessTokenTTL(): ?\DateInterval
{
return new \DateInterval('PT5H');
}
}
24 changes: 24 additions & 0 deletions tests/Fixtures/FakeGrantNullAccessTokenTTL.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace League\Bundle\OAuth2ServerBundle\Tests\Fixtures;

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;

final class FakeGrantNullAccessTokenTTL extends AbstractGrant implements GrantTypeInterface
{
public function getIdentifier(): string
{
return self::class;
}

public function respondToAccessTokenRequest(ServerRequestInterface $request, ResponseTypeInterface $responseType, \DateInterval $accessTokenTTL): ResponseTypeInterface
{
return new Response();
}
}
24 changes: 24 additions & 0 deletions tests/Fixtures/FakeGrantUndefinedAccessTokenTTL.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace League\Bundle\OAuth2ServerBundle\Tests\Fixtures;

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;

final class FakeGrantUndefinedAccessTokenTTL extends AbstractGrant implements GrantTypeInterface
{
public function getIdentifier(): string
{
return self::class;
}

public function respondToAccessTokenRequest(ServerRequestInterface $request, ResponseTypeInterface $responseType, \DateInterval $accessTokenTTL): ResponseTypeInterface
{
return new Response();
}
}
33 changes: 33 additions & 0 deletions tests/Fixtures/FakeLegacyGrant.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace League\Bundle\OAuth2ServerBundle\Tests\Fixtures;

use League\Bundle\OAuth2ServerBundle\AuthorizationServer\GrantTypeInterface;
use League\OAuth2\Server\Grant\AbstractGrant;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use Nyholm\Psr7\Response;
use Psr\Http\Message\ServerRequestInterface;

// TODO remove code bloc when bundle interface and configurator will be deleted
/**
* @deprecated
*/
final class FakeLegacyGrant extends AbstractGrant implements GrantTypeInterface
{
public function getIdentifier(): string
{
return 'fake_legacy_grant';
}

public function respondToAccessTokenRequest(ServerRequestInterface $request, ResponseTypeInterface $responseType, \DateInterval $accessTokenTTL): ResponseTypeInterface
{
return new Response();
}

public function getAccessTokenTTL(): ?\DateInterval
{
return new \DateInterval('PT5H');
}
}
Loading
Loading