Skip to content
This repository was archived by the owner on May 31, 2024. It is now read-only.

Commit 4a5dea2

Browse files
committed
feature #14673 New Guard Authentication System (e.g. putting the joy back into security) (weaverryan)
This PR was merged into the 2.8 branch. Discussion ---------- New Guard Authentication System (e.g. putting the joy back into security) | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | at least partially: #14300, #11158, #11451, #10035, #10463, #8606, probably more | License | MIT | Doc PR | symfony/symfony-docs#5265 Hi guys! Though it got much easier in 2.4 with `pre_auth`, authentication is a pain in Symfony. This introduces a new authentication provider called guard, with one goal in mind: put everything you need for *any* authentication system into one spot. ### How it works With guard, you can perform custom authentication just by implementing the [GuardAuthenticatorInterface](https://github.com/weaverryan/symfony/blob/guard/src/Symfony/Component/Security/Guard/GuardAuthenticatorInterface.php) and registering it as a service. It has methods for every part of a custom authentication flow I can think of. For a working example, see https://github.com/weaverryan/symfony-demo/tree/guard-auth. This uses 2 authenticators simultaneously, creating a system that handles [form login](https://github.com/weaverryan/symfony-demo/blob/guard-auth/src/AppBundle/Security/FormLoginAuthenticator.php) and [api token auth](https://github.com/weaverryan/symfony-demo/blob/guard-auth/src/AppBundle/Security/TokenAuthenticator.php) with a respectable amount of code. The [security.yml](https://github.com/weaverryan/symfony-demo/blob/guard-auth/app/config/security.yml) is also quite simple. This also supports "manual login" without jumping through hoops: https://github.com/weaverryan/symfony-demo/blob/guard-auth/src/AppBundle/Controller/SecurityController.php#L45 I've also tested with "remember me" and "switch user" - no problems with either. I hope you like it :). ### What's Needed 1) **Other Use-Cases?**: Please think about the code and try it. What use-cases are we *not* covering? I want Guard to be simple, but cover the 99.9% use-cases. 2) **Remember me** functionality cannot be triggered via manual login. That's true now, and it's not fixed, and it's tricky. ### Deprecations? This is a new feature, so no deprecations. But, creating a login form with a guard authenticator is a whole heck of a lot easier to understand than `form_login` or even `simple_form`. In a perfect world, we'd either deprecate those or make them use "guard" internally so that we have just **one** way of performing authentication. Thanks! Commits ------- a01ed35 Adding the necessary files so that Guard can be its own installable component d763134 Removing unnecessary override e353833 fabbot dd485f4 Adding a new exception and throwing it when the User changes 302235e Fixing a bug where having an authentication failure would log you out. 396a162 Tweaks thanks to Wouter c9d9430 Adding logging on this step and switching the order - not for any huge reason 31f9cae Adding a base class to assist with form login authentication 0501761 Allowing for other authenticators to be checked 293c8a1 meaningless author and license changes 81432f9 Adding missing factory registration 7a94994 Thanks again fabbot! 7de05be A few more changes thanks to @iltar ffdbc66 Splitting the getting of the user and checking credentials into two steps 6edb9e1 Tweaking docblock on interface thanks to @iltar d693721 Adding periods at the end of exceptions, and changing one class name to LogicException thanks to @iltar eb158cb Updating interface method per suggestion - makes sense to me, Request is redundant c73c32e Thanks fabbot! 6c180c7 Adding an edge case - this should not happen anyways 180e2c7 Properly handles "post auth" tokens that have become not authenticated 873ed28 Renaming the tokens to be clear they are "post" and "pre" auth - also adding an interface a0bceb4 adding Guard tests 05af97c Initial commit (but after some polished work) of the new Guard authentication system 330aa7f Improving phpdoc on AuthenticationEntryPointInterface so people that implement this understand it
2 parents 8c2b94f + 038c729 commit 4a5dea2

20 files changed

