Skip to content

Commit 61b7e38

Browse files
committed
Add support for authenticator security system
1 parent a654afe commit 61b7e38

34 files changed

+1168
-193
lines changed

.github/workflows/unit-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ jobs:
1212
matrix:
1313
symfony-version:
1414
- "4.4.*"
15+
- "5.1.*"
1516
- "5.2.*"
1617
php-version:
1718
- "7.2"

.psalm.baseline.xml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<files psalm-version="4.7.0@d4377c0baf3ffbf0b1ec6998e8d1be2a40971005">
3+
<file src="src/Resources/config/services.php">
4+
<ImplicitToStringCast occurrences="1">
5+
<code>service(GrantConfigurator::class)</code>
6+
</ImplicitToStringCast>
7+
<MixedInferredReturnType occurrences="1">
8+
<code>ReferenceConfigurator</code>
9+
</MixedInferredReturnType>
10+
<MixedReturnStatement occurrences="1">
11+
<code>($fn)($id)</code>
12+
</MixedReturnStatement>
13+
</file>
14+
<file src="src/Security/Authenticator/OAuth2Authenticator.php">
15+
<PossiblyInvalidArgument occurrences="1">
16+
<code>$this-&gt;getUserBadge($userIdentifier)</code>
17+
</PossiblyInvalidArgument>
18+
</file>
19+
</files>

psalm.xml

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1212
xmlns="https://getpsalm.org/schema/config"
1313
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
14+
errorBaseline=".psalm.baseline.xml"
1415
>
1516
<projectFiles>
1617
<directory name="src"/>
@@ -44,27 +45,6 @@
4445
<RawObjectIteration errorLevel="error"/>
4546
<InvalidStringClass errorLevel="error"/>
4647
<UnresolvableInclude errorLevel="error"/>
47-
<ImplicitToStringCast errorLevel="error">
48-
<errorLevel type="suppress">
49-
<file name="src/Resources/config/services.php" />
50-
</errorLevel>
51-
</ImplicitToStringCast>
52-
<MixedInferredReturnType errorLevel="error">
53-
<errorLevel type="suppress">
54-
<file name="src/Resources/config/services.php" />
55-
</errorLevel>
56-
</MixedInferredReturnType>
57-
<MixedReturnStatement errorLevel="error">
58-
<errorLevel type="suppress">
59-
<file name="src/Resources/config/services.php" />
60-
</errorLevel>
61-
</MixedReturnStatement>
62-
<InvalidArgument errorLevel="error">
63-
<errorLevel type="suppress">
64-
<file name="src/Resources/config/routes.php" />
65-
<referencedFunction name="Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator::controller" />
66-
</errorLevel>
67-
</InvalidArgument>
6848
</issueHandlers>
6949

7050
<plugins>

src/DependencyInjection/LeagueOAuth2ServerExtension.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
use League\Bundle\OAuth2ServerBundle\Manager\Doctrine\RefreshTokenManager;
1818
use League\Bundle\OAuth2ServerBundle\Manager\ScopeManagerInterface;
1919
use League\Bundle\OAuth2ServerBundle\Model\Scope as ScopeModel;
20-
use League\Bundle\OAuth2ServerBundle\Security\Authentication\Token\OAuth2TokenFactory;
20+
use League\Bundle\OAuth2ServerBundle\Security\Authentication\Token\LegacyOAuth2TokenFactory;
21+
use League\Bundle\OAuth2ServerBundle\Security\Authenticator\OAuth2Authenticator;
2122
use League\Bundle\OAuth2ServerBundle\Service\CredentialsRevoker\DoctrineCredentialsRevoker;
2223
use League\OAuth2\Server\AuthorizationServer;
2324
use League\OAuth2\Server\CryptKey;
@@ -60,9 +61,12 @@ public function load(array $configs, ContainerBuilder $container)
6061
$this->configureResourceServer($container, $config['resource_server']);
6162
$this->configureScopes($container, $config['scopes']);
6263

63-
$container->findDefinition(OAuth2TokenFactory::class)
64+
$container->findDefinition(LegacyOAuth2TokenFactory::class)
6465
->setArgument(0, $config['role_prefix']);
6566

