Skip to content

Commit d693432

Browse files
committed
Added pre-authenticated authenticators (X.509 & REMOTE_USER)
1 parent 4c24b4a commit d693432

File tree

6 files changed

+419
-1
lines changed

6 files changed

+419
-1
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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\Component\Security\Http\Authenticator;
13+
14+
use Psr\Log\LoggerInterface;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\Response;
17+
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
18+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
19+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
20+
use Symfony\Component\Security\Core\Exception\AuthenticationException;
21+
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
22+
use Symfony\Component\Security\Core\User\UserInterface;
23+
use Symfony\Component\Security\Core\User\UserProviderInterface;
24+
25+
/**
26+
* The base authenticator for authenticators to use pre-authenticated
27+
* requests (e.g. using certificates).
28+
*
29+
* @author Wouter de Jong <wouter@wouterj.nl>
30+
* @author Fabien Potencier <fabien@symfony.com>
31+
*
32+
* @internal
33+
* @experimental in Symfony 5.1
34+
*/
35+
abstract class AbstractPreAuthenticatedAuthenticator implements InteractiveAuthenticatorInterface, CustomAuthenticatedInterface
36+
{
37+
private $userProvider;
38+
private $tokenStorage;
39+
private $firewallName;
40+
private $logger;
41+
42+
public function __construct(UserProviderInterface $userProvider, TokenStorageInterface $tokenStorage, string $firewallName, ?LoggerInterface $logger = null)
43+
{
44+
$this->userProvider = $userProvider;
45+
$this->tokenStorage = $tokenStorage;
46+
$this->firewallName = $firewallName;
47+
$this->logger = $logger;
48+
}
49+
50+
/**
51+
* Returns the username of the pre-authenticated user.
52+
*
53+
* This authenticator is skipped if null is returned or a custom
54+
* BadCredentialsException is thrown.
55+
*/
56+
abstract protected function extractUsername(Request $request): ?string;
57+
58+
public function supports(Request $request): ?bool
59+
{
60+
try {
61+
$username = $this->extractUsername($request);
62+
} catch (BadCredentialsException $e) {
63+
$this->clearToken($e);
64+
65+
if (null !== $this->logger) {
66+
$this->logger->debug('Skipping pre-authenticated authenticator as a BadCredentialsException is thrown.', ['exception' => $e, 'authenticator' => \get_class($this)]);
67+
}
68+
69+
return false;
70+
}
71+
72+
if (null === $username) {
73+
if (null !== $this->logger) {
74+
$this->logger->debug('Skipping pre-authenticated authenticator no username could be extracted.', ['authenticator' => \get_class($this)]);
75+
}
76+
77+
return false;
78+
}
79+
80+
$request->attributes->set('_pre_authenticated_username', $username);
81+
82+
return true;
83+
}
84+
85+
public function getCredentials(Request $request)
86+
{
87+
return [
88+
'username' => $request->attributes->get('_pre_authenticated_username'),
89+
];
90+
}
91+
92+
public function getUser($credentials): ?UserInterface
93+
{
94+
return $this->userProvider->loadUserByUsername($credentials['username']);
95+
}
96+
97+
public function checkCredentials($credentials, UserInterface $user): bool
98+
{
99+
// the user is already authenticated before it entered Symfony
100+
return true;
101+
}
102+
103+
public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface
104+
{
105+
return new PreAuthenticatedToken($user, null, $providerKey);
106+
}
107+
108+
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response
109+
{
110+
return null; // let the original request continue
111+
}
112+
113+
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
114+
{
115+
$this->clearToken($exception);
116+
117+
return null;
118+
}
119+
120+
public function isInteractive(): bool
121+
{
122+
return true;
123+
}
124+
125+
private function clearToken(AuthenticationException $exception): void
126+
{
127+
$token = $this->tokenStorage->getToken();
128+
if ($token instanceof PreAuthenticatedToken && $this->firewallName === $token->getProviderKey()) {
129+
$this->tokenStorage->setToken(null);
130+
131+
if (null !== $this->logger) {
132+
$this->logger->info('Cleared pre-authenticated token due to an exception.', ['exception' => $exception]);
133+
}
134+
}
135+
}
136+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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\Component\Security\Http\Authenticator;
13+
14+
use Psr\Log\LoggerInterface;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
17+
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
18+
use Symfony\Component\Security\Core\User\UserProviderInterface;
19+
20+
/**
21+
* This authenticator authenticates a remote user.
22+
*
23+
* @author Wouter de Jong <wouter@wouterj.nl>
24+
* @author Fabien Potencier <fabien@symfony.com>
25+
* @author Maxime Douailin <maxime.douailin@gmail.com>
26+
*
27+
* @internal in Symfony 5.1
28+
*/
29+
class RemoteUserAuthenticator extends AbstractPreAuthenticatedAuthenticator
30+
{
31+
private $userKey;
32+
33+
public function __construct(UserProviderInterface $userProvider, TokenStorageInterface $tokenStorage, string $firewallName, string $userKey = 'REMOTE_USER', ?LoggerInterface $logger = null)
34+
{
35+
parent::__construct($userProvider, $tokenStorage, $firewallName, $logger);
36+
37+
$this->userKey = $userKey;
38+
}
39+
40+
protected function extractUsername(Request $request): ?string
41+
{
42+
if (!$request->server->has($this->userKey)) {
43+
throw new BadCredentialsException(sprintf('User key was not found: "%s".', $this->userKey));
44+
}
45+
46+
return $request->server->get($this->userKey);
47+
}
48+
}

Authenticator/X509Authenticator.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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\Component\Security\Http\Authenticator;
13+
14+
use Psr\Log\LoggerInterface;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
17+
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
18+
use Symfony\Component\Security\Core\User\UserProviderInterface;
19+
20+
/**
21+
* This authenticator authenticates pre-authenticated (by the
22+
* webserver) X.509 certificates.
23+
*
24+
* @author Wouter de Jong <wouter@wouterj.nl>
25+
* @author Fabien Potencier <fabien@symfony.com>
26+
*
27+
* @internal
28+
* @experimental in Symfony 5.1
29+
*/
30+
class X509Authenticator extends AbstractPreAuthenticatedAuthenticator
31+
{
32+
private $userKey;
33+
private $credentialsKey;
34+
35+
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)
36+
{
37+
parent::__construct($userProvider, $tokenStorage, $firewallName, $logger);
38+
39+
$this->userKey = $userKey;
40+
$this->credentialsKey = $credentialsKey;
41+
}
42+
43+
protected function extractUsername(Request $request): string
44+
{
45+
$username = null;
46+
if ($request->server->has($this->userKey)) {
47+
$username = $request->server->get($this->userKey);
48+
} elseif (
49+
$request->server->has($this->credentialsKey)
50+
&& preg_match('#emailAddress=([^,/@]++@[^,/]++)#', $request->server->get($this->credentialsKey), $matches)
51+
) {
52+
$username = $matches[1];
53+
}
54+
55+
if (null === $username) {
56+
throw new BadCredentialsException(sprintf('SSL credentials not found: %s, %s', $this->userKey, $this->credentialsKey));
57+
}
58+
59+
return $username;
60+
}
61+
}

