When logging in a user, hook_user('login') isn’t called consistently and neither are the other tasks associated with a successful login.

All of the appropriate tasks occur in user_login_submit() which is called when a user completes the user login form:

making a note in watchdog
updating user's login timestamp
firing hook_user('login)
and generating new session ID
But not all of those tasks are completed in the other places that users are logged in: user_pass_reset() (one time password), user_register_submit() (register and login immediately), blogapi_validate_user(), drupal_login().

Those login tasks should be called the same place where the actual login occurs; namely where the user is loaded into global $user in user_authenticate()

I’ve attached a patch that consolidates those tasks into one place. The patch is functional, but not particularly pretty. A more elegant solution would be great!

Not generating a new session ID when logging in is a security issue. In theory, it is susceptible to a session fixation attack.

Comments

moshe weitzman’s picture

I'll try to take a look at this next week. I agree that it is a critical bug report (i.e. needs fixing before release).

johnalbin’s picture

Please note that 5.x is also susceptible to the “not generating a new session ID” security issue.

moshe weitzman’s picture

Is a bit hard to parse what is going on here. Hopefully someone else is available to review this.

dmitrig01’s picture

Status: Needs review » Needs work

How do I test this?

johnalbin’s picture

Status: Needs work » Needs review
StatusFileSize
new4.46 KB

Re-rolled patch after #152492 was committed.

Moshe, I also hand-rolled part of the patch to make it clearer what is being modified. The previous auto-gen patch was indeed a little confusing. Mostly this patch moves code around rather than creating new code.

dmitrig01’s picture

Status: Needs review » Reviewed & tested by the community

u huh

moshe weitzman’s picture

Status: Reviewed & tested by the community » Needs review

since this patch is non trivial, we should have a detailed positive review before RTBC. please state what you tested in the patch, and how that is better than current drupal.

i sometimes rtbc patches without much comment also, but i do that on very trivial patches.

moshe weitzman’s picture

Status: Needs review » Reviewed & tested by the community

code looks sane.

i tested login and registartion with openID and without. all works well.

i got one NOTICE as follows. That can be fixed later if needed. This patch is RTBC anyway.
notice: Undefined index: values in /Users/mw/htd/pr3/modules/user/user.module on line 1293

gábor hojtsy’s picture

Status: Reviewed & tested by the community » Needs work

Erm, why should user_login_authenticate_validate() call user_authenticate() twice?

moshe weitzman’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new4.71 KB

good catch. rerolled without the first line

dries’s picture

Can't user_authenticate() just take a $form_values instead of ($name, $pass, $form_values = array()) or is user_authenticate() used in situations that does not involve a form? If so, I think it's a big ugly to pass in the $form_values() in user_authenticate().

johnalbin’s picture

Both blogapi_validate_user() and drupal_login() call user_authenticate() without a form. And, yes, user_authenticate($username, $password, $form_values) is a big ugly; I mentioned that in my original posting.

We could still change the definition to user_authenticate($form_values) and then just modify blogapi_validate_user() and drupal_login() to call user_authenticate(array('user' => $username, 'pass' => $password)).

But one concern I have with that idea is that it’s too easy for a developer to accidently call user_authenticate(array('user' => $username, 'password' => $password)) (note the wrong key) which would mean that the user would be logged in without checking the password. Perhaps we could throw an error if 'pass' is not passed as a key to user_authenticate?

dries’s picture

Moving to user_authenticate($form_values) might make sense as we're moving towards more FAPI goodness and programmable submitted forms.

What do the others think of that?

moshe weitzman’s picture

user_authenticate($form_values) is fine with me and John' suggestion to require a 'pass' element makes sense.

dries’s picture

Status: Reviewed & tested by the community » Needs work

Yes, I'm also OK with the proposed solution.

aliljet’s picture

I've been running up against this bug as well, and I noticed that this extends not only to the login functionality, but also the "logout" functionality as well... can anyone else verify that?

johnalbin’s picture

StatusFileSize
new6.11 KB

Updated patch with agreed-upon solution.

Pavan, I haven’t noticed the same thing with logout; all the logout tasks appear to be in user_logout(). Can you give some specifics?

johnalbin’s picture

Status: Needs work » Needs review
chx’s picture

StatusFileSize
new7.4 KB

Reroll against HEAD. Changed and to && as we use that.

chx’s picture

another important note: do not credit me when committing this, I just rerolled.

snufkin’s picture

Status: Needs review » Needs work
StatusFileSize
new7.34 KB

I tested with login, logout, register. did not test with openid.

Patch needs reroll to work, it seems that poll.module has been altered since. User creation works, watchdog entries are created. however when i try to create a poll (some poll code has been altered in the patch) i get this:

    * notice: Undefined property: stdClass::$date in /home/balu/projects/drupal/HEAD/modules/node/node.pages.inc on line 170.
    * notice: Undefined property: stdClass::$created in /home/balu/projects/drupal/HEAD/modules/node/node.pages.inc on line 191.

Preview and viewing the poll generates a lot of errors too. Rerolled patch in attachment.

chx’s picture

Status: Needs work » Needs review
StatusFileSize
new13.78 KB

poll changes are totally astray they slipped in from another issue. sorry.

bdragon’s picture

Status: Needs review » Needs work

/me gets the feeling that was the wrong patch.

Moment, I'll do a reroll...

bdragon’s picture

Status: Needs work » Needs review
StatusFileSize
new6.29 KB

OK, here's my take on the poll-less reroll.

snufkin’s picture

I have tested it, login (logout) works, log entries are created. Central login (user@drupal.org) works as well as login via block. The only thing that failed to work with me was the OpenID login, but I tried without the patch too, and it fails the same way.

johnalbin’s picture

FYI, Brandon’s is the correct re-roll.

bdragon’s picture

Assigned: Unassigned » bdragon
Status: Needs review » Needs work

Taking over.

Patch no longer applies, rerolling.

bdragon’s picture

Status: Needs work » Needs review
StatusFileSize
new6.71 KB

Rerolled and fixed a bit of the docs.

snufkin’s picture

Status: Needs review » Reviewed & tested by the community
StatusFileSize
new6.71 KB

I corrected a syntax error on line 67 of the patch. Tested, works. Session entry appears in the logs, NOTE: session open does show up when logging in via openid.

chx’s picture

openid can be a supplemental patch this one works and does what it should.

ilo’s picture

Reading the conversation I realize it's the best moment to suggest another change in the login flow..

Just to notice, user_authenticate DOES load the user in the current session, and it's being done in the validation part of the form, the hook_validate. This can be wrong, and should be done in the hook_submit instead (actually it's being done in both hooks.) The problem running user_authenticate in the validation part is that if you want to add another field to the login form using form_alter, even if that attach a new validation function to the form or any of the form fields, the validation has already loaded the user in the current session.

