10BC0 Refactor to an event based authentication approach · symfony/symfony@999ec27 · GitHub
[go: up one dir, main page]

Skip to content

Commit 999ec27

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 b14a5e8 commit 999ec27

26 files changed

+874
-316
lines changed

src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public function createGuard(ContainerBuilder $container, string $id, array $conf
106106
->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.form_login'))
107107
->replaceArgument(1, isset($config['csrf_token_generator']) ? new Reference($config['csrf_token_generator']) : null)
108108
->replaceArgument(2, new Reference($userProviderId))
109-
->replaceArgument(4, $options);
109+
->replaceArgument(3, $options);
110110

111111
return $authenticatorId;
112112
}

src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -419,16 +419,13 @@ private function createFirewall(ContainerBuilder $container, string $id, array $
419419
$configuredEntryPoint = isset($firewall['entry_point']) ? $firewall['entry_point'] : null;
420420

421421
if ($this->guardAuthenticationManagerEnabled) {
422-
// guard authentication manager listener (must be before calling createAuthenticationListeners() to inject remember me services)
422+
// Remember me listener (must be before calling createAuthenticationListeners() to inject remember me services)
423423
$container
424-
->setDefinition('security.firewall.guard.'.$id, new ChildDefinition('security.firewall.guard'))
425-
->replaceArgument(2, new Reference('security.firewall.guard.'.$id.'.locator'))
426-
->replaceArgument(3, $id)
427-
->addTag('kernel.event_listener', ['event' => KernelEvents::REQUEST])
424+
->setDefinition('security.listener.remember_me.'.$id, new ChildDefinition('security.listener.remember_me'))
425+
->replaceArgument(0, $id)
426+
->addTag('kernel.event_subscriber')
428427
->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none'])
429428
;
430-
431-
$listeners[] = new Reference('security.firewall.guard.'.$id);
432429
}
433430

434431
// Authentication listeners
@@ -438,18 +435,23 @@ private function createFirewall(ContainerBuilder $container, string $id, array $
438435
$authenticationProviders = array_merge($authenticationProviders, $firewallAuthenticationProviders);
439436

440437
if ($this->guardAuthenticationManagerEnabled) {
441-
// add authentication providers for this firewall to the GuardManagerListener (if guard is enabled)
438+
// guard authentication manager listener
442439
$container
443440
->setDefinition('security.firewall.guard.'.$id.'.locator', new ChildDefinition('security.firewall.guard.locator'))
444441
->setArguments([array_map(function ($id) {
445442
return new Reference($id);
446443
}, $firewallAuthenticationProviders)])
447444
->addTag('container.service_locator')
448445
;
446+
449447
$container
450-
->getDefinition('security.firewall.guard.'.$id)
448+
->setDefinition('security.firewall.guard.'.$id, new ChildDefinition('security.firewall.guard'))
451449
->replaceArgument(2, new Reference('security.firewall.guard.'.$id.'.locator'))
450+
->replaceArgument(3, $id)
451+
->addTag('kernel.event_listener', ['event' => KernelEvents::REQUEST])
452452
;
453+
454+
$listeners[] = new Reference('security.firewall.guard.'.$id);
453455
}
454456

455457
$config->replaceArgument(7, $configuredEntryPoint ?: $defaultEntryPoint);

src/Symfony/Bundle/SecurityBundle/EventListener/LazyGuardManagerListener.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Psr\Log\LoggerInterface;
1515
use Symfony\Component\DependencyInjection\ServiceLocator;
16+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
1617
use Symfony\Component\HttpFoundation\Request;
1718
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
1819
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
@@ -32,9 +33,10 @@ public function __construct(
3233
GuardAuthenticatorHandler $guardHandler,
3334
ServiceLocator $guardLocator,
3435
string $providerKey,
36+
EventDispatcherInterface $eventDispatcher,
3537
?LoggerInterface $logger = null
3638
) {
37-
parent::__construct($authenticationManager, $guardHandler, [], $providerKey, $logger);
39+
parent::__construct($authenticationManager, $guardHandler, [], $providerKey, $eventDispatcher, $logger);
3840

3941
$this->guardLocator = $guardLocator;
4042
}

src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,41 @@
1212
class="Symfony\Bundle\SecurityBundle\EventListener\LazyGuardManagerListener"
1313
abstract="true">
1414
<tag name="monolog.logger" channel="security" />
15-
<argument type="service" id="security.authentication.manager"/>
16-
<argument type="service" id="security.authentication.guard_handler"/>
15+
<argument type="service" id="security.authentication.manager" />
16+
<argument type="service" id="security.authentication.guard_handler" />
1717
<argument/> <!-- guard authenticator locator -->
1818
<argument/> <!-- provider key -->
19+
<argument type="service" id="event_dispatcher" />
1920
<argument type="service" id="logger" on-invalid="null" />
2021
</service>
2122

23+
<!-- Listeners -->
24+
25+
<service id="Symfony\Component\Security\Http\EventListener\AuthenticatingListener">
26+
<tag name="kernel.event_subscriber" />
27+
<argument type="service" id="security.encoder_factory" />
28+
</service>
29+
30+
<service id="Symfony\Component\Security\Http\EventListener\PasswordMigratingListener">
31+
<tag name="kernel.event_subscriber" />
32+
<argument type="service" id="security.encoder_factory" />
33+
</service>
34+
35+
<service id="Symfony\Component\Security\Http\EventListener\UserCheckerListener">
36+
<tag name="kernel.event_subscriber" />
37+
<argument type="service" id="Symfony\Component\Security\Core\User\UserCheckerInterface" />
38+
</service>
39+
40+
<service id="security.listener.remember_me"
41+
class="Symfony\Component\Security\Http\EventListener\RememberMeListener"
42+
abstract="true">
43+
<tag name="monolog.logger" channel="security" />
44+
<argument/> <!-- provider key -->
45+
<argument type="service" id="logger" on-invalid="null" />
46+
</service>
47+
48+
<!-- Authenticators -->
49+
2250
<service id="security.authenticator.http_basic"
2351
class="Symfony\Component\Security\Http\Authentication\Authenticator\HttpBasicAuthenticator"
2452
abstract="true">
@@ -34,14 +62,13 @@
3462
<argument type="service" id="security.http_utils" />
3563
<argument /> <!-- csrf token generator -->
3664
<argument type="abstract">user provider</argument>
37-
<argument type="service" id="security.encoder_factory" />
3865
<argument type="abstract">options</argument>
3966
</service>
4067

4168
<service id="security.authenticator.anonymous"
4269
class="Symfony\Component\Security\Http\Authentication\Authenticator\AnonymousAuthenticator"
4370
abstract="true">
44-
<argument /> <!-- secret -->
71+
<argument type="abstract">secret</argument>
4572
<argument type="service" id="security.token_storage" />
4673
</service>
4774
</services>

src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
</service>
5555
<service id="security.authentication.manager.guard" class="Symfony\Component\Security\Http\Authentication\GuardAuthenticationManager">
5656
<argument /> <!-- guard authenticators -->
57-
<argument type="service" id="Symfony\Component\Security\Core\User\UserCheckerInterface" /> <!-- User Checker -->
57+
<argument type="service" id="event_dispatcher" />
5858
<argument>%security.authentication.manager.erase_credentials%</argument>
5959
<call method="setEventDispatcher">
6060
<argument type="service" id="event_dispatcher" />

src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php

Lines changed: 116 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@
1313

1414
use Psr\Log\LoggerInterface;
1515
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\Response;
1617
use Symfony\Component\HttpKernel\Event\RequestEvent;
1718
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
1819
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticationGuardToken;
20+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
21+
use Symfony\Component\Security\Core\Exception\AuthenticationException;
1922
use Symfony\Component\Security\Guard\AuthenticatorInterface;
2023
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
2124
use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken as GuardPreAuthenticationGuardToken;
@@ -104,15 +107,122 @@ public function setRememberMeServices(RememberMeServicesInterface $rememberMeSer
104107
$this->rememberMeServices = $rememberMeServices;
105108
}
106109

107-
protected function createPreAuthenticatedToken($credentials, string $uniqueGuardKey, string $providerKey): PreAuthenticationGuardToken
110+
/**
111+
* @param AuthenticatorInterface[] $guardAuthenticators
112+
*/
113+
protected function executeGuardAuthenticators(array $guardAuthenticators, RequestEvent $event): void
108114
{
109-
return new GuardPreAuthenticationGuardToken($credentials, $uniqueGuardKey, $providerKey);
115+
foreach ($guardAuthenticators as $key => $guardAuthenticator) {
116+
$uniqueGuardKey = $this->providerKey.'_'.$key;;
117+
118+
$this->executeGuardAuthenticator($uniqueGuardKey, $guardAuthenticator, $event);
119+
120+
if ($event->hasResponse()) {
121+
if (null !== $this->logger) {
122+
$this->logger->debug('The "{authenticator}" authenticator set the response. Any later authenticator will not be called', ['authenticator' => \get_class($guardAuthenticator)]);
123+
}
124+
125+
break;
126+
}
127+
}
110128
}
111129

112-
protected function getGuardKey(string $key): string
130+
private function executeGuardAuthenticator(string $uniqueGuardKey, AuthenticatorInterface $guardAuthenticator, RequestEvent $event)
113131
{
114-
// get a key that's unique to *this* guard authenticator
115-
// this MUST be the same as GuardAuthenticationProvider
116-
return $this->providerKey.'_'.$key;
132+
$request = $event->getRequest();
133+
try {
134+
if (null !== $this->logger) {
135+
$this->logger->debug('Calling getCredentials() on guard authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]);
136+
}
137+
138+
// allow the authenticator to fetch authentication info from the request
139+
$credentials = $guardAuthenticator->getCredentials($request);
140+
141+
if (null === $credentials) {
142+
throw new \UnexpectedValueException(sprintf('The return value of "%1$s::getCredentials()" must not be null. Return false from "%1$s::supports()" instead.', get_debug_type($guardAuthenticator)));
143+
}
144+
145+
// create a token with the unique key, so that the provider knows which authenticator to use
146+
$token = $this->createPreAuthenticatedToken($credentials, $uniqueGuardKey, $this->providerKey);
147+
148+
if (null !== $this->logger) {
149+
$this->logger->debug('Passing guard token information to the GuardAuthenticationProvider', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]);
150+
}
151+
// pass the token into the AuthenticationManager system
152+
// this indirectly calls GuardAuthenticationProvider::authenticate()
153+
$token = $this->authenticationManager->authenticate($token);
154+
155+
if (null !== $this->logger) {
156+
$this->logger->info('Guard authentication successful!', ['token' => $token, 'authenticator' => \get_class($guardAuthenticator)]);
157+
}
158+
159+
// sets the token on the token storage, etc
160+
$this->guardHandler->authenticateWithToken($token, $request, $this->providerKey);
161+
} catch (AuthenticationException $e) {
162+
// oh no! Authentication failed!
163+
164+
if (null !== $this->logger) {
165+
$this->logger->info('Guard authentication failed.', ['exception' => $e, 'authenticator' => \get_class($guardAuthenticator)]);
166+
}
167+
168+
$response = $this->guardHandler->handleAuthenticationFailure($e, $request, $guardAuthenticator, $this->providerKey);
169+
170+
if ($response instanceof Response) {
171+
$event->setResponse($response);
172+
}
173+
174+
return;
175+
}
176+
177+
// success!
178+
$response = $this->guardHandler->handleAuthenticationSuccess($token, $request, $guardAuthenticator, $this->providerKey);
179+
if ($response instanceof Response) {
180+
if (null !== $this->logger) {
181+
$this->logger->debug('Guard authenticator set success response.', ['response' => $response, 'authenticator' => \get_class($guardAuthenticator)]);
182+
}
183+
184+
$event->setResponse($response);
185+
} else {
186+
if (null !== $this->logger) {
187+
$this->logger->debug('Guard authenticator set no success response: request continues.', ['authenticator' => \get_class($guardAuthenticator)]);
188+
}
189+
}
190+
191+
// attempt to trigger the remember me functionality
192+
$this->triggerRememberMe($guardAuthenticator, $request, $token, $response);
193+
}
194+
195+
protected function triggerRememberMe($guardAuthenticator, Request $request, TokenInterface $token, Response $response = null)
196+
{
197+
if (!$guardAuthenticator instanceof AuthenticatorInterface && !$guardAuthenticator instanceof CoreAuthenticatorInterface) {
198+
throw new \UnexpectedValueException('Invalid guard authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.');
199+
}
200+
201+
if (null === $this->rememberMeServices) {
202+
if (null !== $this->logger) {
203+
$this->logger->debug('Remember me skipped: it is not configured for the firewall.', ['authenticator' => \get_class($guardAuthenticator)]);
204+
}
205+
206+
return;
207+
}
208+
209+
if (!$guardAuthenticator->supportsRememberMe()) {
210+
if (null !== $this->logger) {
211+
$this->logger->debug('Remember me skipped: your authenticator does not support it.', ['authenticator' => \get_class($guardAuthenticator)]);
212+
}
213+
214+
return;
215+
}
216+
217+
if (!$response instanceof Response) {
218+
throw new \LogicException(sprintf('"%s::onAuthenticationSuccess()" *must* return a Response if you want to use the remember me functionality. Return a Response, or set remember_me to false under the guard configuration.', get_debug_type($guardAuthenticator)));
219+
}
220+
221+
$this->rememberMeServices->loginSuccess($request, $response, $token);
222+
}
223+
224+
protected function createPreAuthenticatedToken($credentials, string $uniqueGuardKey, string $providerKey): PreAuthenticationGuardToken
225+
{
226+
return new GuardPreAuthenticationGuardToken($credentials, $uniqueGuardKey, $providerKey);
117227
}
118228
}

src/Symfony/Component/Security/Guard/PasswordAuthenticatedInterface.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111

1212
namespace Symfony\Component\Security\Guard;
1313

14-
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
15-
1614
/**
1715
* An optional interface for "guard" authenticators that deal with user passwords.
1816
*/
@@ -24,6 +22,4 @@ interface PasswordAuthenticatedInterface
2422
* @param mixed $credentials The user credentials
2523
*/
2624
public function getPassword($credentials): ?string;
27-
28-
/* public function getPasswordEncoder(): ?UserPasswordEncoderInterface; */
2925
}