+1587
-3
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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\Core\Exception;
13+
14+
/**
15+
* AuthenticationServiceException is thrown when an authenticated token becomes un-authentcated between requests.
16+
*
17+
* In practice, this is due to the User changing between requests (e.g. password changes),
18+
* causes the token to become un-authenticated.
19+
*
20+
* @author Ryan Weaver <ryan@knpuniversity.com>
21+
*/
22+
class AuthenticationExpiredException extends AccountStatusException
23+
{
24+
/**
25+
* {@inheritdoc}
26+
*/
27+
public function getMessageKey()
28+
{
29+
return 'Authentication expired because your account information has changed.';
30+
}
31+
}

Guard/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
vendor/
2+
composer.lock
3+
phpunit.xml

Guard/AbstractGuardAuthenticator.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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\Guard;
13+
14+
use Symfony\Component\Security\Core\User\UserInterface;
15+
use Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken;
16+
17+
/**
18+
* An optional base class that creates a PostAuthenticationGuardToken for you.
19+
*
20+
* @author Ryan Weaver <ryan@knpuniversity.com>
21+
*/
22+
abstract class AbstractGuardAuthenticator implements GuardAuthenticatorInterface
23+
{
24+
/**
25+
* Shortcut to create a PostAuthenticationGuardToken for you, if you don't really
26+
* care about which authenticated token you're using.
27+
*
28+
* @param UserInterface $user
29+
* @param string $providerKey
30+
*
31+
* @return PostAuthenticationGuardToken
32+
*/
33+
public function createAuthenticatedToken(UserInterface $user, $providerKey)
34+
{
35+
return new PostAuthenticationGuardToken(
36+
$user,
37+
$providerKey,
38+
$user->getRoles()
39+
);
40+
}
41+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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\Guard\Authenticator;
13+
14+
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
15+
use Symfony\Component\HttpFoundation\RedirectResponse;
16+
use Symfony\Component\HttpFoundation\Request;
17+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
18+
use Symfony\Component\Security\Core\Exception\AuthenticationException;
19+
use Symfony\Component\Security\Core\Security;
20+
21+
/**
22+
* A base class to make form login authentication easier!
23+
*
24+
* @author Ryan Weaver <ryan@knpuniversity.com>
25+
*/
26+
abstract class AbstractFormLoginAuthenticator extends AbstractGuardAuthenticator
27+
{
28+
/**
29+
* Return the URL to the login page.
30+
*
31+
* @return string
32+
*/
33+
abstract protected function getLoginUrl();
34+
35+
/**
36+
* The user will be redirected to the secure page they originally tried
37+
* to access. But if no such page exists (i.e. the user went to the
38+
* login page directly), this returns the URL the user should be redirected
39+
* to after logging in successfully (e.g. your homepage).
40+
*
41+
* @return string
42+
*/
43+
abstract protected function getDefaultSuccessRedirectUrl();
44+
45+
/**
46+
* Override to change what happens after a bad username/password is submitted.
47+
*
48+
* @param Request $request
49+
* @param AuthenticationException $exception
50+
*
51+
* @return RedirectResponse
52+
*/
53+
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
54+
{
55+
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
56+
$url = $this->getLoginUrl();
57+
58+
return new RedirectResponse($url);
59+
}
60+
61+
/**
62+
* Override to change what happens after successful authentication.
63+
*
64+
* @param Request $request
65+
* @param TokenInterface $token
66+
* @param string $providerKey
67+
*
68+
* @return RedirectResponse
69+
*/
70+
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
71+
{
72+
// if the user hit a secure page and start() was called, this was
73+
// the URL they were on, and probably where you want to redirect to
74+
$targetPath = $request->getSession()->get('_security.'.$providerKey.'.target_path');
75+
76+
if (!$targetPath) {
77+
$targetPath = $this->getDefaultSuccessRedirectUrl();
78+
}
79+
80+
return new RedirectResponse($targetPath);
81+
}
82+
83+
public function supportsRememberMe()
84+
{
85+
return true;
86+
}
87+
88+
/**
89+
* Override to control what happens when the user hits a secure page
90+
* but isn't logged in yet.
91+
*
92+
* @param Request $request
93+
* @param AuthenticationException|null $authException
94+
*
95+
* @return RedirectResponse
96+
*/
97+
public function start(Request $request, AuthenticationException $authException = null)
98+
{
99+
$url = $this->getLoginUrl();
100+
101+
return new RedirectResponse($url);
102+
}
103+
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
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\Guard\Firewall;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpFoundation\Response;
16+
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
17+
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
18+
use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken;
19+
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
20+
use Symfony\Component\Security\Guard\GuardAuthenticatorInterface;
21+
use Psr\Log\LoggerInterface;
22+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
23+
use Symfony\Component\Security\Core\Exception\AuthenticationException;
24+
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
25+
use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
26+
27+
/**
28+
* Authentication listener for the "guard" system.
29+
*
30+
* @author Ryan Weaver <ryan@knpuniversity.com>
31+
*/
32+
class GuardAuthenticationListener implements ListenerInterface
33+
{
34+
private $guardHandler;
35+
private $authenticationManager;
36+
private $providerKey;
37+
private $guardAuthenticators;
38+
private $logger;
39+
private $rememberMeServices;
40+
41+
/**
42+
* @param GuardAuthenticatorHandler $guardHandler The Guard handler
43+
* @param AuthenticationManagerInterface $authenticationManager An AuthenticationManagerInterface instance
44+
* @param string $providerKey The provider (i.e. firewall) key
45+
* @param GuardAuthenticatorInterface[] $guardAuthenticators The authenticators, with keys that match what's passed to GuardAuthenticationProvider
46+
* @param LoggerInterface $logger A LoggerInterface instance
47+
*/
48+
public function __construct(GuardAuthenticatorHandler $guardHandler, AuthenticationManagerInterface $authenticationManager, $providerKey, array $guardAuthenticators, LoggerInterface $logger = null)
49+
{
50+
if (empty($providerKey)) {
51+
throw new \InvalidArgumentException('$providerKey must not be empty.');
52+
}
53+
54+
$this->guardHandler = $guardHandler;
55+
$this->authenticationManager = $authenticationManager;
56+
$this->providerKey = $providerKey;
57+
$this->guardAuthenticators = $guardAuthenticators;
58+
$this->logger = $logger;
59+
}
60+
61+
/**
62+
* Iterates over each authenticator to see if each wants to authenticate the request.
63+
*
64+
* @param GetResponseEvent $event
65+
*/
66+
public function handle(GetResponseEvent $event)
67+
{
68+
if (null !== $this->logger) {
69+
$this->logger->info('Checking for guard authentication credentials.', array('firewall_key' => $this->providerKey, 'authenticators' => count($this->guardAuthenticators)));
70+
}
71+
72+
foreach ($this->guardAuthenticators as $key => $guardAuthenticator) {
73+
// get a key that's unique to *this* guard authenticator
74+
// this MUST be the same as GuardAuthenticationProvider
75+
$uniqueGuardKey = $this->providerKey.'_'.$key;
76+
77+
$this->executeGuardAuthenticator($uniqueGuardKey, $guardAuthenticator, $event);
78+
}
79+
}
80+
81+
private function executeGuardAuthenticator($uniqueGuardKey, GuardAuthenticatorInterface $guardAuthenticator, GetResponseEvent $event)
82+
{
83+
$request = $event->getRequest();
84+
try {
85+
if (null !== $this->logger) {
86+
$this->logger->info('Calling getCredentials on guard configurator.', array('firewall_key' => $this->providerKey, 'authenticator' => get_class($guardAuthenticator)));
87+
}
88+
89+
// allow the authenticator to fetch authentication info from the request
90+
$credentials = $guardAuthenticator->getCredentials($request);
91+
92+
// allow null to be returned to skip authentication
93+
if (null === $credentials) {
94+
return;
95+
}
96+
97+
// create a token with the unique key, so that the provider knows which authenticator to use
98+
$token = new PreAuthenticationGuardToken($credentials, $uniqueGuardKey);
99+
100+
if (null !== $this->logger) {
101+
$this->logger->info('Passing guard token information to the GuardAuthenticationProvider', array('firewall_key' => $this->providerKey, 'authenticator' => get_class($guardAuthenticator)));
102+
}
103+
// pass the token into the AuthenticationManager system
104+
// this indirectly calls GuardAuthenticationProvider::authenticate()
105+
$token = $this->authenticationManager->authenticate($token);
106+
107+
if (null !== $this->logger) {
108+
$this->logger->info('Guard authentication successful!', array('token' => $token, 'authenticator' => get_class($guardAuthenticator)));
109+
}
110+
111+
// sets the token on the token storage, etc
112+
$this->guardHandler->authenticateWithToken($token, $request);
113+
} catch (AuthenticationException $e) {
114+
// oh no! Authentication failed!
115+
116+
if (null !== $this->logger) {
117+
$this->logger->info('Guard authentication failed.', array('exception' => $e, 'authenticator' => get_class($guardAuthenticator)));
118+
}
119+
120+
$response = $this->guardHandler->handleAuthenticationFailure($e, $request, $guardAuthenticator, $this->providerKey);
121+
122+
if ($response instanceof Response) {
123+
$event->setResponse($response);
124+
}
125+
126+
return;
127+
}
128+
129+
// success!
130+
$response = $this->guardHandler->handleAuthenticationSuccess($token, $request, $guardAuthenticator, $this->providerKey);
131+
if ($response instanceof Response) {
132+
if (null !== $this->logger) {
133+
$this->logger->info('Guard authenticator set success response.', array('response' => $response, 'authenticator' => get_class($guardAuthenticator)));
134+
}
135+
136+
$event->setResponse($response);
137+
} else {
138+
if (null !== $this->logger) {
139+
$this->logger->info('Guard authenticator set no success response: request continues.', array('authenticator' => get_class($guardAuthenticator)));
140+
}
141+
}
142+
143+
// attempt to trigger the remember me functionality
144+
$this->triggerRememberMe($guardAuthenticator, $request, $token, $response);
145+
}
146+
147+
/**
148+
* Should be called if this listener will support remember me.
149+
*
150+
* @param RememberMeServicesInterface $rememberMeServices
151+
*/
152+
public function setRememberMeServices(RememberMeServicesInterface $rememberMeServices)
153+
{
154+
$this->rememberMeServices = $rememberMeServices;
155+
}
156+
157+
/**
158+
* Checks to see if remember me is supported in the authenticator and
159+
* on the firewall. If it is, the RememberMeServicesInterface is notified.
160+
*
161+
* @param GuardAuthenticatorInterface $guardAuthenticator
162+
* @param Request $request
163+
* @param TokenInterface $token
164+
* @param Response $response
165+
*/
166+
private function triggerRememberMe(GuardAuthenticatorInterface $guardAuthenticator, Request $request, TokenInterface $token, Response $response = null)
167+
{
168+
if (null === $this->rememberMeServices) {
169+
if (null !== $this->logger) {
170+
$this->logger->info('Remember me skipped: it is not configured for the firewall.', array('authenticator' => get_class($guardAuthenticator)));
171+
}
172+
173+
return;
174+
}
175+
176+
if (!$guardAuthenticator->supportsRememberMe()) {
177+
if (null !== $this->logger) {
178+
$this->logger->info('Remember me skipped: your authenticator does not support it.', array('authenticator' => get_class($guardAuthenticator)));
179+
}
180+
181+
return;
182+
}
183+
184+
if (!$response instanceof Response) {
185+
throw new \LogicException(sprintf(
186+
'%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.',
187+
get_class($guardAuthenticator)
188+
));
189+
}
190+
191+
$this->rememberMeServices->loginSuccess($request, $response, $token);
192+
}
193+
}

0 commit comments

Comments
 (0)