Problem/Motivation
Comment in core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php says it all:
// Flood protection: this is very similar to the user login form code.
// @see \Drupal\user\Form\UserLoginForm::validateAuthentication()
This should be motivation enough to extract that security critical code to a place where it can be shared with the login form as well as with contrib authentication methods.
In core alone we already have 3 places that duplicate this security critical code:
core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
core/modules/user/src/Controller/UserAuthenticationController.php
core/modules/user/src/Form/UserLoginForm.php
It will be useful here as well #2339399: User edit form does not use flood control and allow for password brute force attacks
Proposed resolution
Create a new User AuthFloodService that combines all necessary security checks for user authentication.
* User name + password verification
* Check if user account is blocked
* Check flood control to avoid brute force attacks on passwords
Remaining tasks
Review patch.
User interface changes
None.
API changes
Only API addition: new UserAuthFlood service.
| Comment | File | Size | Author |
|---|---|---|---|
| #75 | user-flood-2431357-75.patch | 43.63 KB | Munavijayalakshmi |
| #74 | user-flood-2431357-74.patch | 44.07 KB | klausi |
| #51 | extract_a_reusable-2431357-51.patch | 31.82 KB | mr.baileys |
| #51 | interdiff.txt | 1.94 KB | mr.baileys |
Issue fork drupal-2431357
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #1
znerol commentedThis should be functionally equivalent with
HEAD. This brings downBasicAuth::authenticate()to ~10 lines of simple code.Comment #3
znerol commentedFix @count in Sorry, there have been more than @count failed login attempts for this account message.
Comment #4
vijaycs85Thanks @znerol. Looks very good. Here are few comments.
Nice! we don't need to duplicate basic_auth and contrib can extend LoginFlood, if overrides needed.
Just has loginFlood, but I do prefer UserLoginFlood.
ideally, @flood should be @flood.default_storage and alias of @flood.storage.db
Comment #5
berdir3. I don't think that is needed or how we do it elsewhere? https://www.drupal.org/node/2306083
Comment #6
znerol commentedLooking at
getAccountIdentifier, I wonder whether we could get rid of the entity manager dependency completely and instead just use$nameinstead of$account->id(). Is it really necessary/desirable that we only record flood-events for existing users?Comment #7
znerol commentedReroll.
Comment #8
dawehnerWe have here potentially a little bit more conservative ... given that authentication providers are constructors quite early. Better do it lazy, to avoid all kind of potential problems.
I curious whether the two methods could / should be moved into a common helper method. IsAllowedRequest($ip, $username) or something like that.
Should we name the service loginFlood?
I first thought it would be a good idea to move the namespace into the methods, but it turned out, just no, it would be horrible DX wise.
If you compare FloodInterface with LoginFloodInterface it almost seems to be as if it could be named IpAwareFloodInterface ... at least parts of it.
Comment #9
znerol commentedBasicAuth. Still the condition is so simple that I'd prefer to keep it like this also because then we do not need to add yet another method to the interface.windowandthresholdparameters. The current implementation intentionally is specific to login. It even frees user code from dealing with the configuration. In addition the interface is a great place for code comments turned into documentation.Comment #10
andypostisAllowedHost() looks more related to ban module, no idea now how to do that right
user_is_blocked looks the same as isAllowedAccount()
too much calls
seems easy to pass a readonly config object to constructor
I'd swap order of arguments to be more like result
this naming change breaks BC for the login form, better to change back
Comment #11
cilefen commentedThis would be useful for the user edit form as well. #2339399: User edit form does not use flood control and allow for password brute force attacks The issue summary should be updated accordingly.
Comment #12
znerol commentedReroll.
Comment #13
znerol commentedReroll.
Comment #14
znerol commentedReview in #10 gives me a hard time.
user_is_blocked()also works if the user entity is not loaded. Changing that toUserInterface::isActive()would mean loading the user entity before checking flood. We know from #2345611: Load user entity in Cookie AuthenticationProvider instead of using manual queries that this might have performance implications. Therefore I'd prefer to not change that in this patch but rather in the other one.flood_control_user_identifierform state is removed completely. Theflood_control_triggeredform state is not touched.Comment #15
znerol commentedGiven that #2339399: User edit form does not use flood control and allow for password brute force attacks wants to reuse the service introduced here, it is probably necessary to find a better name for the new class (and maybe address #10.1). The OWASP Guide to Authentication uses the term Threshold Governor for the facility we are trying to implement here. I did not find any class/interface in Symfony which serves that purpose (there are some third party bundles like this).
Comment #16
gregglesSince #2339399: User edit form does not use flood control and allow for password brute force attacks is major and is blocked on this, moving this to Major (right?).
Comment #17
znerol commentedI'm willing to continue the work here. Would be great to have feedback on the naming (see #15).
This basically protects against brute-forcing user credentials (not restricted to login). The current patch provides
\Drupal\user\Flood\UserLoginFlood. An easy first step would be to rename it to:Drupal\user\Flood\CredentialsCheckFlood.php.Comment #18
gregglesI think that CredentialCheckFlood is a good name for it since it is not only about Login.
Comment #19
znerol commentedRenamed the classes, services and changed the docs/comments.
Comment #20
znerol commentedComment #21
pwolanin commentedWe should also re-use this on the used edit form when the user enters their password - if they leave the account logged in, there is no brute-force protection
Comment #22
andypost@pwolanin +100 but maybe we need separate issue for that (scope is not clear)
Comment #23
greggles@pwolanin - is that #2339399: User edit form does not use flood control and allow for password brute force attacks ?
Comment #24
cilefen commented@greggles Yes.
Comment #26
neclimdulThis looks pretty good! Dug pretty deep through the changes and things almost identical just refactored which is great. Have a couple concerns though.
isAllowedAccount() calls getAccountIdentifier() does an uncached property lookup and then both the clearAccount() and register() call it again and do another uncached lookup of the same account. That means on both the pass and fail path we do at least 2 lookups and then the cached entity retrieval just in the flood code. That doesn't seem ideal.
is loosing this ok?
This confused me at first too but for future reviewers it is because clearAccount has access to the id builder in the flood class so it doesn't have to cache the earlier resolution and use it. Previous comments about adding uncached lookups applies though.
Comment #27
mr.baileys1: Added drupal_static caching to
\Drupal\user\Flood\CredentialsCheckFlood::getAccountIdentifier()2: according to the
\Drupal\Core\Authentication\AuthenticationProviderInterface, it should either returnSince we have a failed authentication, not returning (NULL) seems better than returning an empty array.
Comment #29
mr.baileysI feel stupid now.
Comment #31
mgiffordThe bots like this now. What manual testing is needed?
Comment #32
mgiffordre-uploading the patch for the bot.
Comment #34
catchWhy can't basic_auth.authentication.basic_auth use the service provided by user module?
Comment #36
bojanz commentedThe service needs to inject EntityTypeManager instead of EntityManager.
The code has no reason to do $config = $this->configFactory->get($this->configName); in every method, that can be done in the constructor.
We should not be using drupal_static in a class. Just use a class attribute.
Comment #37
bojanz commentedI'm interested in the answer to #34 too.
For now here's a reroll with fixes for #36.
Comment #38
dawehnerUnneeded now
All the methods deal with IPs so I'm wondering whether it should be part of the name of the interface
IMHO classes which provide internal state/storage should also have a reset method. On the other hand I seriously wonder whether this static cache is helpful. How likely will you call methods multiple times per request?
Comment #40
bojanz commentedRemoved the @file docblocks, fixed a stupid mistake that caused many of the test fails.
Comment #42
tim.plunkettShould be
$this->config =Comment #43
znerol commentedRegarding the caching of account objects, see #6. In my opinion we should not load the account at all but just treat any flood event equal (regardless of whether or not there is actually an account with that name). Since this would be a change in behavior (which is not desirable during a refactoring) and in order to facilitate reviews I left it there.
Comment #44
bojanz commented@tim.plunkett
D'oh. Rushing things is my middle name.
Comment #45
bojanz commentedInterdiff got eaten.
Comment #47
dawehnerRerolled it a bit and worked on using it for
\Drupal\user\Controller\UserAuthenticationControlleras wellComment #49
larowlanComment #50
manuel garcia commentedPatch #47 applies cleanly, removing the tag...
Comment #51
mr.baileys#47 changed the wording on the access-denied-by-flood-protection-messages, which is causing the 2 test failures. Updated the messages in the test to make the testbot happy.
Comment #52
larowlanI think this is classified as a BC break.
i.e. we might need to retain the original signature and add the new service as a fifth optional argument
But I'd check with @alexpott or @catch to make sure that I'm right about this first, it might be an allowed BC break.
Looking good though!
Comment #53
tim.plunkett- https://www.drupal.org/core/d8-bc-policy#constructors
Comment #54
berdirThat's correct but we've started using the pattern of keeping the argument optional with a fallback to the controller in a few places recently and I like that. We usually use that if it's a class that is quite likely extend somewhere, which is probably not the case here, but there's an argument to be made about this case too:
If you use basic authentication or have the user login block placed on your site then applying this patch gives you a fatal error on every page, you can't even log in. Accessing update.php as anonymous won't clear the cache, so you have to change settings.php to allow anonymous access or use rebuild.php or so.
We could have a single issue to remove all those fallbacks in 9.x and add the same @todo everywhere.
Comment #55
klausiI think this patch does not got far enough. BasicAuth.php should be just a thin wrapper that extracts username and password from the request, then calls some user-authentication-user-is-blocked-plus-flood-control service. That puts the flood and authentication logic into one central place and can be used from multiple locations. Because we always need to check if the user is blocked + the password matches + flood control.
This is security critical code and everytime people have to write an authentication provider they need to get this right. They should not have to think about this.
Comment #56
andypostIn case of external auth password could be skipped
Comment #57
klausiIn case of external auth systems you can swap out the user.auth service. I'm saying we should have a user.all_inclusive_with_flood_control_plus_blocked_check service which uses user.auth (wraps it). With a better name :) user.login? user.login_flood? user.auth_flood? user.auth_check_flood?
The service can return a result object with a bool success true/false and a fail message in case authentication failed.
Comment #58
klausiLet me try.
Comment #59
klausineed to run now, work in progress patch.
Comment #61
klausiNow demonstrating that we can share code between basic auth and user auth controller.
Still some rough edges in the patch:
* usage of t()
* no interface for the new service
* is it ok to keep the AuthResult class as data junk with public properties?
* very long authenticate() method in the new service, can we split it up in a meaningful way?
The next step will be to try to refactor UserLoginForm to use this new service. The biggest difference between HTTP API call messages and login form messages is that the latter can contain links (for example to reset your password). I'm not sure yet how we can deal with that difference.
Comment #62
klausiImplemented UserLoginForm. I solved the problem with the messages by introducing constants as error codes for each error state. That way a consumer such as UserLoginForm can override the specific messages easily.
The code unification to one service also surfaced inconsistent behaviour for the HTTP API user login and the form login. I adapted the test case accordingly so that we have the same messages on forms as well as in API responses.
I was able remove large swaths of duplicated code, yay! Let's see where this blows up now.
TODO:
* Refactor authenticate() method because is has too many if/else levels
* Finalize the naming. How do you like the current user.auth_flood, UserAuthFlood and UserAuthFloodInterface names?
Comment #64
klausiCool, that went better than I thought.
Comment #65
dawehnerHow do we deal with this empty case here, don't we break the API somehow here?
I really like the idea of a result object, that makes it clear to understand what is going on.
I was wondering whether we should add public methods. I've seen some committers freaking out about objects having no public methods. Personally I agree with you, this object is not meant to be swapped out
Is there a way we introduce custom static factories like:
AuthResult::userBlocked($message)andAuthResult::success($user). We could then even have different objects one for success one for failure or so, basically similarily to the way howAccessResultworks.Comment #66
klausi1. BasicAuth.php wrongly returned [], but AuthenticationProviderInterface says you should return NULL on authentication failure.
4. AccessResultInterface is quite a different beast because it offers functionality to combine multiple access results into one. It has quite a bit more of complexity than our authentication result here. I would avoid adding complexity if we don't have to - all we want to do here is provide a multidemensional result after the authentication. More like an array in the old days, but by using a class we can make the contents explicit and better document it. What would be the benefit of having 2 classes for success and failure? Same for having public methods, they would just be dumb getters for the properties.
I just feels wrong to write methods for on object that has zero behavior attached to it. It is just a data container. Anyone else think we should blow up the public methods on the AuthResult class?
Comment #67
klausiMore feedback from Crell and bojanz says that we should not do public properties, so let's do a full blown class with getters and an AuthResultInterface in the next iteration of this patch.
Comment #68
berdirNon-public properties doesn't mean we need an interface. I agree with them and think we should not have public properties but that doesn't mean we need an interface.
Comment #69
catchYeah I think single class + protected properties + methods is OK for this. The class with public properties is not much more defined than stdClass() so methods is good though, but no point in an interface unless we were going to add another class, which we've no intention of (and could add one then if we wanted).
Comment #71
klausinow with public methods on AuthResult.
Also updated issue summary.
Comment #72
klausibetter title.
Comment #74
klausiForgot to convert one line in BasicAuth, tests now passing locally.
Comment #75
Munavijayalakshmi commentedRerolled the patch.
Comment #86
darvanenI was beginning to re-roll this... but I see that there is now a UserFloodControl service that separates out the logic around allowing requests and the registration of flood events.
It does not centralise the error messaging the way the patches here do.
Is this issue defunct or does it still have some value?
Comment #88
bradjones1Yes, this issue is still active in so far as there's still repeated logic regarding user authentication... the user flood control service helps abstract out some of the inner workings/wrapping around the flood service, but it's not re-usable (e.g., by other authentication providers) for auth.