Skip to content

Commit 7b25756

Browse files
committed
feature #28 Add default scopes (mtarld)
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
2 parents c1fff46 + 2ea9d4d commit 7b25756

26 files changed

+376
-67
lines changed

docs/basic-setup.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ oauth2_restricted:
125125
126126
## Security roles
127127
128-
Once the user gets past the `oauth2` firewall, they will be granted additional roles based on their granted [token scopes](controlling-token-scopes.md).
128+
Once the user gets past the `oauth2` firewall, they will be granted additional roles based on their granted [token scopes](token-scopes.md).
129129
By default, the roles are named in the following format:
130130
131131
```

docs/controlling-token-scopes.md

Lines changed: 0 additions & 34 deletions
This file was deleted.

docs/index.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,14 @@ For implementation into Symfony projects, please see [bundle documentation](docs
7878
# How to generate a public key: https://oauth2.thephpleague.com/installation/#generating-public-and-private-keys
7979
public_key: ~ # Required, Example: /var/oauth/public.key
8080
81-
# Scopes that you wish to utilize in your application.
82-
# This should be a simple array of strings.
83-
scopes: []
81+
scopes:
82+
# Scopes that you wish to utilize in your application.
83+
# This should be a simple array of strings.
84+
available: []
85+
86+
# Scopes that will be assigned when no scope given.
87+
# This should be a simple array of strings.
88+
default: []
8489
8590
# Configures different persistence methods that can be used by the bundle for saving client and token data.
8691
# Only one persistence method can be configured at a time.
@@ -140,7 +145,7 @@ security:
140145
## Configuration
141146
142147
* [Basic setup](basic-setup.md)
143-
* [Controlling token scopes](controlling-token-scopes.md)
148+
* [Token scopes](token-scopes.md)
144149
* [Implementing custom grant type](implementing-custom-grant-type.md)
145150
* [Using custom client](using-custom-client.md)
146151

docs/token-scopes.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Token scopes
2+
3+
## Setting default scopes
4+
5+
Having a client with no scope gives the client access to all the scopes.
6+
In most cases, it's a bad idea and could result as security vulnerability.
7+
8+
That's why you have to specify in the bundle configuration the default scopes that will be applied when no scope is given:
9+
```yaml
10+
# config/packages/league_oauth2_server.yaml
11+
12+
league_oauth2_server:
13+
scopes:
14+
available: [EMAIL, PREFERENCES]
15+
default: [EMAIL]
16+
```
17+
18+
If you still want clients without scopes to have access to every scopes, you can use role hierarchy as a workaround:
19+
```yaml
20+
# config/packages/league_oauth2_server.yaml
21+
22+
league_oauth2_server:
23+
role_prefix: ROLE_OAUTH2_
24+
25+
scopes:
26+
available: [EMAIL, PREFERENCES, SUPER_USER]
27+
default: [SUPER_USER]
28+
```
29+
30+
```yaml
31+
# config/packages/security.yaml
32+
security:
33+
role_hierarchy:
34+
ROLE_OAUTH2_SUPER_USER: [ROLE_OAUTH2_EMAIL, ROLE_OAUTH2_PREFERENCES]
35+
```
36+
37+
## Controlling token scopes
38+
39+
It's possible to alter issued access token's scopes by subscribing to the `league.oauth2_server.scope_resolve` event.
40+
41+
### Example
42+
43+
#### Listener
44+
```php
45+
<?php
46+
47+
namespace App\EventListener;
48+
49+
use League\Bundle\OAuth2ServerBundle\Event\ScopeResolveEvent;
50+
51+
final class ScopeResolveListener
52+
{
53+
public function onScopeResolve(ScopeResolveEvent $event): void
54+
{
55+
$requestedScopes = $event->getScopes();
56+
57+
// ...Make adjustments to the client's requested scopes...
58+
59+
$event->setScopes(...$requestedScopes);
60+
}
61+
}
62+
```
63+
64+
#### Service configuration
65+
66+
```yaml
67+
App\EventListener\ScopeResolveListener:
68+
tags:
69+
- { name: kernel.event_listener, event: league.oauth2_server.scope_resolve, method: onScopeResolve }
70+
```

src/Controller/AuthorizationController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
use Psr\Http\Message\ResponseFactoryInterface;
1616
use Symfony\Bridge\PsrHttpMessage\HttpFoundationFactoryInterface;
1717
use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
18-
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
1918
use Symfony\Component\HttpFoundation\Request;
2019
use Symfony\Component\HttpFoundation\Response;
20+
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
2121

2222
final class AuthorizationController
2323
{

src/DependencyInjection/Configuration.php

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,25 @@ private function createScopesNode(): NodeDefinition
138138
$node = $treeBuilder->getRootNode();
139139

140140
$node
141-
->info("Scopes that you wish to utilize in your application.\nThis should be a simple array of strings.")
142-
->scalarPrototype()
143-
->treatNullLike([])
141+
->isRequired()
142+
->children()
143+
->arrayNode('available')
144+
->info("Scopes that you wish to utilize in your application.\nThis should be a simple array of strings.")
145+
->isRequired()
146+
->cannotBeEmpty()
147+
->scalarPrototype()
148+
->cannotBeEmpty()
149+
->end()
150+
->end()
151+
->arrayNode('default')
152+
->info("Scopes that will be assigned when no scope given.\nThis should be a simple array of strings.")
153+
->isRequired()
154+
->cannotBeEmpty()
155+
->scalarPrototype()
156+
->cannotBeEmpty()
157+
->end()
158+
->end()
159+
->end()
144160
;
145161

146162
return $node;

src/DependencyInjection/LeagueOAuth2ServerExtension.php

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ private function configureDoctrinePersistence(ContainerBuilder $container, array
248248
$container
249249
->findDefinition(ClientManager::class)
250250
->replaceArgument(0, $entityManager)
251-
->replaceArgument(1, $config['client']['classname'])
251+
->replaceArgument(2, $config['client']['classname'])
252252
;
253253

254254
$container
@@ -294,13 +294,17 @@ private function configureResourceServer(ContainerBuilder $container, array $con
294294

295295
private function configureScopes(ContainerBuilder $container, array $scopes): void
296296
{
297-
$scopeManager = $container
298-
->findDefinition(
299-
(string) $container->getAlias(ScopeManagerInterface::class)
300-
)
301-
;
297+
$availableScopes = $scopes['available'];
298+
$defaultScopes = $scopes['default'];
299+
300+
if ([] !== $invalidDefaultScopes = array_diff($defaultScopes, $availableScopes)) {
301+
throw new \LogicException(sprintf('Invalid default scopes "%s" for path "league_oauth2_server.scopes.default". Permissible values: "%s"', implode('", "', $invalidDefaultScopes), implode('", "', $availableScopes)));
302+
}
303+
304+
$container->setParameter('league.oauth2_server.scopes.default', $defaultScopes);
302305

303-
foreach ($scopes as $scope) {
306+
$scopeManager = $container->findDefinition(ScopeManagerInterface::class);
307+
foreach ($availableScopes as $scope) {
304308
$scopeManager->addMethodCall('save', [
305309
new Definition(ScopeModel::class, [$scope]),
306310
]);

src/Event/PreSaveClientEvent.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace League\Bundle\OAuth2ServerBundle\Event;
6+
7+
use League\Bundle\OAuth2ServerBundle\Model\AbstractClient;
8+
use Symfony\Contracts\EventDispatcher\Event;
9+
10+
/**
11+
* @author Mathias Arlaud <[email protected]>
12+
*/
13+
class PreSaveClientEvent extends Event
14+
{
15+
/**
16+
* @var AbstractClient
17+
*/
18+
private $client;
19+
20+
public function __construct(AbstractClient $client)
21+
{
22+
$this->client = $client;
23+
}
24+
25+
public function getClient(): AbstractClient
26+
{
27+
return $this->client;
28+
}
29+
30+
public function setClient(AbstractClient $client): void
31+
{
32+
$this->client = $client;
33+
}
34+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace League\Bundle\OAuth2ServerBundle\EventListener;
6+
7+
use League\Bundle\OAuth2ServerBundle\Event\PreSaveClientEvent;
8+
use League\Bundle\OAuth2ServerBundle\Model\Scope;
9+
10+
/**
11+
* Sets default scopes to the client before being saved by a ClientManager if no scope is specified.
12+
*
13+
* @author Mathias Arlaud <[email protected]>
14+
*/
15+
class AddClientDefaultScopesListener
16+
{
17+
/**
18+
* @var list<string>
19+
*/
20+
private $defaultScopes;
21+
22+
/**
23+
* @param list<string> $defaultScopes
24+
*/
25+
public function __construct(array $defaultScopes)
26+
{
27+
$this->defaultScopes = $defaultScopes;
28+
}
29+
30+
public function __invoke(PreSaveClientEvent $event): void
31+
{
32+
$client = $event->getClient();
33+
if ([] !== $client->getScopes()) {
34+
return;
35+
}
36+
37+
$client->setScopes(...array_map(static function (string $scope): Scope {
38+
return new Scope($scope);
39+
}, $this->defaultScopes));
40+
}
41+
}

src/Manager/Doctrine/ClientManager.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55
namespace League\Bundle\OAuth2ServerBundle\Manager\Doctrine;
66

77
use Doctrine\ORM\EntityManagerInterface;
8+
use League\Bundle\OAuth2ServerBundle\Event\PreSaveClientEvent;
89
use League\Bundle\OAuth2ServerBundle\Manager\ClientFilter;
910
use League\Bundle\OAuth2ServerBundle\Manager\ClientManagerInterface;
1011
use League\Bundle\OAuth2ServerBundle\Model\AbstractClient;
1112
use League\Bundle\OAuth2ServerBundle\Model\Grant;
1213
use League\Bundle\OAuth2ServerBundle\Model\RedirectUri;
1314
use League\Bundle\OAuth2ServerBundle\Model\Scope;
15+
use League\Bundle\OAuth2ServerBundle\OAuth2Events;
16+
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
1417

1518
final class ClientManager implements ClientManagerInterface
1619
{
@@ -24,12 +27,21 @@ final class ClientManager implements ClientManagerInterface
2427
*/
2528
private $clientFqcn;
2629

30+
/**
31+
* @var EventDispatcherInterface
32+
*/
33+
private $dispatcher;
34+
2735
/**
2836
* @param class-string<AbstractClient> $clientFqcn
2937
*/
30-
public function __construct(EntityManagerInterface $entityManager, string $clientFqcn)
31-
{
38+
public function __construct(
39+
EntityManagerInterface $entityManager,
40+
EventDispatcherInterface $dispatcher,
41+
string $clientFqcn
42+
) {
3243
$this->entityManager = $entityManager;
44+
$this->dispatcher = $dispatcher;
3345
$this->clientFqcn = $clientFqcn;
3446
}
3547

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

4355
public function save(AbstractClient $client): void
4456
{
57+
/** @var PreSaveClientEvent $event */
58+
$event = $this->dispatcher->dispatch(new PreSaveClientEvent($client), OAuth2Events::PRE_SAVE_CLIENT);
59+
$client = $event->getClient();
60+
4561
$this->entityManager->persist($client);
4662
$this->entityManager->flush();
4763
}

0 commit comments

Comments
 (0)