Skip to content

Commit 13e4aad

Browse files
committed
Refactor to an event based authentication approach
This allows more flexibility for the authentication manager (to e.g. implement login throttling, easier remember me, etc). It is also a known design pattern in Symfony HttpKernel.
1 parent 2ee64d4 commit 13e4aad

18 files changed

+661
-290
lines changed

Authentication/Authenticator/AnonymousAuthenticator.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
* @final
2828
* @experimental in 5.1
2929
*/
30-
class AnonymousAuthenticator implements AuthenticatorInterface
30+
class AnonymousAuthenticator implements AuthenticatorInterface, CustomAuthenticatedInterface
3131
{
3232
private $secret;
3333
private $tokenStorage;
@@ -49,14 +49,15 @@ public function getCredentials(Request $request)
4949
return [];
5050
}
5151

52-
public function getUser($credentials): ?UserInterface
52+
public function checkCredentials($credentials, UserInterface $user): bool
5353
{
54-
return new User('anon.', null);
54+
// anonymous users do not have credentials
55+
return true;
5556
}
5657

57-
public function checkCredentials($credentials, UserInterface $user): bool
58+
public function getUser($credentials): ?UserInterface
5859
{
59-
return true;
60+
return new User('anon.', null);
6061
}
6162

6263
public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface

Authentication/Authenticator/AuthenticatorInterface.php

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,18 +70,6 @@ public function getCredentials(Request $request);
7070
*/
7171
public function getUser($credentials): ?UserInterface;
7272

73-
/**
74-
* Returns true if the credentials are valid.
75-
*
76-
* If false is returned, authentication will fail. You may also throw
77-
* an AuthenticationException if you wish to cause authentication to fail.
78-
*
79-
* @param mixed $credentials the value returned from getCredentials()
80-
*
81-
* @throws AuthenticationException
82-
*/
83-
public function checkCredentials($credentials, UserInterface $user): bool;
84-
8573
/**
8674
* Create an authenticated token for the given user.
8775
*
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Symfony\Component\Security\Http\Authentication\Authenticator;
4+
5+
use Symfony\Component\Security\Core\Exception\AuthenticationException;
6+
use Symfony\Component\Security\Core\User\UserInterface;
7+
8+
/**
9+
* This interface should be implemented by authenticators that
10+
* require custom (not password related) authentication.
11+
*
12+
* @author Wouter de Jong <wouter@wouterj.nl>
13+
*/
14+
interface CustomAuthenticatedInterface
15+
{
16+
/**
17+
* Returns true if the credentials are valid.
18+
*
19+
* If false is returned, authentication will fail. You may also throw
20+
* an AuthenticationException if you wish to cause authentication to fail.
21+
*
22+
* @param mixed $credentials the value returned from getCredentials()
23+
*
24+
* @throws AuthenticationException
25+
*/
26+
public function checkCredentials($credentials, UserInterface $user): bool;
27+
}

