Skip to content

Commit

Permalink
feature #28 Add default scopes (mtarld)
Browse files Browse the repository at this point in the history
This PR was merged into the 0.1-dev branch.

Discussion
----------

Add default scopes

- Closes #22
- Replaces `Symfony\Component\EventDispatcher\EventDispatcherInterface` by `Symfony\Contracts\EventDispatcher\EventDispatcherInterface` to be consistent

Need to wait for #24 to see the green CI

Commits
-------

2ea9d4d Add default scopes
  • Loading branch information
chalasr committed Aug 23, 2021
2 parents c1fff46 + 2ea9d4d commit 7b25756
Show file tree
Hide file tree
Showing 26 changed files with 376 additions and 67 deletions.
2 changes: 1 addition & 1 deletion docs/basic-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ oauth2_restricted:
## Security roles
Once the user gets past the `oauth2` firewall, they will be granted additional roles based on their granted [token scopes](controlling-token-scopes.md).
Once the user gets past the `oauth2` firewall, they will be granted additional roles based on their granted [token scopes](token-scopes.md).
By default, the roles are named in the following format:
```
Expand Down
34 changes: 0 additions & 34 deletions docs/controlling-token-scopes.md

This file was deleted.

13 changes: 9 additions & 4 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,14 @@ For implementation into Symfony projects, please see [bundle documentation](docs
# How to generate a public key: https://oauth2.thephpleague.com/installation/#generating-public-and-private-keys
public_key: ~ # Required, Example: /var/oauth/public.key
# Scopes that you wish to utilize in your application.
# This should be a simple array of strings.
scopes: []
scopes:
# Scopes that you wish to utilize in your application.
# This should be a simple array of strings.
available: []
# Scopes that will be assigned when no scope given.
# This should be a simple array of strings.
default: []
# Configures different persistence methods that can be used by the bundle for saving client and token data.
# Only one persistence method can be configured at a time.
Expand Down Expand Up @@ -140,7 +145,7 @@ security:
## Configuration
* [Basic setup](basic-setup.md)
* [Controlling token scopes](controlling-token-scopes.md)
* [Token scopes](token-scopes.md)
* [Implementing custom grant type](implementing-custom-grant-type.md)
* [Using custom client](using-custom-client.md)
Expand Down
70 changes: 70 additions & 0 deletions docs/token-scopes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Token scopes

## Setting default scopes

Having a client with no scope gives the client access to all the scopes.
In most cases, it's a bad idea and could result as security vulnerability.

That's why you have to specify in the bundle configuration the default scopes that will be applied when no scope is given:
```yaml
# config/packages/league_oauth2_server.yaml

league_oauth2_server:
scopes:
available: [EMAIL, PREFERENCES]
default: [EMAIL]
```
If you still want clients without scopes to have access to every scopes, you can use role hierarchy as a workaround:
```yaml
# config/packages/league_oauth2_server.yaml

league_oauth2_server:
role_prefix: ROLE_OAUTH2_