It's being an issue for the captcha and other modules doing operations to the login form.

May I suggest to just simply check if username and password are valid in the validation hook, and perform the authentication in the submit hook? it makes more sense this way.. and will allow other modules to attach validators in the login form.

PD: Comming from here.. http://drupal.org/node/177623

gábor hojtsy’s picture

Status: Reviewed & tested by the community » Needs review

I have a few smaller style issues after looking at the code:

- Not enough whitespace before the @return docs at user_authenticate()
- Why do we recreate a very similar array in user_authenticate(), if we can modify and pass on the array?
- Do we document void return values?
- Where does $merge_data come from? (I did not have the time to review patched code, and it looks suspicious).

moshe weitzman’s picture

Status: Needs review » Reviewed & tested by the community
StatusFileSize
new7.01 KB

I tested this patch hand it works as advertised. Code looks good.

Addressing Gabor's style issues:
* I removed the extra whitespace
* We are recreating that array because $form_values might have other elements in it and user_load() actuially uses every element thats passed in. We have to pass this subset and only this subset.
* I removed the void return docs. It might be a good idea to start doing this in the future, though.
* $merge_data contains some registration info which has been massaged or provided from default values. I think that could be refactored out, but it will require reworking the registration process which is not in scope for this issue. I see no danger here.

I am seeing a NOTICE when using a one time login link but thats not related to this patch. I will file separate issue.

johnalbin’s picture