67+
$container->findDefinition(OAuth2Authenticator::class)
68+
->setArgument(3, $config['role_prefix']);
69+
6670
$container->findDefinition(ConvertExceptionToResponseListener::class)
6771
->addTag('kernel.event_listener', [
6872
'event' => KernelEvents::EXCEPTION,
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace League\Bundle\OAuth2ServerBundle\DependencyInjection\Security;
6+
7+
use League\Bundle\OAuth2ServerBundle\Security\Authentication\Provider\OAuth2Provider;
8+
use League\Bundle\OAuth2ServerBundle\Security\EntryPoint\OAuth2EntryPoint;
9+
use League\Bundle\OAuth2ServerBundle\Security\Firewall\OAuth2Listener;
10+
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
11+
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
12+
use Symfony\Component\DependencyInjection\ChildDefinition;
13+
use Symfony\Component\DependencyInjection\ContainerBuilder;
14+
use Symfony\Component\DependencyInjection\Reference;
15+
16+
final class LegacyOAuth2Factory implements SecurityFactoryInterface
17+
{
18+
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint): array
19+
{
20+
$provider = sprintf('security.authentication.provider.oauth2.%s', $id);
21+
$container
22+
->setDefinition($provider, new ChildDefinition(OAuth2Provider::class))
23+
->replaceArgument(0, new Reference($userProvider))
24+
->replaceArgument(3, $id);
25+
26+
$listener = sprintf('security.authentication.listener.oauth2.%s', $id);
27+
$container
28+
->setDefinition($listener, new ChildDefinition(OAuth2Listener::class))
29+
->replaceArgument(4, $id);
30+
31+
return [$provider, $listener, OAuth2EntryPoint::class];
32+
}
33+
34+
public function getPosition(): string
35+
{
36+
return 'pre_auth';
37+
}
38+
39+
public function getKey(): string
40+
{
41+
return 'oauth2';
42+
}
43+
44+
public function addConfiguration(NodeDefinition $builder): void
45+
{
46+
}
47+
}

src/DependencyInjection/Security/OAuth2Factory.php

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,57 +5,56 @@
55
namespace League\Bundle\OAuth2ServerBundle\DependencyInjection\Security;
66

77
use League\Bundle\OAuth2ServerBundle\Security\Authentication\Provider\OAuth2Provider;
8+
use League\Bundle\OAuth2ServerBundle\Security\Authenticator\OAuth2Authenticator;
89
use League\Bundle\OAuth2ServerBundle\Security\EntryPoint\OAuth2EntryPoint;
910
use League\Bundle\OAuth2ServerBundle\Security\Firewall\OAuth2Listener;
11+
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface;
1012
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
1113
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
1214
use Symfony\Component\DependencyInjection\ChildDefinition;
1315
use Symfony\Component\DependencyInjection\ContainerBuilder;
1416
use Symfony\Component\DependencyInjection\Reference;
1517

16-
final class OAuth2Factory implements SecurityFactoryInterface
18+
/**
19+
* @author Mathias Arlaud <[email protected]>
20+
*/
21+
final class OAuth2Factory implements SecurityFactoryInterface, AuthenticatorFactoryInterface
1722
{
18-
/**
19-
* {@inheritdoc}
20-
*/
21-
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
23+
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint): array
2224
{
23-
$providerId = 'security.authentication.provider.oauth2.' . $id;
25+
$provider = sprintf('security.authentication.provider.oauth2.%s', $id);
2426
$container
25-
->setDefinition($providerId, new ChildDefinition(OAuth2Provider::class))
27+
->setDefinition($provider, new ChildDefinition(OAuth2Provider::class))
2628
->replaceArgument(0, new Reference($userProvider))
2729
->replaceArgument(3, $id);
2830

29-
$listenerId = 'security.authentication.listener.oauth2.' . $id;
31+
$listener = sprintf('security.authentication.listener.oauth2.%s', $id);
3032
$container
31-
->setDefinition($listenerId, new ChildDefinition(OAuth2Listener::class))
33+
->setDefinition($listener, new ChildDefinition(OAuth2Listener::class))
3234
->replaceArgument(4, $id);
3335

34-
return [$providerId, $listenerId, OAuth2EntryPoint::class];
36+
return [$provider, $listener, OAuth2EntryPoint::class];
3537
}
3638

37-
/**
38-
* {@inheritdoc}
39-
*/
40-
public function getPosition()
39+
public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProvider): string
40+
{
41+
$authenticator = sprintf('security.authenticator.oauth2.%s', $firewallName);
42+
$container->setDefinition($authenticator, new ChildDefinition(OAuth2Authenticator::class));
43+
44+
return $authenticator;
45+
}
46+
47+
public function getPosition(): string
4148
{
4249
return 'pre_auth';
4350
}
4451

45-
/**
46-
* {@inheritdoc}
47-
*/
48-
public function getKey()
52+
public function getKey(): string
4953
{
5054
return 'oauth2';
5155
}
5256

53-
/**
54-
* {@inheritdoc}
55-
*
56-
* @return void
57-
*/
58-
public function addConfiguration(NodeDefinition $node)
57+
public function addConfiguration(NodeDefinition $builder): void
5958
{
6059
}
6160
}