scopes:
available: [EMAIL, PREFERENCES, SUPER_USER]
default: [SUPER_USER]
```
```yaml
# config/packages/security.yaml
security:
role_hierarchy:
ROLE_OAUTH2_SUPER_USER: [ROLE_OAUTH2_EMAIL, ROLE_OAUTH2_PREFERENCES]
```
## Controlling token scopes
It's possible to alter issued access token's scopes by subscribing to the `league.oauth2_server.scope_resolve` event.

### Example

#### Listener
```php
<?php
namespace App\EventListener;
use League\Bundle\OAuth2ServerBundle\Event\ScopeResolveEvent;
final class ScopeResolveListener
{
public function onScopeResolve(ScopeResolveEvent $event): void
{
$requestedScopes = $event->getScopes();
// ...Make adjustments to the client's requested scopes...
$event->setScopes(...$requestedScopes);
}
}
```

#### Service configuration

```yaml
App\EventListener\ScopeResolveListener:
tags:
- { name: kernel.event_listener, event: league.oauth2_server.scope_resolve, method: onScopeResolve }
```
2 changes: 1 addition & 1 deletion src/Controller/AuthorizationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
use Psr\Http\Message\ResponseFactoryInterface;
use Symfony\Bridge\PsrHttpMessage\HttpFoundationFactoryInterface;
use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

final class AuthorizationController
{
Expand Down
22 changes: 19 additions & 3 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,25 @@ private function createScopesNode(): NodeDefinition
$node = $treeBuilder->getRootNode();

$node
->info("Scopes that you wish to utilize in your application.\nThis should be a simple array of strings.")
->scalarPrototype()
->treatNullLike([])
->isRequired()
->children()
->arrayNode('available')
->info("Scopes that you wish to utilize in your application.\nThis should be a simple array of strings.")
->isRequired()
->cannotBeEmpty()
->scalarPrototype()
->cannotBeEmpty()
->end()
->end()
->arrayNode('default')
->info("Scopes that will be assigned when no scope given.\nThis should be a simple array of strings.")
->isRequired()
->cannotBeEmpty()
->scalarPrototype()
->cannotBeEmpty()
->end()
->end()
->end()
;

return $node;
Expand Down
18 changes: 11 additions & 7 deletions src/DependencyInjection/LeagueOAuth2ServerExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ private function configureDoctrinePersistence(ContainerBuilder $container, array
$container
->findDefinition(ClientManager::class)
->replaceArgument(0, $entityManager)
->replaceArgument(1, $config['client']['classname'])
->replaceArgument(2, $config['client']['classname'])
;

$container
Expand Down Expand Up @@ -294,13 +294,17 @@ private function configureResourceServer(ContainerBuilder $container, array $con

private function configureScopes(ContainerBuilder $container, array $scopes): void
{
$scopeManager = $container
->findDefinition(
(string) $container->getAlias(ScopeManagerInterface::class)
)
;
$availableScopes = $scopes['available'];
$defaultScopes = $scopes['default'];

if ([] !== $invalidDefaultScopes = array_diff($defaultScopes, $availableScopes)) {
throw new \LogicException(sprintf('Invalid default scopes "%s" for path "league_oauth2_server.scopes.default". Permissible values: "%s"', implode('", "', $invalidDefaultScopes), implode('", "', $availableScopes)));
}

$container->setParameter('league.oauth2_server.scopes.default', $defaultScopes);

foreach ($scopes as $scope) {
$scopeManager = $container->findDefinition(ScopeManagerInterface::class);
foreach ($availableScopes as $scope) {
$scopeManager->addMethodCall('save', [
new Definition(ScopeModel::class, [$scope]),
]);
Expand Down
34 changes: 34 additions & 0 deletions src/Event/PreSaveClientEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace League\Bundle\OAuth2ServerBundle\Event;

use League\Bundle\OAuth2ServerBundle\Model\AbstractClient;
use Symfony\Contracts\EventDispatcher\Event;

/**
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*/
class PreSaveClientEvent extends Event
{
/**
* @var AbstractClient
*/
private $client;

public function __construct(AbstractClient $client)
{
$this->client = $client;
}

public function getClient(): AbstractClient
{
return $this->client;
}

public function setClient(AbstractClient $client): void
{
$this->client = $client;
}
}
41 changes: 41 additions & 0 deletions src/EventListener/AddClientDefaultScopesListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace League\Bundle\OAuth2ServerBundle\EventListener;

use League\Bundle\OAuth2ServerBundle\Event\PreSaveClientEvent;
use League\Bundle\OAuth2ServerBundle\Model\Scope;

/**
* Sets default scopes to the client before being saved by a ClientManager if no scope is specified.
*
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*/
class AddClientDefaultScopesListener
{
/**
* @var list<string>
*/
private $defaultScopes;

/**
* @param list<string> $defaultScopes
*/
public function __construct(array $defaultScopes)
{
$this->defaultScopes = $defaultScopes;
}

public function __invoke(PreSaveClientEvent $event): void
{
$client = $event->getClient();
if ([] !== $client->getScopes()) {
return;
}

$client->setScopes(...array_map(static function (string $scope): Scope {
return new Scope($scope);
}, $this->defaultScopes));
}
}
20 changes: 18 additions & 2 deletions src/Manager/Doctrine/ClientManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
namespace League\Bundle\OAuth2ServerBundle\Manager\Doctrine;

use Doctrine\ORM\EntityManagerInterface;
use League\Bundle\OAuth2ServerBundle\Event\PreSaveClientEvent;
use League\Bundle\OAuth2ServerBundle\Manager\ClientFilter;
use League\Bundle\OAuth2ServerBundle\Manager\ClientManagerInterface;
use League\Bundle\OAuth2ServerBundle\Model\AbstractClient;
use League\Bundle\OAuth2ServerBundle\Model\Grant;
use League\Bundle\OAuth2ServerBundle\Model\RedirectUri;
use League\Bundle\OAuth2ServerBundle\Model\Scope;
use League\Bundle\OAuth2ServerBundle\OAuth2Events;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

final class ClientManager implements ClientManagerInterface
{
Expand All @@ -24,12 +27,21 @@ final class ClientManager implements ClientManagerInterface
*/
private $clientFqcn;

/**
* @var EventDispatcherInterface
*/
private $dispatcher;

/**
* @param class-string<AbstractClient> $clientFqcn
*/
public function __construct(EntityManagerInterface $entityManager, string $clientFqcn)
{
public function __construct(
EntityManagerInterface $entityManager,
EventDispatcherInterface $dispatcher,
string $clientFqcn
) {
$this->entityManager = $entityManager;
$this->dispatcher = $dispatcher;
$this->clientFqcn = $clientFqcn;
}

Expand All @@ -42,6 +54,10 @@ public function find(string $identifier): ?AbstractClient

public function save(AbstractClient $client): void
{
/** @var PreSaveClientEvent $event */
$event = $this->dispatcher->dispatch(new PreSaveClientEvent($client), OAuth2Events::PRE_SAVE_CLIENT);
$client = $event->getClient();

$this->entityManager->persist($client);
$this->entityManager->flush();
}
Expand Down
17 changes: 17 additions & 0 deletions src/Manager/InMemory/ClientManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@

namespace League\Bundle\OAuth2ServerBundle\Manager\InMemory;

use League\Bundle\OAuth2ServerBundle\Event\PreSaveClientEvent;
use League\Bundle\OAuth2ServerBundle\Manager\ClientFilter;
use League\Bundle\OAuth2ServerBundle\Manager\ClientManagerInterface;
use League\Bundle\OAuth2ServerBundle\Model\AbstractClient;
use League\Bundle\OAuth2ServerBundle\Model\Grant;
use League\Bundle\OAuth2ServerBundle\Model\RedirectUri;
use League\Bundle\OAuth2ServerBundle\Model\Scope;
use League\Bundle\OAuth2ServerBundle\OAuth2Events;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

final class ClientManager implements ClientManagerInterface
{
Expand All @@ -18,13 +21,27 @@ final class ClientManager implements ClientManagerInterface
*/
private $clients = [];

/**
* @var EventDispatcherInterface
*/
private $dispatcher;

public function __construct(EventDispatcherInterface $dispatcher)
{
$this->dispatcher = $dispatcher;
}

public function find(string $identifier): ?AbstractClient
{
return $this->clients[$identifier] ?? null;
}

public function save(AbstractClient $client): void
{
/** @var PreSaveClientEvent $event */
$event = $this->dispatcher->dispatch(new PreSaveClientEvent($client), OAuth2Events::PRE_SAVE_CLIENT);
$client = $event->getClient();

$this->clients[$client->getIdentifier()] = $client;
}

Expand Down
8 changes: 8 additions & 0 deletions src/OAuth2Events.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,12 @@ final class OAuth2Events
* must be redirected to resolve the authorization request.
*/
public const AUTHORIZATION_REQUEST_RESOLVE = 'league.oauth2_server.event.authorization_request_resolve';

/**
* The PRE_SAVE_CLIENT event occurs right before the client is saved
* by a ClientManager.
*
* You could alter the client here.
*/
public const PRE_SAVE_CLIENT = 'league.oauth2_server.event.pre_save_client';
}
Loading

0 comments on commit 7b25756

Please sign in to comment.