Skip to content

Commit 5464c57

Browse files
wouterjchalasr
authored andcommitted
[SecurityBundle] Improve support for authenticators that don't need a user provider
Ref #48285
1 parent cb6f2c5 commit 5464c57

File tree

7 files changed

+93
-14
lines changed

7 files changed

+93
-14
lines changed

src/Symfony/Bundle/SecurityBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Deprecate enabling bundle and not configuring it
88
* Add `_stateless` attribute to the request when firewall is stateless
9+
* Add `StatelessAuthenticatorFactoryInterface` for authenticators targeting `stateless` firewalls only and that don't require a user provider
910

1011
6.2
1112
---

src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AccessTokenFactory.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
*
2424
* @internal
2525
*/
26-
final class AccessTokenFactory extends AbstractFactory
26+
final class AccessTokenFactory extends AbstractFactory implements StatelessAuthenticatorFactoryInterface
2727
{
2828
private const PRIORITY = -40;
2929

@@ -67,7 +67,7 @@ public function getKey(): string
6767
return 'access_token';
6868
}
6969

70-
public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string
70+
public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, ?string $userProviderId): string
7171
{
7272
$successHandler = isset($config['success_handler']) ? new Reference($this->createAuthenticationSuccessHandler($container, $firewallName, $config)) : null;
7373
$failureHandler = isset($config['failure_handler']) ? new Reference($this->createAuthenticationFailureHandler($container, $firewallName, $config)) : null;
@@ -78,7 +78,7 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal
7878
->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.access_token'))
7979
->replaceArgument(0, new Reference($config['token_handler']))
8080
->replaceArgument(1, new Reference($extractorId))
81-
->replaceArgument(2, new Reference($userProviderId))
81+
->replaceArgument(2, $userProviderId ? new Reference($userProviderId) : null)
8282
->replaceArgument(3, $successHandler)
8383
->replaceArgument(4, $failureHandler)
8484
->replaceArgument(5, $config['realm'])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory;
13+
14+
use Symfony\Component\DependencyInjection\ContainerBuilder;
15+
16+
/**
17+
* Stateless authenticators are authenticators that can work without a user provider.
18+
*
19+
* This situation can only occur in stateless firewalls, as statefull firewalls
20+
* need the user provider to refresh the user in each subsequent request. A
21+
* stateless authenticator can be used on both stateless and statefull authenticators.
22+
*
23+
* @author Wouter de Jong <wouter@wouterj.nl>
24+
*/
25+
interface StatelessAuthenticatorFactoryInterface extends AuthenticatorFactoryInterface
26+
{
27+
public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, ?string $userProviderId): string|array;
28+
}

src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Bridge\Twig\Extension\LogoutUrlExtension;
1515
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface;
1616
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FirewallListenerFactoryInterface;
17+
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\StatelessAuthenticatorFactoryInterface;
1718
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface;
1819
use Symfony\Bundle\SecurityBundle\SecurityBundle;
1920
use Symfony\Component\Config\Definition\ConfigurationInterface;
@@ -615,6 +616,10 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri
615616
throw new InvalidConfigurationException(sprintf('Authenticator factory "%s" ("%s") must implement "%s".', get_debug_type($factory), $key, AuthenticatorFactoryInterface::class));
616617
}
617618