Authentication/Authenticator/FormLoginAuthenticator.php

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\HttpFoundation\Response;
1616
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
1717
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
18+
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
1819
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
1920
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
2021
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
@@ -23,6 +24,7 @@
2324
use Symfony\Component\Security\Core\User\UserProviderInterface;
2425
use Symfony\Component\Security\Csrf\CsrfToken;
2526
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
27+
use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface;
2628
use Symfony\Component\Security\Http\HttpUtils;
2729
use Symfony\Component\Security\Http\ParameterBagUtils;
2830
use Symfony\Component\Security\Http\Util\TargetPathTrait;
@@ -34,23 +36,19 @@
3436
* @final
3537
* @experimental in 5.1
3638
*/
37-
class FormLoginAuthenticator extends AbstractFormLoginAuthenticator
39+
class FormLoginAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface
3840
{
39-
use TargetPathTrait, UsernamePasswordTrait {
40-
UsernamePasswordTrait::checkCredentials as checkPassword;
41-
}
41+
use TargetPathTrait;
4242

4343
private $options;
4444
private $httpUtils;
4545
private $csrfTokenManager;
4646
private $userProvider;
47-
private $encoderFactory;
4847

49-
public function __construct(HttpUtils $httpUtils, ?CsrfTokenManagerInterface $csrfTokenManager, UserProviderInterface $userProvider, EncoderFactoryInterface $encoderFactory, array $options)
48+
public function __construct(HttpUtils $httpUtils, ?CsrfTokenManagerInterface $csrfTokenManager, UserProviderInterface $userProvider, array $options)
5049
{
5150
$this->httpUtils = $httpUtils;
5251
$this->csrfTokenManager = $csrfTokenManager;
53-
$this->encoderFactory = $encoderFactory;
5452
$this->options = array_merge([
5553
'username_parameter' => '_username',
5654
'password_parameter' => '_password',
@@ -109,11 +107,17 @@ public function getCredentials(Request $request): array
109107
return $credentials;
110108
}
111109

110+
public function getPassword($credentials): ?string
111+
{
112+
return $credentials['password'];
113+
}
114+
112115
public function getUser($credentials): ?UserInterface
113116
{
114117
return $this->userProvider->loadUserByUsername($credentials['username']);
115118
}
116119

120+
/* @todo How to do CSRF protection?
117121
public function checkCredentials($credentials, UserInterface $user): bool
118122
{
119123
if (null !== $this->csrfTokenManager) {
@@ -123,6 +127,11 @@ public function checkCredentials($credentials, UserInterface $user): bool
123127
}
124128
125129
return $this->checkPassword($credentials, $user);
130+
}*/
131+
132+
public function createAuthenticatedToken(UserInterface $user, $providerKey): TokenInterface
133+
{
134+
return new UsernamePasswordToken($user, null, $providerKey, $user->getRoles());
126135
}
127136

128137
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): Response

Authentication/Authenticator/HttpBasicAuthenticator.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
use Symfony\Component\HttpFoundation\Request;
1616
use Symfony\Component\HttpFoundation\Response;
1717
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
18+
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
1819
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
1920
use Symfony\Component\Security\Core\Exception\AuthenticationException;
2021
use Symfony\Component\Security\Core\User\UserInterface;
2122
use Symfony\Component\Security\Core\User\UserProviderInterface;
23+
use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface;
2224
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
2325

2426
/**
@@ -28,10 +30,8 @@
2830
* @final
2931
* @experimental in 5.1
3032
*/
31-
class HttpBasicAuthenticator implements AuthenticatorInterface, AuthenticationEntryPointInterface
33+
class HttpBasicAuthenticator implements AuthenticatorInterface, AuthenticationEntryPointInterface, PasswordAuthenticatedInterface
3234
{
33-
use UsernamePasswordTrait;
34-
3535
private $realmName;
3636
private $userProvider;
3737
private $encoderFactory;
@@ -67,11 +67,21 @@ public function getCredentials(Request $request)
6767
];
6868
}
6969

70+
public function getPassword($credentials): ?string
71+
{
72+
return $credentials['password'];
73+
}
74+
7075
public function getUser($credentials): ?UserInterface
7176
{
7277
return $this->userProvider->loadUserByUsername($credentials['username']);
7378
}
7479

80+
public function createAuthenticatedToken(UserInterface $user, $providerKey): TokenInterface
81+
{
82+
return new UsernamePasswordToken($user, null, $providerKey, $user->getRoles());
83+
}
84+
7585
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): ?Response
7686
{
7787
return null;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Symfony\Component\Security\Http\Authentication\Authenticator;
4+
5+
/**
6+
* This interface should be implemented when the authenticator
7+
* doesn't need to check credentials (e.g. when using API tokens)
8+
*
9+
* @author Wouter de Jong <wouter@wouterj.nl>
10+
*/
11+
interface TokenAuthenticatedInterface
12+
{
13+
/**
14+
* Extracts the token from the credentials.
15+
*
16+
* If you return null, the credentials will not be marked as
17+
* valid and a BadCredentialsException is thrown.
18+
*
19+
* @param mixed $credentials The user credentials
20+
*
21+
* @return mixed|null the token - if any - or null otherwise
22+
*/
23+
public function getToken($credentials);
24+
}

Authentication/Authenticator/UsernamePasswordTrait.php

Lines changed: 0 additions & 50 deletions
This file was deleted.

Authentication/GuardAuthenticationManager.php

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
namespace Symfony\Component\Security\Http\Authentication;
1313

1414
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
15+
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
16+
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
17+
use Symfony\Component\Security\Core\User\UserInterface;
1518
use Symfony\Component\Security\Http\Authentication\Authenticator\AuthenticatorInterface;
1619
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticationGuardToken;
1720
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
@@ -21,7 +24,7 @@
2124
use Symfony\Component\Security\Core\Exception\AuthenticationException;
2225
use Symfony\Component\Security\Core\Exception\AuthenticationExpiredException;
2326
use Symfony\Component\Security\Core\Exception\ProviderNotFoundException;
24-
use Symfony\Component\Security\Core\User\UserCheckerInterface;
27+
use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent;
2528
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
2629

2730
/**
@@ -35,18 +38,16 @@ class GuardAuthenticationManager implements AuthenticationManagerInterface
3538
use GuardAuthenticationManagerTrait;
3639

3740
private $guardAuthenticators;
38-
private $userChecker;
39-
private $eraseCredentials;
40-
/** @var EventDispatcherInterface */
4141
private $eventDispatcher;
42+
private $eraseCredentials;
4243

4344
/**
4445
* @param iterable|AuthenticatorInterface[] $guardAuthenticators The authenticators, with keys that match what's passed to GuardAuthenticationListener
4546
*/
46-
public function __construct($guardAuthenticators, UserCheckerInterface $userChecker, bool $eraseCredentials = true)
47+
public function __construct(iterable $guardAuthenticators, EventDispatcherInterface $eventDispatcher, bool $eraseCredentials = true)
4748
{
4849
$this->guardAuthenticators = $guardAuthenticators;
49-
$this->userChecker = $userChecker;
50+
$this->eventDispatcher = $eventDispatcher;
5051
$this->eraseCredentials = $eraseCredentials;
5152
}
5253

@@ -100,6 +101,40 @@ public function authenticate(TokenInterface $token)
100101
return $result;
101102
}
102103

