-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
New Guard Authentication System (e.g. putting the joy back into security) #14673
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
||
// check the AdvancedUserInterface methods! | ||
$this->userChecker->checkPreAuth($user); | ||
$this->userChecker->checkPostAuth($user); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't the checkPostAuth()
be done after createAuthenticatedToken()
?
I have a small concern regarding the user checker though. Say that before querying anything to find a user, I want to check if the IP has a time out, how would I do it here?
In the old situation I would have created my own simple-authenticator which did this. After this PR, I'd have to put it in a GuardAuthenticatorInterface
. Neither of those solutions seems feasible for me as I would have to create either a weird inheritance tree or duplicate code in my authenticators. It would be great for you (and others ofc!) to think with me on the following points:
- Would it be possible to use an actual FormType (with optional validation) here?
- Would it be possible to implement something related to symfony/security - tagging UserCheckerInterface #11090?
Regarding those points, after an x amount of failed attempts, I have to show a captcha which has to be displayed until either valid or another x attempts have failed. This bundle would add the 'captcha' type to a form which you can then re-use for both the validation and display. I want to do this before the authentication takes place (why query the database for a blocked IP). https://github.com/Gregwar/CaptchaBundle
Just a random brainfart
I can also imagine the possibility to automate certain validation points by using a FormType with constraints; With a data transformer to put the user in there automatically via a user provider based on the username.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A lot here :)
-
Actually,
checkPostAuth()
is right, butcheckPreAuth()
was wrong - it should happen right after fetching the User, but before checking credentials (seeUserAuthenticationProvider
). I've just split a method on the interface to make this possible and moved thecheckPreAuth()
in the provider. Thanks for asking about this - I hadn't really realized I had things in the wrong spot. -
About the IP timeout idea, was this simpler with the simple-authenticator? Or are you saying that in both cases, you'd need to use inheritance or duplication to put the code into the "simple authenticator" or the "guard authenticator", so it's the same, but no better? I just commented on symfony/security - tagging UserCheckerInterface #11090 - I think it can be solved there.
-
We should avoid using forms or validation anywhere. But more importantly, the "guard authenticators" are so simple that you can do whatever you want, including using forms and validation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding point 2, It will be similar. In my case I've actually implement a custom UserChecker, which I manually had to inject (can't configure that at all). I've made a custom method called checkPreUserLoad()
in a custom UserCheckerInterface
implementation and created an abstract authenticator that all our different authentication classes (simple-form, simple-pre-auth) extend. In your implementation, I would probably put half of this inside the part where you get the values from the request and pass the captcha valid/invalid/not required from there.
I was wondering if it would be possible to tackle that problem while we are at it.
public function checkPreUserLoad()
{
$request = $this->request_stack->getCurrentRequest();
$resolved = $this->login_ip_resolver->resolveStatus($request->getClientIp());
if ($resolved->requiresBlock()) {
throw new BlockThresholdExceededException('Too many failed login attempts, ip blocked');
}
if (!$resolved->requiresCaptcha()) {
return;
}
// captcha is required
$form = $this->form_factory->create('login', null, ['add_captcha' => true]);
$form->handleRequest($request);
// only check the captcha, there is currently no username/password validation
if ($request->request->has('login') && $form->has('captcha')) {
if ($form->isSubmitted() && !$form->get('captcha')->isValid()) {
throw new InvalidCaptchaException('Provided captcha was not valid');
}
// captcha is valid, we can skip it
return;
}
throw new CaptchaThresholdExceededException('Too many failed login attempts, captcha required');
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would the user checker stuff in #11090 solve this? If not, what do you propose? This looks like a lot of business logic to me, and putting that either into the user checker or somewhere in the authenticator (or the authenticator calls out to another service which holds the logic) seems right and simple to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the solution I proposed in #11090 will in fact solve this issue. That is Backwards compatible, just need to modify the configuration for this factory.
Eventually I can use a service decorator to replace the security.user_checker
and voila, everything magically works without needing to change this code.
I really love this PR! 👍 It makes things a lot simpler. As you can see I have already worked my way through the changes to comment. It's really nice to see the difference between the pre and post authentication tokens. Edit: also fixes #14300 |
👍 congrats @weaverryan! It's a very nice move towards reducing perceived complexity of the Security component. I'd like to make two quick comments regarding the naming and code of some methods: 1) Are you planning to add new methods to get credentials from different sources than the // redundant method name?
public function getCredentialsFromRequest(Request $request) { ... }
// acceptable alternative?
public function getCredentials(Request $request) { ... } 2) The autoLogin() code looks very convoluted to me. In an ideal world, I'd like to do the following: $security->login($user); I know that we can't do that for Symfony, but could we simplify that code as much as possible? |
* @param GuardAuthenticatorInterface[] $guardAuthenticators The authenticators, with keys that match what's passed to GuardAuthenticationProvider | ||
* @param LoggerInterface $logger A LoggerInterface instance | ||
*/ | ||
public function __construct(GuardAuthenticatorHandler $guardHandler, AuthenticationManagerInterface $authenticationManager, $providerKey, $guardAuthenticators, LoggerInterface $logger = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the docblock, $guardAuthenticators
is an array of GuardAuthenticatorInterface
. Should the argument be array $guardAuthenticators
, or is this left out intentionally?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not intentional at all - thanks for catching it
Thanks! |
* @param GuardAuthenticatorInterface[] $guardAuthenticators The authenticators, with keys that match what's passed to GuardAuthenticationProvider | ||
* @param LoggerInterface $logger A LoggerInterface instance | ||
*/ | ||
public function __construct(GuardAuthenticatorHandler $guardHandler, AuthenticationManagerInterface $authenticationManager, $providerKey, array $guardAuthenticators, LoggerInterface $logger = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there anything preventing you from using a NullLogger
instance if not provided ? In order to avoid such things:
if (null !== $this->logger) {
...
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nothing technical preventing that, but NullLogger
isn't used anywhere in core currently - having an optional argument like this is used :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I think he means is the following:
$this->logger = $logger ?: new NullLogger();
IMO this is a cleaner solution as you don't have to worry about if($this->logger)
statements. You can just log away safely.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #14682
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agree with @iltar it does simplify a little bit IMO
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👎 for a NullLogger, Symfony should take options for most performance.
Wow ! This looks great ! 👍 |
|
||
public function setAuthenticated($authenticated) | ||
{ | ||
throw new \LogicException('The PreAuthenticationGuardToken is *always* not authenticated.'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know it's quite common in English, but being dutch, "always not" is very confusing for me. What about replacing it with "never"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either is *never* authenticated
, or is *always* unauthenticated
makes more sense to me.
private function triggerRememberMe(GuardAuthenticatorInterface $guardAuthenticator, Request $request, TokenInterface $token, Response $response = null) | ||
{ | ||
if (!$guardAuthenticator->supportsRememberMe()) { | ||
return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should you not inform that the guard doesn't support rembember-me? And do this check after checking if remember-me was actually enabled for the firewall.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds reasonable to me :) making that change
great work 👍 |
very nice 👍 |
I waited for this so long! Hope it gets into core ASAP |
Well done! This was one of the most lacked features in symfony until now. |
Very nice! In the beginning of learning Symfony the authentication was not easy for me! This makes it so much easier for new users! |
…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
The subtree-split is now active for this repository and available here: https://github.com/symfony/security-guard |
@weaverryan Symfony 2.7 compatibility in composer.json wouldn't be too bad, so I could try out the guard in an existing application without pulling the entire framework from an unstable branch. Apart from that: 👏 Great work, I'm glad to see this feature being merged. 😃 |
The unit tests pass, if I pull every required Symfony component from 2.7.4 but security-core. As far as I can tell, the only reason why security-core has to be But what you can probably do is setting security-http to |
@weaverryan Great job! |
thanks @fabpot 👍 |
$this->event = null; | ||
$this->logger = null; | ||
$this->request = null; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing $this->rememberMeServices = null;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
addressed in #15920
…stof) This PR was merged into the 2.8 branch. Discussion ---------- Add the replace rules for the security-guard component | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | n/a | License | MIT | Doc PR | n/a The update of composer replacements was forgotten in #14673 Commits ------- 5ef8abc Add the replace rules for the security-guard component
This PR was merged into the 2.8 branch. Discussion ---------- Guard minor tweaks | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | n/a | License | MIT | Doc PR | n/a Various completely minor things, most from suggestions on #14673 Commits ------- 869d5a7 tweaking message related to configuration edge case that we want to be helpful with da4758a Minor tweaks - lowering the required security-http requirement and nulling out a test field
Nice! Would this also allow "simple" configuration via annotations? #13950 |
…tar) This PR was squashed before being merged into the 2.8 branch (closes #14721). Discussion ---------- [Security] Configuring a user checker per firewall _Changed my base branch to avoid issues, closed old PR_ | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed ticket | #11090 and helps #14673 | License | MIT | Doc PR | symfony/symfony-docs/pull/5530 This pull request adds support for a configurable user checker per firewall. An example could be: ```yml services: app.user_checker: class: App\Security\UserChecker arguments: - "@request_stack" security: firewalls: secured_area: pattern: ^/ anonymous: ~ basic_auth: ~ user_checker: app.user_checker ``` The above example will use the `UserChecker` defined as `app.user_checker`. If the `user_checker` option is left empty, `security.user_checker` will be used. If the `user_checkers` option is not defined, it will fall back to the original behavior to not break backwards compatibility and will validate using the existing `UserChecker`: `security.user_checker`. I left the default argument in the service definitions to be `security.user_checker` to include backwards compatibility for people who for some reason don't have the extension executed. You can obtain the checker for a specific firewall by appending the firewall name to it. For the firewall `secured_area`, this would be `security.user_checker.secured_area`. Commits ------- 76bc662 [Security] Configuring a user checker per firewall
@weaverryan Super simple! Love it! |
* | ||
* @throws AuthenticationException | ||
*/ | ||
public function checkCredentials($credentials, UserInterface $user); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It makes be nervous that this method, by default, results in a user being successfully authenticated and requires throwing an exception in the case that the user should NOT be authenticated. In the case that a developer accidentally writes a bug that doesn't throw an exception in a complex (or any) setup, the mistake results in _users being authenticated that shouldn't have been._
I think a safer way to do this would be to explicitly return a value (perhaps a boolean) indicating whether the credential check was successful or not. In the GuardAuthenticationProvider
you could very easily check this boolean value for falseness (in the case that someone doesn't return) and then throw an AuthenticationException
to keep the rest of the logic in the Provider the same.
TLDR; Which is worse? Authenticating a user who has invalid credentials? Or not authenticating a user who has valid credentials? I guess I'd rather not take a chance with security. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I agree - the same could be said for returning false
. For example, if you collect the result in a boolean variable $valid
which is returned, that variable could easily have the wrong truth value simply by mistaking &&
for ||
somewhere in your code.
The only way to prevent bugs like the one you described is by properly testing the authenticator. I don't think that throwing an exception vs. returning false
makes any difference here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see #16395
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Heh, maybe I should look at the tip of 2.8 before I go commenting on old PRs. Sorry everyone.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@patrick-mcdougle No, thank you! I was trying to find you at the conference - I made a pull request within an hour after your asked this very good question - and wanted to tell you about it :).
Imo this is a developer mistake and tests should've caught that. There's already a weird case with As long as it's stated that an exception should be thrown in the docs: 👎 from my side for this request. Maybe result objects are a better suggestion, where you can specify a reason. It would conflict with the current flow of |
…eaverryan) This PR was merged into the 2.8 branch. Discussion ---------- Documentation for the new Guard authentication style | Q | A | ------------- | --- | Doc fix? | no | New docs? | yes symfony/symfony#14673 | Applies to | 2.8+ | Fixed tickets | n/a Hi guys! This is a WIP documentation for a proposed new authentication system. I've written just enough so people can understand how to use it, but will finish it later once the code has gotten reviewed. Thanks! Commits ------- 51720c7 Many fixes thanks to great review from ogizanagi, javiereguiluz and others 4752d4c adding one clarifying message 9782ff1 adding toc entries 62dcae3 Using JsonResponse + cleanup 440fe6f revamping Guard article bfce91b Fixing minor comments 9e411fe I'm extending the abstract class - so mention that. Also adding anonymous ac107c7 WIP documentation for the new guard auth
…okbook documentation (mheki) This PR was squashed before being merged into the 2.8 branch (closes #5886). Discussion ---------- [2.8] Add "How to Use Multiple Guard Authenticators" cookbook documentation | Q | A | ------------- | --- | Doc fix? | no | New docs? | yes (symfony/symfony#14673) | Applies to | `2.8` onwards Hi guys, this is my first contribution to the symfony docs. During my preparations for the Symfony Guard component workshops I have spent some time trying to figure out the problem described here. I hope this cookbook entry will help others save their time. cc @weaverryan Thanks! Commits ------- 121196d [2.8] Add "How to Use Multiple Guard Authenticators" cookbook documentation
Question in your working example shouldn't be return true if else?
To
Because i think it will always hit the user with a BadCredentialsException() |
@bogdaniel this behaviour was changed after this PR (but before releasing 2.8.0) it seems like the demo hasn't been updated. |
@wouterj This shouldn't be the place to maintain an up-to-date demo for this feature. imho, the PR should be seen as a documentation of the development process and the demo shows how the feature has worked at the time when the PR was merged. @bogdaniel: Please see the docs for an up-to-date documentation on this feature: http://symfony.com/doc/current/cookbook/security/guard-authentication.html |
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 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 and api token auth with a respectable amount of code. The 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
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.
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 evensimple_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!