src/EventListener/ConvertExceptionToResponseListener.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44

55
namespace League\Bundle\OAuth2ServerBundle\EventListener;
66

7-
use League\Bundle\OAuth2ServerBundle\Security\Exception\InsufficientScopesException;
8-
use League\Bundle\OAuth2ServerBundle\Security\Exception\Oauth2AuthenticationFailedException;
7+
use League\Bundle\OAuth2ServerBundle\Security\Exception\OAuth2AuthenticationException;
98
use Symfony\Component\HttpFoundation\Response;
109
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
1110

@@ -17,8 +16,8 @@ final class ConvertExceptionToResponseListener
1716
public function onKernelException(ExceptionEvent $event): void
1817
{
1918
$exception = $event->getThrowable();
20-
if ($exception instanceof InsufficientScopesException || $exception instanceof Oauth2AuthenticationFailedException) {
21-
$event->setResponse(new Response($exception->getMessage(), (int) $exception->getCode()));
19+
if ($exception instanceof OAuth2AuthenticationException) {
20+
$event->setResponse(new Response($exception->getMessage(), $exception->getStatusCode(), ['WWW-Authenticate' => 'Bearer']));
2221
}
2322
}
2423
}

src/LeagueOAuth2ServerBundle.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\DoctrineOrmMappingsPass;
88
use League\Bundle\OAuth2ServerBundle\DependencyInjection\LeagueOAuth2ServerExtension;
9+
use League\Bundle\OAuth2ServerBundle\DependencyInjection\Security\LegacyOAuth2Factory;
910
use League\Bundle\OAuth2ServerBundle\DependencyInjection\Security\OAuth2Factory;
11+
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface;
1012
use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension;
1113
use Symfony\Component\DependencyInjection\ContainerBuilder;
1214
use Symfony\Component\HttpKernel\Bundle\Bundle;
@@ -38,7 +40,9 @@ private function configureSecurityExtension(ContainerBuilder $container): void
3840
{
3941
/** @var SecurityExtension $extension */
4042
$extension = $container->getExtension('security');
41-
$extension->addSecurityListenerFactory(new OAuth2Factory());
43+
44+
// BC Layer for < 5.1 versions
45+
$extension->addSecurityListenerFactory(interface_exists(AuthenticatorFactoryInterface::class) ? new OAuth2Factory() : new LegacyOAuth2Factory());
4246
}
4347

4448
private function configureDoctrineExtension(ContainerBuilder $container): void

src/Resources/config/services.php

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@
3232
use League\Bundle\OAuth2ServerBundle\Manager\ScopeManagerInterface;
3333
use League\Bundle\OAuth2ServerBundle\OAuth2Events;
3434
use League\Bundle\OAuth2ServerBundle\Security\Authentication\Provider\OAuth2Provider;
35-
use League\Bundle\OAuth2ServerBundle\Security\Authentication\Token\OAuth2TokenFactory;
35+
use League\Bundle\OAuth2ServerBundle\Security\Authentication\Token\LegacyOAuth2TokenFactory;
36+
use League\Bundle\OAuth2ServerBundle\Security\Authenticator\OAuth2Authenticator;
3637
use League\Bundle\OAuth2ServerBundle\Security\EntryPoint\OAuth2EntryPoint;
38+
use League\Bundle\OAuth2ServerBundle\Security\EventListener\CheckScopesListener;
3739
use League\Bundle\OAuth2ServerBundle\Security\Firewall\OAuth2Listener;
3840
use League\OAuth2\Server\AuthorizationServer;
3941
use League\OAuth2\Server\Grant\AuthCodeGrant;
@@ -128,11 +130,24 @@ function service(string $id): ReferenceConfigurator
128130
->alias(AuthCodeRepository::class, 'league.oauth2_server.repository.auth_code')
129131

