Skip to content

[SecurityBundle] Improve support for authenticators that don't need a user provider #48594

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

Merged
merged 1 commit into from
Dec 18, 2022
Merged
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
1 change: 1 addition & 0 deletions src/Symfony/Bundle/SecurityBundle/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ CHANGELOG

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

6.2
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
*
* @internal
*/
final class AccessTokenFactory extends AbstractFactory
final class AccessTokenFactory extends AbstractFactory implements StatelessAuthenticatorFactoryInterface
{
private const PRIORITY = -40;

Expand Down Expand Up @@ -67,7 +67,7 @@ public function getKey(): string
return 'access_token';
}

public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string
public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, ?string $userProviderId): string
{
$successHandler = isset($config['success_handler']) ? new Reference($this->createAuthenticationSuccessHandler($container, $firewallName, $config)) : null;
$failureHandler = isset($config['failure_handler']) ? new Reference($this->createAuthenticationFailureHandler($container, $firewallName, $config)) : null;
Expand All @@ -78,7 +78,7 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal
->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.access_token'))
->replaceArgument(0, new Reference($config['token_handler']))
->replaceArgument(1, new Reference($extractorId))
->replaceArgument(2, new Reference($userProviderId))
->replaceArgument(2, $userProviderId ? new Reference($userProviderId) : null)
->replaceArgument(3, $successHandler)
->replaceArgument(4, $failureHandler)
->replaceArgument(5, $config['realm'])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory;

use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
* Stateless authenticators are authenticators that can work without a user provider.
*
* This situation can only occur in stateless firewalls, as statefull firewalls
* need the user provider to refresh the user in each subsequent request. A
* stateless authenticator can be used on both stateless and statefull authenticators.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
interface StatelessAuthenticatorFactoryInterface extends AuthenticatorFactoryInterface
{
public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, ?string $userProviderId): string|array;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Symfony\Bridge\Twig\Extension\LogoutUrlExtension;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FirewallListenerFactoryInterface;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\StatelessAuthenticatorFactoryInterface;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Component\Config\Definition\ConfigurationInterface;
Expand Down Expand Up @@ -615,6 +616,10 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri
throw new InvalidConfigurationException(sprintf('Authenticator factory "%s" ("%s") must implement "%s".', get_debug_type($factory), $key, AuthenticatorFactoryInterface::class));
}

if (null === $userProvider && !$factory instanceof StatelessAuthenticatorFactoryInterface) {
$userProvider = $this->createMissingUserProvider($container, $id, $key);
}

$authenticators = $factory->createAuthenticator($container, $id, $firewall[$key], $userProvider);
if (\is_array($authenticators)) {
foreach ($authenticators as $authenticator) {
Expand All @@ -641,7 +646,7 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri
return [$listeners, $defaultEntryPoint];
}

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

if (!$providerIds) {
$userProvider = sprintf('security.user.provider.missing.%s', $factoryKey);
$container->setDefinition(
$userProvider,
(new ChildDefinition('security.user.provider.missing'))->replaceArgument(0, $id)
);
if ($firewall['stateless'] ?? false) {
return null;
}

return $userProvider;
return $this->createMissingUserProvider($container, $id, $factoryKey);
}

if ('remember_me' === $factoryKey || 'anonymous' === $factoryKey || 'custom_authenticators' === $factoryKey) {
Expand All @@ -680,6 +683,17 @@ private function getUserProvider(ContainerBuilder $container, string $id, array
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));
}

private function createMissingUserProvider(ContainerBuilder $container, string $id, string $factoryKey): string
{
$userProvider = sprintf('security.user.provider.missing.%s', $factoryKey);
$container->setDefinition(
$userProvider,
(new ChildDefinition('security.user.provider.missing'))->replaceArgument(0, $id)
);

return $userProvider;
}

private function createHashers(array $hashers, ContainerBuilder $container)
{
$hasherMap = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,4 +315,16 @@ public function customQueryAccessTokenFailure(): iterable
{
yield ['/foo?protection_token=INVALID_ACCESS_TOKEN'];
}

public function testSelfContainedTokens()
{
$client = $this->createClient(['test_case' => 'AccessToken', 'root_config' => 'config_self_contained_token.yml']);
$client->catchExceptions(false);
$client->request('GET', '/foo', [], [], ['HTTP_AUTHORIZATION' => 'Bearer SELF_CONTAINED_ACCESS_TOKEN']);
$response = $client->getResponse();

$this->assertInstanceOf(Response::class, $response);
$this->assertSame(200, $response->getStatusCode());
$this->assertSame(['message' => 'Welcome @dunglas!'], json_decode($response->getContent(), true));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,17 @@
namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AccessTokenBundle\Security\Handler;

use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\InMemoryUser;
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;

class AccessTokenHandler implements AccessTokenHandlerInterface
{
public function __construct()
{
}

public function getUserBadgeFrom(string $accessToken): UserBadge
{
return match ($accessToken) {
'VALID_ACCESS_TOKEN' => new UserBadge('dunglas'),
'SELF_CONTAINED_ACCESS_TOKEN' => new UserBadge('dunglas', fn () => new InMemoryUser('dunglas', null, ['ROLE_USER'])),
default => throw new BadCredentialsException('Invalid credentials.'),
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
imports:
- { resource: ./../config/framework.yml }

framework:
http_method_override: false
serializer: ~

security:
password_hashers:
Symfony\Component\Security\Core\User\InMemoryUser: plaintext

firewalls:
main:
pattern: ^/
stateless: true
access_token:
token_handler: access_token.access_token_handler
token_extractors: 'header'
realm: 'My API'

access_control:
- { path: ^/foo, roles: ROLE_USER }

services:
access_token.access_token_handler:
class: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AccessTokenBundle\Security\Handler\AccessTokenHandler