Added pre-authenticated authenticators (X.509 & REMOTE_USER)
This commit is contained in:
parent
f5e11e5f32
commit
95edc806a1
@ -22,7 +22,7 @@ use Symfony\Component\DependencyInjection\Reference;
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Maxime Douailin <maxime.douailin@gmail.com>
|
||||
*/
|
||||
class RemoteUserFactory implements SecurityFactoryInterface
|
||||
class RemoteUserFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface
|
||||
{
|
||||
public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint)
|
||||
{
|
||||
@ -43,6 +43,19 @@ class RemoteUserFactory implements SecurityFactoryInterface
|
||||
return [$providerId, $listenerId, $defaultEntryPoint];
|
||||
}
|
||||
|
||||
public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId)
|
||||
{
|
||||
$authenticatorId = 'security.authenticator.remote_user.'.$id;
|
||||
$container
|
||||
->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.remote_user'))
|
||||
->replaceArgument(0, new Reference($userProviderId))
|
||||
->replaceArgument(2, $firewallName)
|
||||
->replaceArgument(3, $config['user'])
|
||||
;
|
||||
|
||||
return $authenticatorId;
|
||||
}
|
||||
|
||||
public function getPosition()
|
||||
{
|
||||
return 'pre_auth';
|
||||
|
@ -21,7 +21,7 @@ use Symfony\Component\DependencyInjection\Reference;
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class X509Factory implements SecurityFactoryInterface
|
||||
class X509Factory implements SecurityFactoryInterface, AuthenticatorFactoryInterface
|
||||
{
|
||||
public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint)
|
||||
{
|
||||
@ -44,6 +44,20 @@ class X509Factory implements SecurityFactoryInterface
|
||||
return [$providerId, $listenerId, $defaultEntryPoint];
|
||||
}
|
||||
|
||||
public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId)
|
||||
{
|
||||
$authenticatorId = 'security.authenticator.x509.'.$id;
|
||||
$container
|
||||
->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.x509'))
|
||||
->replaceArgument(0, new Reference($userProviderId))
|
||||
->replaceArgument(2, $firewallName)
|
||||
->replaceArgument(3, $config['user'])
|
||||
->replaceArgument(4, $config['credentials'])
|
||||
;
|
||||
|
||||
return $authenticatorId;
|
||||
}
|
||||
|
||||
public function getPosition()
|
||||
{
|
||||
return 'pre_auth';
|
||||
|
@ -10,6 +10,7 @@
|
||||
class="Symfony\Component\Security\Http\Authentication\AuthenticatorManager"
|
||||
abstract="true"
|
||||
>
|
||||
<tag name="monolog.logger" channel="security" />
|
||||
<argument type="abstract">authenticators</argument>
|
||||
<argument type="service" id="security.token_storage" />
|
||||
<argument type="service" id="event_dispatcher" />
|
||||
@ -82,6 +83,7 @@
|
||||
<service id="security.authenticator.http_basic"
|
||||
class="Symfony\Component\Security\Http\Authenticator\HttpBasicAuthenticator"
|
||||
abstract="true">
|
||||
<tag name="monolog.logger" channel="security" />
|
||||
<argument type="abstract">realm name</argument>
|
||||
<argument type="abstract">user provider</argument>
|
||||
<argument type="service" id="logger" on-invalid="null" />
|
||||
@ -111,5 +113,28 @@
|
||||
<argument type="abstract">options</argument>
|
||||
<argument type="service" id="security.authentication.session_strategy" />
|
||||
</service>
|
||||
|
||||
<service id="security.authenticator.x509"
|
||||
class="Symfony\Component\Security\Http\Authenticator\X509Authenticator"
|
||||
abstract="true">
|
||||
<tag name="monolog.logger" channel="security" />
|
||||
<argument type="abstract">user provider</argument>
|
||||
<argument type="service" id="security.token_storage"/>
|
||||
<argument type="abstract">firewall name</argument>
|
||||
<argument type="abstract">user key</argument>
|
||||
<argument type="abstract">credentials key</argument>
|
||||
<argument type="service" id="logger" on-invalid="null" />
|
||||
</service>
|
||||
|
||||
<service id="security.authenticator.remote_user"
|
||||
class="Symfony\Component\Security\Http\Authenticator\RemoteUserAuthenticator"
|
||||
abstract="true">
|
||||
<tag name="monolog.logger" channel="security" />
|
||||
<argument type="abstract">user provider</argument>
|
||||
<argument type="service" id="security.token_storage"/>
|
||||
<argument type="abstract">firewall name</argument>
|
||||
<argument type="abstract">user key</argument>
|
||||
<argument type="service" id="logger" on-invalid="null" />
|
||||
</service>
|
||||
</services>
|
||||
</container>
|
||||
|
@ -0,0 +1,136 @@
|
||||
<?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\Component\Security\Http\Authenticator;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
/**
|
||||
* The base authenticator for authenticators to use pre-authenticated
|
||||
* requests (e.g. using certificates).
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* @internal
|
||||
* @experimental in Symfony 5.1
|
||||
*/
|
||||
abstract class AbstractPreAuthenticatedAuthenticator implements InteractiveAuthenticatorInterface, CustomAuthenticatedInterface
|
||||
{
|
||||
private $userProvider;
|
||||
private $tokenStorage;
|
||||
private $firewallName;
|
||||
private $logger;
|
||||
|
||||
public function __construct(UserProviderInterface $userProvider, TokenStorageInterface $tokenStorage, string $firewallName, ?LoggerInterface $logger = null)
|
||||
{
|
||||
$this->userProvider = $userProvider;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->firewallName = $firewallName;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the username of the pre-authenticated user.
|
||||
*
|
||||
* This authenticator is skipped if null is returned or a custom
|
||||
* BadCredentialsException is thrown.
|
||||
*/
|
||||
abstract protected function extractUsername(Request $request): ?string;
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
try {
|
||||
$username = $this->extractUsername($request);
|
||||
} catch (BadCredentialsException $e) {
|
||||
$this->clearToken($e);
|
||||
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->debug('Skipping pre-authenticated authenticator as a BadCredentialsException is thrown.', ['exception' => $e, 'authenticator' => \get_class($this)]);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (null === $username) {
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->debug('Skipping pre-authenticated authenticator no username could be extracted.', ['authenticator' => \get_class($this)]);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$request->attributes->set('_pre_authenticated_username', $username);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getCredentials(Request $request)
|
||||
{
|
||||
return [
|
||||
'username' => $request->attributes->get('_pre_authenticated_username'),
|
||||
];
|
||||
}
|
||||
|
||||
public function getUser($credentials): ?UserInterface
|
||||
{
|
||||
return $this->userProvider->loadUserByUsername($credentials['username']);
|
||||
}
|
||||
|
||||
public function checkCredentials($credentials, UserInterface $user): bool
|
||||
{
|
||||
// the user is already authenticated before it entered Symfony
|
||||
return true;
|
||||
}
|
||||
|
||||
public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface
|
||||
{
|
||||
return new PreAuthenticatedToken($user, null, $providerKey);
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response
|
||||
{
|
||||
return null; // let the original request continue
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||
{
|
||||
$this->clearToken($exception);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function isInteractive(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
private function clearToken(AuthenticationException $exception): void
|
||||
{
|
||||
$token = $this->tokenStorage->getToken();
|
||||
if ($token instanceof PreAuthenticatedToken && $this->firewallName === $token->getProviderKey()) {
|
||||
$this->tokenStorage->setToken(null);
|
||||
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->info('Cleared pre-authenticated token due to an exception.', ['exception' => $exception]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
<?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\Component\Security\Http\Authenticator;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
/**
|
||||
* This authenticator authenticates a remote user.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Maxime Douailin <maxime.douailin@gmail.com>
|
||||
*
|
||||
* @internal in Symfony 5.1
|
||||
*/
|
||||
class RemoteUserAuthenticator extends AbstractPreAuthenticatedAuthenticator
|
||||
{
|
||||
private $userKey;
|
||||
|
||||
public function __construct(UserProviderInterface $userProvider, TokenStorageInterface $tokenStorage, string $firewallName, string $userKey = 'REMOTE_USER', ?LoggerInterface $logger = null)
|
||||
{
|
||||
parent::__construct($userProvider, $tokenStorage, $firewallName, $logger);
|
||||
|
||||
$this->userKey = $userKey;
|
||||
}
|
||||
|
||||
protected function extractUsername(Request $request): ?string
|
||||
{
|
||||
if (!$request->server->has($this->userKey)) {
|
||||
throw new BadCredentialsException(sprintf('User key was not found: "%s".', $this->userKey));
|
||||
}
|
||||
|
||||
return $request->server->get($this->userKey);
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
<?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\Component\Security\Http\Authenticator;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
/**
|
||||
* This authenticator authenticates pre-authenticated (by the
|
||||
* webserver) X.509 certificates.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* @internal
|
||||
* @experimental in Symfony 5.1
|
||||
*/
|
||||
class X509Authenticator extends AbstractPreAuthenticatedAuthenticator
|
||||
{
|
||||
private $userKey;
|
||||
private $credentialsKey;
|
||||
|
||||
public function __construct(UserProviderInterface $userProvider, TokenStorageInterface $tokenStorage, string $firewallName, string $userKey = 'SSL_CLIENT_S_DN_Email', string $credentialsKey = 'SSL_CLIENT_S_DN', ?LoggerInterface $logger = null)
|
||||
{
|
||||
parent::__construct($userProvider, $tokenStorage, $firewallName, $logger);
|
||||
|
||||
$this->userKey = $userKey;
|
||||
$this->credentialsKey = $credentialsKey;
|
||||
}
|
||||
|
||||
protected function extractUsername(Request $request): string
|
||||
{
|
||||
$username = null;
|
||||
if ($request->server->has($this->userKey)) {
|
||||
$username = $request->server->get($this->userKey);
|
||||
} elseif (
|
||||
$request->server->has($this->credentialsKey)
|
||||
&& preg_match('#emailAddress=([^,/@]++@[^,/]++)#', $request->server->get($this->credentialsKey), $matches)
|
||||
) {
|
||||
$username = $matches[1];
|
||||
}
|
||||
|
||||
if (null === $username) {
|
||||
throw new BadCredentialsException(sprintf('SSL credentials not found: %s, %s', $this->userKey, $this->credentialsKey));
|
||||
}
|
||||
|
||||
return $username;
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ namespace Symfony\Component\Security\Http\EventListener;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Core\User\UserCheckerInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\AbstractPreAuthenticatedAuthenticator;
|
||||
use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent;
|
||||
|
||||
/**
|
||||
@ -23,7 +24,7 @@ class UserCheckerListener implements EventSubscriberInterface
|
||||
|
||||
public function preCredentialsVerification(VerifyAuthenticatorCredentialsEvent $event): void
|
||||
{
|
||||
if (null === $event->getUser()) {
|
||||
if (null === $event->getUser() || $event->getAuthenticator() instanceof AbstractPreAuthenticatedAuthenticator) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,62 @@
|
||||
<?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\Component\Security\Http\Tests\Authenticator;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\RemoteUserAuthenticator;
|
||||
|
||||
class RemoteUserAuthenticatorTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider provideAuthenticators
|
||||
*/
|
||||
public function testSupport(RemoteUserAuthenticator $authenticator, $parameterName)
|
||||
{
|
||||
$request = $this->createRequest([$parameterName => 'TheUsername']);
|
||||
|
||||
$this->assertTrue($authenticator->supports($request));
|
||||
}
|
||||
|
||||
public function testSupportNoUser()
|
||||
{
|
||||
$authenticator = new RemoteUserAuthenticator($this->createMock(UserProviderInterface::class), new TokenStorage(), 'main');
|
||||
|
||||
$this->assertFalse($authenticator->supports($this->createRequest([])));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideAuthenticators
|
||||
*/
|
||||
public function testGetCredentials(RemoteUserAuthenticator $authenticator, $parameterName)
|
||||
{
|
||||
$request = $this->createRequest([$parameterName => 'TheUsername']);
|
||||
|
||||
$authenticator->supports($request);
|
||||
$this->assertEquals(['username' => 'TheUsername'], $authenticator->getCredentials($request));
|
||||
}
|
||||
|
||||
public function provideAuthenticators()
|
||||
{
|
||||
$userProvider = $this->createMock(UserProviderInterface::class);
|
||||
|
||||
yield [new RemoteUserAuthenticator($userProvider, new TokenStorage(), 'main'), 'REMOTE_USER'];
|
||||
yield [new RemoteUserAuthenticator($userProvider, new TokenStorage(), 'main', 'CUSTOM_USER_PARAMETER'), 'CUSTOM_USER_PARAMETER'];
|
||||
}
|
||||
|
||||
private function createRequest(array $server)
|
||||
{
|
||||
return new Request([], [], [], [], [], $server);
|
||||
}
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
<?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\Component\Security\Http\Tests\Authenticator;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\X509Authenticator;
|
||||
|
||||
class X509AuthenticatorTest extends TestCase
|
||||
{
|
||||
private $userProvider;
|
||||
private $authenticator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->userProvider = $this->createMock(UserProviderInterface::class);
|
||||
$this->authenticator = new X509Authenticator($this->userProvider, new TokenStorage(), 'main');
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideServerVars
|
||||
*/
|
||||
public function testAuthentication($user, $credentials)
|
||||
{
|
||||
$serverVars = [];
|
||||
if ('' !== $user) {
|
||||
$serverVars['SSL_CLIENT_S_DN_Email'] = $user;
|
||||
}
|
||||
if ('' !== $credentials) {
|
||||
$serverVars['SSL_CLIENT_S_DN'] = $credentials;
|
||||
}
|
||||
|
||||
$request = $this->createRequest($serverVars);
|
||||
$this->assertTrue($this->authenticator->supports($request));
|
||||
$this->assertEquals(['username' => $user], $this->authenticator->getCredentials($request));
|
||||
}
|
||||
|
||||
public static function provideServerVars()
|
||||
{
|
||||
yield ['TheUser', 'TheCredentials'];
|
||||
yield ['TheUser', ''];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideServerVarsNoUser
|
||||
*/
|
||||
public function testAuthenticationNoUser($emailAddress, $credentials)
|
||||
{
|
||||
$request = $this->createRequest(['SSL_CLIENT_S_DN' => $credentials]);
|
||||
|
||||
$this->assertTrue($this->authenticator->supports($request));
|
||||
$this->assertEquals(['username' => $emailAddress], $this->authenticator->getCredentials($request));
|
||||
}
|
||||
|
||||
public static function provideServerVarsNoUser()
|
||||
{
|
||||
yield ['cert@example.com', 'CN=Sample certificate DN/emailAddress=cert@example.com'];
|
||||
yield ['cert+something@example.com', 'CN=Sample certificate DN/emailAddress=cert+something@example.com'];
|
||||
yield ['cert@example.com', 'CN=Sample certificate DN,emailAddress=cert@example.com'];
|
||||
yield ['cert+something@example.com', 'CN=Sample certificate DN,emailAddress=cert+something@example.com'];
|
||||
yield ['cert+something@example.com', 'emailAddress=cert+something@example.com,CN=Sample certificate DN'];
|
||||
yield ['cert+something@example.com', 'emailAddress=cert+something@example.com'];
|
||||
yield ['firstname.lastname@mycompany.co.uk', 'emailAddress=firstname.lastname@mycompany.co.uk,CN=Firstname.Lastname,OU=london,OU=company design and engineering,OU=Issuer London,OU=Roaming,OU=Interactive,OU=Users,OU=Standard,OU=Business,DC=england,DC=core,DC=company,DC=co,DC=uk'];
|
||||
}
|
||||
|
||||
public function testSupportNoData()
|
||||
{
|
||||
$request = $this->createRequest([]);
|
||||
|
||||
$this->assertFalse($this->authenticator->supports($request));
|
||||
}
|
||||
|
||||
public function testAuthenticationCustomUserKey()
|
||||
{
|
||||
$authenticator = new X509Authenticator($this->userProvider, new TokenStorage(), 'main', 'TheUserKey');
|
||||
|
||||
$request = $this->createRequest([
|
||||
'TheUserKey' => 'TheUser',
|
||||
]);
|
||||
$this->assertTrue($authenticator->supports($request));
|
||||
$this->assertEquals(['username' => 'TheUser'], $authenticator->getCredentials($request));
|
||||
}
|
||||
|
||||
public function testAuthenticationCustomCredentialsKey()
|
||||
{
|
||||
$authenticator = new X509Authenticator($this->userProvider, new TokenStorage(), 'main', 'SSL_CLIENT_S_DN_Email', 'TheCertKey');
|
||||
|
||||
$request = $this->createRequest([
|
||||
'TheCertKey' => 'CN=Sample certificate DN/emailAddress=cert@example.com',
|
||||
]);
|
||||
$this->assertTrue($authenticator->supports($request));
|
||||
$this->assertEquals(['username' => 'cert@example.com'], $authenticator->getCredentials($request));
|
||||
}
|
||||
|
||||
private function createRequest(array $server)
|
||||
{
|
||||
return new Request([], [], [], [], [], $server);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user