619+
if (null === $userProvider && !$factory instanceof StatelessAuthenticatorFactoryInterface) {
620+
$userProvider = $this->createMissingUserProvider($container, $id, $key);
621+
}
622+
618623
$authenticators = $factory->createAuthenticator($container, $id, $firewall[$key], $userProvider);
619624
if (\is_array($authenticators)) {
620625
foreach ($authenticators as $authenticator) {
@@ -641,7 +646,7 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri
641646
return [$listeners, $defaultEntryPoint];
642647
}
643648

644-
private function getUserProvider(ContainerBuilder $container, string $id, array $firewall, string $factoryKey, ?string $defaultProvider, array $providerIds, ?string $contextListenerId): string
649+
private function getUserProvider(ContainerBuilder $container, string $id, array $firewall, string $factoryKey, ?string $defaultProvider, array $providerIds, ?string $contextListenerId): ?string
645650
{
646651
if (isset($firewall[$factoryKey]['provider'])) {
647652
if (!isset($providerIds[$normalizedName = str_replace('-', '_', $firewall[$factoryKey]['provider'])])) {
@@ -660,13 +665,11 @@ private function getUserProvider(ContainerBuilder $container, string $id, array
660665
}
661666

662667
if (!$providerIds) {
663-
$userProvider = sprintf('security.user.provider.missing.%s', $factoryKey);
664-
$container->setDefinition(
665-
$userProvider,
666-
(new ChildDefinition('security.user.provider.missing'))->replaceArgument(0, $id)
667-
);
668+
if ($firewall['stateless'] ?? false) {
669+
return null;
670+
}
668671

669-
return $userProvider;
672+
return $this->createMissingUserProvider($container, $id, $factoryKey);
670673
}
671674

672675
if ('remember_me' === $factoryKey || 'anonymous' === $factoryKey || 'custom_authenticators' === $factoryKey) {
@@ -680,6 +683,17 @@ private function getUserProvider(ContainerBuilder $container, string $id, array
680683
throw new InvalidConfigurationException(sprintf('Not configuring explicitly the provider for the "%s" authenticator on "%s" firewall is ambiguous as there is more than one registered provider.', $factoryKey, $id));
681684
}
682685

686+
private function createMissingUserProvider(ContainerBuilder $container, string $id, string $factoryKey): string
687+
{
688+
$userProvider = sprintf('security.user.provider.missing.%s', $factoryKey);
689+
$container->setDefinition(
690+
$userProvider,
691+
(new ChildDefinition('security.user.provider.missing'))->replaceArgument(0, $id)
692+
);
693+
694+
return $userProvider;
695+
}
696+
683697
private function createHashers(array $hashers, ContainerBuilder $container)
684698
{
685699
$hasherMap = [];

src/Symfony/Bundle/SecurityBundle/Tests/Functional/AccessTokenTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,4 +315,16 @@ public function customQueryAccessTokenFailure(): iterable
315315
{
316316
yield ['/foo?protection_token=INVALID_ACCESS_TOKEN'];
317317
}
318+
319+
public function testSelfContainedTokens()
320+
{
321+
$client = $this->createClient(['test_case' => 'AccessToken', 'root_config' => 'config_self_contained_token.yml']);
322+
$client->catchExceptions(false);
323+
$client->request('GET', '/foo', [], [], ['HTTP_AUTHORIZATION' => 'Bearer SELF_CONTAINED_ACCESS_TOKEN']);
324+
$response = $client->getResponse();
325+
326+
$this->assertInstanceOf(Response::class, $response);
327+
$this->assertSame(200, $response->getStatusCode());
328+
$this->assertSame(['message' => 'Welcome @dunglas!'], json_decode($response->getContent(), true));
329+
}
318330
}

src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AccessTokenBundle/Security/Handler/AccessTokenHandler.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,17 @@
1212
namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AccessTokenBundle\Security\Handler;
1313

1414
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
15+
use Symfony\Component\Security\Core\User\InMemoryUser;
1516
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
1617
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
1718

1819
class AccessTokenHandler implements AccessTokenHandlerInterface
1920
{
20-
public function __construct()
21-
{
22-
}
23-
2421
public function getUserBadgeFrom(string $accessToken): UserBadge
2522
{
2623
return match ($accessToken) {
2724
'VALID_ACCESS_TOKEN' => new UserBadge('dunglas'),
25+
'SELF_CONTAINED_ACCESS_TOKEN' => new UserBadge('dunglas', fn () => new InMemoryUser('dunglas', null, ['ROLE_USER'])),
2826
default => throw new BadCredentialsException('Invalid credentials.'),
2927
};
3028
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
imports:
2+
- { resource: ./../config/framework.yml }
3+
4+
framework:
5+
http_method_override: false
6+
serializer: ~
7+
8+
security:
9+
password_hashers:
10+
Symfony\Component\Security\Core\User\InMemoryUser: plaintext
11+
12+
firewalls:
13+
main:
14+
pattern: ^/
15+
stateless: true
16+
access_token:
17+
token_handler: access_token.access_token_handler
18+
token_extractors: 'header'
19+
realm: 'My API'
20+
21+
access_control:
22+
- { path: ^/foo, roles: ROLE_USER }
23+
24+
services:
25+
access_token.access_token_handler:
26+
class: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AccessTokenBundle\Security\Handler\AccessTokenHandler

0 commit comments

Comments
 (0)