If I understand my order of operations correctly (http://www.php.net/manual/en/language.operators.php), the if statement in the patched user_authenticate() (line 1226) won’t work correctly:

  if (!empty($form_values['name']) && !empty($form_values['pass']) && $account = user_load(array('name' => $form_values['name'], 'pass' => trim($form_values['pass']), 'status' => 1))) {

&& has higher precedence than =, so !empty($form_values['name']) && !empty($form_values['pass']) && $account will be run first and the result of the user_load() will be assigned to a VALUE rather than to a VARIABLE.

Or am I wrong?

These kind of issues are why I prefer "and" and "or" over && and ||. Why does Drupal prefer &&? Simple because its shorter?

johnalbin’s picture

Status: Reviewed & tested by the community » Needs work

Also, doesn’t the new user_external_login() need to call user_authenticate_tasks()?

bjaspan’s picture

subscribe

bdragon’s picture

Assigned: bdragon » Unassigned

Back to anon

john morahan’s picture

Status: Needs work » Needs review
StatusFileSize
new6.33 KB

rerolled: drupal.module is gone; also fixed a notice: "Undefined index: values in /.../modules/user/user.pages.inc on line 103." (I just removed $form_state['values'] as it's not set - not sure if that's right).

JohnAlbin: from the page you linked:

Note: Although = has a lower precedence than most other operators, PHP will still allow expressions similar to the following: if (!$a = foo()), in which case the return value of foo() is put into $a.

so I guess the if statement should be ok. I don't know about user_external_login() - although the phpdoc does say, "The user object must already be authenticated.".

ilo’s picture

In my (humble) opinion, we are still in the same concept error!

I Know there's no need to remember the workflow of the forms API, and the order the hooks should be fired, and In the user.module of any of the drupal versions, the hook_validate of the login form IS LOADING THE USER into the global $user variable, NOT just checking the input data entered, and the hook_submit of the form is used only to redirect the user. This is breaking the feature of attach validators to the login form, because even if a validator returns FALSE, the user.module validator is loading the user with the user_authenticate API call.

I think it's a good idea to fix this within the scope of this post, before closing the bug issue.

moshe weitzman’s picture

@ilo - you are right that this needs fixing.

the bigger problem is that this issue has shown that user login is a mess right now. i take partial responsibility for that. if noone else does so, i will try to spend some time with this issue. i think it requires actually flow charting the different entry points and then trying to simplify and make consistent.

ilo’s picture

I don't know if this may help, but what I've learn from you (all) and this issue is that login form, user authentication, and user creation flows (old drupal.module) are mixed.

Oh, by the way, once finished with this issue, a "drupal_community_invoke('review your modules')" should be fired.. I've seen several contrib modules regarding or implementing their own login flow.. The first example comming to my mind now is ldap authentication. What if we request help from these contributors to find why current implementation didn't work for them? (this request is ugly because will slow down too much the solving of this problem, but my clear why all the code is so messy).

I think I could help to you with this flows, but I'm affraid: I think I need more time to setup a testing site than the time the whole solving may take to you. I any case, if I can help please ask for that! it's too much work for a single person.

moshe weitzman’s picture

You can help. Please do list/describe the functions and flows if possible.

ilo’s picture

Ok, I'm on it. I'll start from scratch with current status in version 6.Beta2 (user.module,v 1.853), discarding the changes you may apply from now on.

gábor hojtsy’s picture

Status: Needs review » Needs work

Guys, this issue is part of the small list of issues holding up Drupal 6, but there was no activity for *3 weeks!*. What can we do to help you push this forward? We are trying to work towards a release soon, but critical user login issues lingering around for weeks is not something which helps... Thanks for all your efforts!

ilo’s picture

I'm currently finishing the login flow maps.. it's really hard to find all critic flow entry points.. Some modules break completely the login flow what is really bad.

I was trying to put here some of the findings, but my time is slow due to overtime work, sorry. I'll be focused on this, but I think (my part) it's not being done (including fixing) on friday..

adTumbler’s picture

I do not know if this helps, but I am having an issue with logout failing.

Drupal 5.3 - fully upgraded, Garland theme

I have been able to reproduce the issue consistently - looks like this.

Log on
Navigate the site
Use breadcrumb to return to home page
Log out

The log out fails - but in a partial way

If you visit any page other than the home page, system recognizes you are not logged on, but on home page it thinks you are logged on.

To get out of this is difficult:

Force a new log screen by going to http://example/user

Edit a block configuration, and save changes

Logout from any page except the home page - lets you get a clean log out

Hope this helps - any explanation for this issue?

gábor hojtsy’s picture

Any interest in making progress with this? We are going ahead with Drupal 6, and blocking on issues which people are not interested in working on is no fun.

chx’s picture

Version: 6.x-dev » 7.x-dev

Past beta4 changing an API when Drupal as it is works? It's clumsy but I can't see this happening.

johnalbin’s picture

Version: 7.x-dev » 6.x-dev
Status: Needs work » Needs review
StatusFileSize
new3.39 KB

I totally agree that the login API needs to be cleaned up for 7.x; it's obviously too late for 6.x. However, there is an underlying bug that needs to be addressed in 6.x.

I created this issue for 6.x in response to Gordon's 5.x critical bug: http://drupal.org/node/138117

So rather than changing the API around by consolidating user login tasks, we should just make sure that the all the places where user_authenticate() is called also perform all the user login tasks.

Let me quote from the original post:

All of the appropriate tasks occur in user_login_submit() which is called when a user completes the user login form:

  • making a note in watchdog
  • updating user's login timestamp
  • firing hook_user('login) <-- this is what makes this bug CRITICAL
  • and generating new session ID <-- when privileges are escalated, a new session ID must be created

But not all of those tasks are completed in the other places that users are logged in:

  • user_pass_reset() (one time password),
  • user_register_submit() (register and login immediately),
  • blogapi_validate_user().

The attached patch simply copies the tasks from user_login_submit() and pastes it into the 4 places its needed. The only code it modifies is in user_pass_reset(), where $edit = array(); user_module_invoke('login', $edit, $user); is changed to user_module_invoke('login', $form_state['values'], $account);; I didn’t see any reason to not include the $form_state in the callback.

It adds more mess to the current state of the API, but it fixes the bugs.

gábor hojtsy’s picture

Status: Needs review » Needs work

Thanks for taking this on! What about moving all this to a function, and call that from each place? Seems like that function would require an $edit and an $account parameter, and would be only a few lines.

chx’s picture

Status: Needs work » Needs review
StatusFileSize
new3.39 KB

The $account parameter would be superflous.

gábor hojtsy’s picture

To me this looks all nice (ie. good phpdoc comments, duplicate code removed, $account really not required, as user_authenticate() updates the global $user). Anyone else (more familiar with the issue) can confirm?

gábor hojtsy’s picture

Status: Needs review » Fixed
StatusFileSize
new7.25 KB

Well, sat down with this,

- applied latest patch,
- fixed the parse errors,
- fixed the blogapi case being called after the function returned anyway
- removed the login time set code from the one time login code as we do this in user_login_finalize()

Then went on to test:

- tried simple login
- tried one time login link
- tried blogapi login

All worked and triggered user_login_finalize() properly. *Then* went through the comments above and did some thinking whether some API change would be better still then relying on arbitrary contrib module maintainers calling the finalize tasks (the session id regeneration being most important). After all, I think an API change is still fine now in the bold name of security (and Dries also suggested this exact API change, although it was long ago) so went back to previous patches and got through all the improvements from there. Looked around and understood what $merge_data was supposed to be and also fixed a user_authenticate() call in install.php

Then went ahead and tested all the stuff as done before, and all still worked. So finally committed the attached patch. I hope this will strengthen login security for Drupal 6. Thanks to all involved!

gábor hojtsy’s picture

Also documented the update requirement at http://drupal.org/node/114774#user_authenticate

moshe weitzman’s picture

Bravo, Gabor. Thanks so much for finishing this off so nicely.

johnalbin’s picture

Thanks, Gábor! That’s much cleaner.

bjaspan’s picture

Status: Fixed » Needs review
StatusFileSize
new616 bytes

This patch broke user_external_login() because that function uses user_login_submit() to do what is now performed in user_authenticate_finalize(). As a result, even though user_exernal_login() is documented to invoke hook_user('login'), etc., it doesn't. This change breaks persistent_login.module and possibly openid.module as well (I haven't checked).

The attached patch changes user_external_login() to use user_authenticate_finalize(). It seems to work and AFAICT it should work. I'm posting this bug here instead of in a new issue, however, because the API is getting rather subtle and I want those who were involved here to review this for correctness.

FYI, any patch changing the user authentication APIs should be tested against openid.module and persistent_login.module because they do user auth in non-typical ways.

gábor hojtsy’s picture

Hm, good catch. It would need some testing definitely.

nancydru’s picture

I suspect that http://drupal.org/node/165642 is part of this, even though it is reported in 5.x.

Even though the drupal module has been moved out of core, there will be many who are still going to use it. The important thing to test here is FIRST login from a user@drupal.org, not a repeat login. One may delete the user and re-test.

I don't currently have a site or time to test this myself.

gábor hojtsy’s picture

Status: Needs review » Fixed
StatusFileSize
new1.57 KB

I went on to test OpenID (registered a testing account at myopenid.com which was quick enough). Indeed, before the patch, the login tasks were not run, after the patch, my session ID was changed properly, the login was logged, etc. Good improvement.

Also looked into better documenting what happens here and added some docs while committing this patch. Submitted the attached patch.

nancydru’s picture

Thanks, Gábor. Will this patch work also on 5.5? Do you know whether it fixes the problem I referenced above?

gábor hojtsy’s picture

I'd suggest opening a new issue for Drupal 5 issues, but from a quick look on the Drupal 5 codebase, it looks like login task inconsistencies are existent there too.

AjK’s picture

Status: Fixed » Needs review

Just a heads up. I tested the latest HEAD with the site_network module (use to be drupal.module in Core). This module no longer functions. I'll look into it in more detail later, just wanted to a) get that info registered on this issue and b) get subscribed to this issue.

Issue moved to http://drupal.org/node/204236

chx’s picture

Priority: Critical » Normal
Status: Needs review » Fixed

removing this from the queue...

gábor hojtsy’s picture

Priority: Normal » Critical

AjK: site_network uses user_authenticate($username, $password);, while the latest Drupal 6 code provides function user_authenticate($form_values = array()). No wonder it does not work. Please submit an issue against that module or fix this in the module if you are the maintainer. This is a core issue, and it is fixed..

Anonymous’s picture

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for two weeks with no activity.