Added pre-authenticated authenticators (X.509 & REMOTE_USER)

This commit is contained in:
Wouter de Jong 2020-03-14 15:17:10 +01:00
parent f5e11e5f32
commit 95edc806a1
9 changed files with 473 additions and 3 deletions

View File

@ -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';

View File

@ -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';

View File

@ -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>

View File

@ -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]);
}
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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);
}
}