130132
// Security layer
133+
->set('league.oauth2_server.authenticator.oauth2', OAuth2Authenticator::class)
134+
->args([
135+
service('league.oauth2-server.psr_http_factory'),
136+
service(ResourceServer::class),
137+
service(UserProviderInterface::class),
138+
null,
139+
])
140+
->alias(OAuth2Authenticator::class, 'league.oauth2_server.authenticator.oauth2')
141+
142+
->set('league.oauth2_server.listener.check_scopes', CheckScopesListener::class)
143+
->tag('kernel.event_subscriber')
144+
->alias(CheckScopesListener::class, 'league.oauth2_server.listener.check_scopes')
145+
131146
->set('league.oauth2_server.provider.oauth2', OAuth2Provider::class)
132147
->args([
133148
service(UserProviderInterface::class),
134149
service(ResourceServer::class),
135-
service(OAuth2TokenFactory::class),
150+
service(LegacyOAuth2TokenFactory::class),
136151
null,
137152
])
138153
->alias(OAuth2Provider::class, 'league.oauth2_server.provider.oauth2')
@@ -145,7 +160,7 @@ function service(string $id): ReferenceConfigurator
145160
service(TokenStorageInterface::class),
146161
service(AuthenticationManagerInterface::class),
147162
service('league.oauth2_server.factory.psr_http'),
148-
service(OAuth2TokenFactory::class),
163+
service(LegacyOAuth2TokenFactory::class),
149164
null,
150165
])
151166
->alias(OAuth2Listener::class, 'league.oauth2_server.security.firewall.oauth2_listener')
@@ -302,8 +317,8 @@ function service(string $id): ReferenceConfigurator
302317
])
303318
->alias(AuthorizationRequestResolveEventFactory::class, 'league.oauth2_server.factory.authorization_request_resolve_event')
304319

305-
->set('league.oauth2_server.factory.oauth2_token', OAuth2TokenFactory::class)
306-
->alias(OAuth2TokenFactory::class, 'league.oauth2_server.factory.oauth2_token')
320+
->set('league.oauth2_server.factory.legacy_oauth2_token', OAuth2TokenFactory::class)
321+
->alias(LegacyOAuth2TokenFactory::class, 'league.oauth2_server.factory.legacy_oauth2_token')
307322

308323
// Storage managers
309324
->set('league.oauth2_server.manager.in_memory.scope', ScopeManager::class)

src/Security/Authentication/Provider/OAuth2Provider.php

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44

55
namespace League\Bundle\OAuth2ServerBundle\Security\Authentication\Provider;
66

7-
use League\Bundle\OAuth2ServerBundle\Security\Authentication\Token\OAuth2Token;
8-
use League\Bundle\OAuth2ServerBundle\Security\Authentication\Token\OAuth2TokenFactory;
7+
use League\Bundle\OAuth2ServerBundle\Security\Authentication\Token\LegacyOAuth2Token;
8+
use League\Bundle\OAuth2ServerBundle\Security\Authentication\Token\LegacyOAuth2TokenFactory;
9+
use League\Bundle\OAuth2ServerBundle\Security\User\NullUser;
910
use League\OAuth2\Server\Exception\OAuthServerException;
1011
use League\OAuth2\Server\ResourceServer;
1112
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
@@ -27,7 +28,7 @@ final class OAuth2Provider implements AuthenticationProviderInterface
2728
private $resourceServer;
2829

2930
/**
30-
* @var OAuth2TokenFactory
31+
* @var LegacyOAuth2TokenFactory
3132
*/
3233
private $oauth2TokenFactory;
3334

@@ -39,7 +40,7 @@ final class OAuth2Provider implements AuthenticationProviderInterface
3940
public function __construct(
4041
UserProviderInterface $userProvider,
4142
ResourceServer $resourceServer,
42-
OAuth2TokenFactory $oauth2TokenFactory,
43+
LegacyOAuth2TokenFactory $oauth2TokenFactory,
4344
string $providerKey
4445
) {
4546
$this->userProvider = $userProvider;
@@ -54,7 +55,7 @@ public function __construct(
5455
public function authenticate(TokenInterface $token)
5556
{
5657
if (!$this->supports($token)) {
57-
throw new \RuntimeException(sprintf('This authentication provider can only handle tokes of type \'%s\'.', OAuth2Token::class));
58+
throw new \RuntimeException(sprintf('This authentication provider can only handle tokens of type \'%s\'.', LegacyOAuth2Token::class));
5859
}
5960

6061
try {
@@ -78,21 +79,21 @@ public function authenticate(TokenInterface $token)
7879
/**
7980
* {@inheritdoc}
8081
*
81-
* @psalm-assert-if-true OAuth2Token $token
82+
* @psalm-assert-if-true LegacyOAuth2Token $token
8283
*/
8384
public function supports(TokenInterface $token)
8485
{
85-
return $token instanceof OAuth2Token && $this->providerKey === $token->getProviderKey();
86+
return $token instanceof LegacyOAuth2Token && $this->providerKey === $token->getProviderKey();
8687
}
8788

88-
private function getAuthenticatedUser(string $userIdentifier): ?UserInterface
89+
private function getAuthenticatedUser(string $userIdentifier): UserInterface
8990
{
9091
if ('' === $userIdentifier) {
9192
/*
9293
* If the identifier is an empty string, that means that the
9394
* access token isn't bound to a user defined in the system.
9495
*/
95-
return null;
96+
return new NullUser();
9697
}
9798

9899
return $this->userProvider->loadUserByUsername($userIdentifier);

0 commit comments

Comments
 (0)