104+
protected function getGuardKey(string $key): string
105+
{
106+
// Guard authenticators in the GuardAuthenticationManager are already indexed
107+
// by an unique key
108+
return $key;
109+
}
110+
111+
private function authenticateViaGuard(AuthenticatorInterface $authenticator, PreAuthenticationGuardToken $token, string $providerKey): TokenInterface
112+
{
113+
// get the user from the Authenticator
114+
$user = $authenticator->getUser($token->getCredentials());
115+
if (null === $user) {
116+
throw new UsernameNotFoundException(sprintf('Null returned from "%s::getUser()".', \get_class($authenticator)));
117+
}
118+
119+
if (!$user instanceof UserInterface) {
120+
throw new \UnexpectedValueException(sprintf('The %s::getUser() method must return a UserInterface. You returned %s.', \get_class($authenticator), \is_object($user) ? \get_class($user) : \gettype($user)));
121+
}
122+
123+
$event = new VerifyAuthenticatorCredentialsEvent($authenticator, $token, $user);
124+
$this->eventDispatcher->dispatch($event);
125+
if (true !== $event->areCredentialsValid()) {
126+
throw new BadCredentialsException(sprintf('Authentication failed because %s did not approve the credentials.', \get_class($authenticator)));
127+
}
128+
129+
// turn the UserInterface into a TokenInterface
130+
$authenticatedToken = $authenticator->createAuthenticatedToken($user, $providerKey);
131+
if (!$authenticatedToken instanceof TokenInterface) {
132+
throw new \UnexpectedValueException(sprintf('The %s::createAuthenticatedToken() method must return a TokenInterface. You returned %s.', \get_class($authenticator), \is_object($authenticatedToken) ? \get_class($authenticatedToken) : \gettype($authenticatedToken)));
133+
}
134+
135+
return $authenticatedToken;
136+
}
137+
103138
private function handleFailure(AuthenticationException $exception, TokenInterface $token)
104139
{
105140
if (null !== $this->eventDispatcher) {
@@ -110,11 +145,4 @@ private function handleFailure(AuthenticationException $exception, TokenInterfac
110145

111146
throw $exception;
112147
}
113-
114-
protected function getGuardKey(string $key): string
115-
{
116-
// Guard authenticators in the GuardAuthenticationManager are already indexed
117-
// by an unique key
118-
return $key;
119-
}
120148
}

0 commit comments

Comments
 (0)