EventListener/UserCheckerListener.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
66
use Symfony\Component\Security\Core\User\UserCheckerInterface;
7+
use Symfony\Component\Security\Http\Authenticator\AbstractPreAuthenticatedAuthenticator;
78
use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent;
89

910
/**
@@ -23,7 +24,7 @@ public function __construct(UserCheckerInterface $userChecker)
2324

2425
public function preCredentialsVerification(VerifyAuthenticatorCredentialsEvent $event): void
2526
{
26-
if (null === $event->getUser()) {
27+
if (null === $event->getUser() || $event->getAuthenticator() instanceof AbstractPreAuthenticatedAuthenticator) {
2728
return;
2829
}
2930

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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\Component\Security\Http\Tests\Authenticator;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
17+
use Symfony\Component\Security\Core\User\UserProviderInterface;
18+
use Symfony\Component\Security\Http\Authenticator\RemoteUserAuthenticator;
19+
20+
class RemoteUserAuthenticatorTest extends TestCase
21+
{
22+
/**
23+
* @dataProvider provideAuthenticators
24+
*/
25+
public function testSupport(RemoteUserAuthenticator $authenticator, $parameterName)
26+
{
27+
$request = $this->createRequest([$parameterName => 'TheUsername']);
28+
29+
$this->assertTrue($authenticator->supports($request));
30+
}
31+
32+
public function testSupportNoUser()
33+
{
34+
$authenticator = new RemoteUserAuthenticator($this->createMock(UserProviderInterface::class), new TokenStorage(), 'main');
35+
36+
$this->assertFalse($authenticator->supports($this->createRequest([])));
37+
}
38+
39+
/**
40+
* @dataProvider provideAuthenticators
41+
*/
42+
public function testGetCredentials(RemoteUserAuthenticator $authenticator, $parameterName)
43+
{
44+
$request = $this->createRequest([$parameterName => 'TheUsername']);
45+
46+
$authenticator->supports($request);
47+
$this->assertEquals(['username' => 'TheUsername'], $authenticator->getCredentials($request));
48+
}
49+
50+
public function provideAuthenticators()
51+
{
52+
$userProvider = $this->createMock(UserProviderInterface::class);
53+
54+
yield [new RemoteUserAuthenticator($userProvider, new TokenStorage(), 'main'), 'REMOTE_USER'];
55+
yield [new RemoteUserAuthenticator($userProvider, new TokenStorage(), 'main', 'CUSTOM_USER_PARAMETER'), 'CUSTOM_USER_PARAMETER'];
56+
}
57+
58+
private function createRequest(array $server)
59+
{
60+
return new Request([], [], [], [], [], $server);
61+
}
62+
}

0 commit comments

Comments
 (0)