src/Symfony/Component C04E /Security/Guard/Provider/GuardAuthenticationProvider.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@
1111

1212
namespace Symfony\Component\Security\Guard\Provider;
1313

14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpFoundation\Response;
16+
use Symfony\Component\HttpKernel\Event\RequestEvent;
17+
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
18+
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
19+
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
20+
use Symfony\Component\Security\Core\User\UserInterface;
21+
use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface;
1422
use Symfony\Component\Security\Http\Authentication\GuardAuthenticationManagerTrait;
1523
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
1624
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
@@ -22,6 +30,7 @@
2230
use Symfony\Component\Security\Guard\AuthenticatorInterface;
2331
use Symfony\Component\Security\Guard\Token\GuardTokenInterface;
2432
use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken;
33+
use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
2534

2635
/**
2736
* Responsible for accepting the PreAuthenticationGuardToken and calling
@@ -41,6 +50,7 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface
4150
private $providerKey;
4251
private $userChecker;
4352
private $passwordEncoder;
53+
private $rememberMeServices;
4454

4555
/**
4656
* @param iterable|AuthenticatorInterface[] $guardAuthenticators The authenticators, with keys that match what's passed to GuardAuthenticationListener
@@ -106,8 +116,48 @@ public function supports(TokenInterface $token)
106116
return $token instanceof GuardTokenInterface;
107117
}
108118

119+
public function setRememberMeServices(RememberMeServicesInterface $rememberMeServices)
120+
{
121+
$this->rememberMeServices = $rememberMeServices;
122+
}
123+
109124
protected function getGuardKey(string $key): string
110125
{
111126
return $this->providerKey.'_'.$key;
112127
}
128+
129+
private function authenticateViaGuard(AuthenticatorInterface $guardAuthenticator, \Symfony\Component\Security\Core\Authentication\Token\PreAuthenticationGuardToken $token, string $providerKey): TokenInterface
130+
{
131+
// get the user from the GuardAuthenticator
132+
$user = $guardAuthenticator->getUser($token->getCredentials(), $this->userProvider);
133+
if (null === $user) {
134+
throw new UsernameNotFoundException(sprintf('Null returned from "%s::getUser()".', get_debug_type($guardAuthenticator)));
135+
}
136+
137+
if (!$user instanceof UserInterface) {
138+
throw new \UnexpectedValueException(sprintf('The "%s::getUser()" method must return a UserInterface. You returned "%s".', get_debug_type($guardAuthenticator), get_debug_type($user)));
139+
}
140+
141+
$this->userChecker->checkPreAuth($user);
142+
if (true !== $checkCredentialsResult = $guardAuthenticator->checkCredentials($token->getCredentials(), $user)) {
143+
if (false !== $checkCredentialsResult) {
144+
throw new \TypeError(sprintf('"%s::checkCredentials()" must return a boolean value.', get_debug_type($guardAuthenticator)));
145+
}
146+
147+
throw new BadCredentialsException(sprintf('Authentication failed because "%s::checkCredentials()" did not return true.', get_debug_type($guardAuthenticator)));
148+
}
149+
150+
if ($this->userProvider instanceof PasswordUpgraderInterface && $guardAuthenticator instanceof PasswordAuthenticatedInterface && null !== $this->passwordEncoder && (null !== $password = $guardAuthenticator->getPassword($token->getCredentials())) && method_exists($this->passwordEncoder, 'needsRehash') && $this->passwordEncoder->needsRehash($user)) {
151+
$this->userProvider->upgradePassword($user, $this->passwordEncoder->encodePassword($user, $password));
152+
}
153+
$this->userChecker->checkPostAuth($user);
154+
155+
// turn the UserInterface into a TokenInterface
156+
$authenticatedToken = $guardAuthenticator->createAuthenticatedToken($user, $providerKey);
157+
if (!$authenticatedToken instanceof TokenInterface) {
158+
throw new \UnexpectedValueException(sprintf('The "%s::createAuthenticatedToken()" method must return a TokenInterface. You returned "%s".', get_debug_type($guardAuthenticator), get_debug_type($authenticatedToken)));
159+
}
160+
161+
return $authenticatedToken;
162+
}
113163
}

0 commit comments

Comments
 (0)
0