Problem/Motivation
There are already two public (functional) methods for generating a URL that performs an action on a user in user.module:
- \user_pass_reset_url()
- \user_cancel_url()
#85494: Use email verification when changing user email addresses at the time of writing threatens to introduce another one, as noted by @alexpott (part 5) and by @larowlan.
Proliferation of public APIs should be avoided, and the user module needs some OO love.
Steps to reproduce
Not applicable.
Proposed resolution
Create an injectable service which can fill the needs of these existing and new methods and refine it to fulfil any related tasks which might appear in future, or leave room for that to occur.
Remaining tasks
- Write the service
- Create relevant tests
- Write change record to deprecate \user_pass_reset_url() and \user_cancel_url()
- Review
User interface changes
None
API changes
- Create new service to respond to all user action URL generation and check requests
- Deprecate
\user_cancel_url
and\user_pass_rehash
as a follow-up task.
Data model changes
None
Release notes snippet
TBC
Issue fork drupal-3229038
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 #2
darvanenHave made a start on this, local testing is showing a few fails, waiting for completion to see what they were.
Comment #3
larowlanI thought we were just moving the procedural stuff to a service?
Comment #4
darvanenWell yes, but there are challenges with that, for instance, the route parameters aren't constant so I've ended up with this mess:
Comment #6
darvanenComment #7
darvanenI realised the service as-is would not work with the route described in #85494: Use email verification when changing user email addresses.
Updated with options to include additional route parameters and a payload to add to the hash. Could tightly couple the two but I thought I'd start with the more flexible option. I suppose one could argue for embedding better security practices in the API, I'd prefer to cover that in documentation in the class (yet to be done properly) but I don't know if there's a policy around this kind of thing already.
Comment #9
darvanenComment #10
darvanenI've come to the conclusion the leaving
\user_cancel_url
and\user_pass_rehash
alone in this change is the best course of action for a few reasons:\user_pass_rehash
to check if their hash is correct, so it should really be used to generate the hash.\user_pass_rehash
which drives home the point for me that the two existing routes should use *that* to generate hashes.Why do I want to remove the user login time from the hash?
I just tried using this new service to create a user email verification link that gets sent in the welcome email (no admin auth required) because I want the following login flow:
This is quite a common pattern in SaaS offerings.
The issue is that the welcome email is generated before the user is automatically logged in so the hash differs.
Why was user login time specifically used? Can we replace it with anything better? I'm loath to just remove it, it would reduce the randomness of the hash.
Comment #11
larowlanSo you're trying to consolidate them into one method?
Could they remain separate methods?
Comment #12
darvanenThey could.
What then of new uses for this service like those surfaced in #85494: Use email verification when changing user email addresses? Do we keep adding new methods? Or do we create a general-use method and treat 'reset' and 'cancel' paths as special cases? I'm not familiar enough with the policy/procedure/standard for handling BC here.
and... if I did put them in as special cases, and assuming I am successful in campaigning for a change to the hash contents, they would need their own special case checkHash methods (or a gate within that method) which really muddies the water if we ever want to standardise away from the current global \user_pass_rehash function which in my opinion should be private/internal only to the service.
boiled down: I think it best to let people use the global functions as pairs in their current state until they are deprecated and removed - and for the new service to implement the best possible logic (with an unchanging API) for core, modules, and builders to swap over to gradually.
Separately:
I did a little testing and I think
$account->getChangedTime()
might be a suitable replacement for$account->getLastLoginTime()
, at least, it doesn't break the two existing core uses of user_pass_rehash, and it changes from day to day or week to week like the lastLoginTime does. Thoughts?Comment #13
darvanenI'm embedding this change into my application to keep me accountable for keeping it moving, and because it's just really useful. So with that in mind I've changed the loginTime element to changedTime just until there's a direction/decision because loginTime doesn't work for my use-case.
Honestly I suspect there will be use-cases where changedTime is similarly unsuitable.
This does still need tests but it's marked 'needs review' to get some direction.
Comment #14
darvanenAnother issue with the contents of the hash has been reported at comment #347 on #85494: Use email verification when changing user email addresses:
Which comes down to the hash being generated before the new password is saved to the account.
Obviously we can't remove all of the changeable values from the hash or it will become static and far easier to use in an attack.
Thinking out loud: perhaps we could pass an
ignore
parameter tofromRoute
andcheckHash
in this service that asks it to leave one and only one of the values out of the hash? (PHP 8.1 enums would make this that much easier, sigh)Looking at the service again I realised I was asking for an associative array but not using the keys in the hash, have fixed that.
Comment #15
dwwHere as part of a review swap. Thanks for opening this and moving it forward! From the title alone, this seems like a good move towards less procedural code.
Haven't closely looked at the MR (yet), but some initial thoughts after reading the summary, comments, and a quick glance at the MR.
The code and issue discussion are now dealing with a whole other question of changing the hashing for the login links. That's not mentioned anywhere in the summary. I think that's pretty major scope creep for what I thought I was going to be looking at. 😉 We probably want to split that off into another issue, perhaps postpone this one on that, and focus on the hashing stuff first. Once that works how we want, we can come back in here and do what this issue is saying to do. But doing the hashing as a side effect of moving these things into a service seems like it's destined to run into trouble and make this hard/impossible to commit. I don't really want to weigh in on the hash questions until we're sure where we should be discussing those.
It's a new service, there is no BC to worry about. As such, I'd say it's important to get the API "Right", as best we can, now. Once we ship this thing, then we have to worry about BC.
I haven't been deep in any of this, and haven't even opened the related issues, so take this with a grain of salt. But if we only have 2 use cases now, and a 3rd on the way, and they each have different needs, sounds like we haven't hit the Rule of 3 yet, so trying to prematurely generalize this is likely to result in YAGNI. If we need 3 kinds of links, and they all are a little different, we should have 3 methods on the service. If there are parts of those methods that could be shared, we can have protected helper methods as appropriate. But the entryway into the service shouldn't be
theOnePublicMethod($tons, $of, $crap, $to, $handle, $anything);
. If we start with separate functions, and find we can later generalize, it's easy to add deprecations to the specific ones and call out to the cleanly generalized thing. If we start from a clumsy general method, it seems harder to deprecate that if we decide it's not really working out. That's how I see it, although I'd defer to framework or release managers who disagree.I don't think I should do a close review of the MR until the scope is sorted out here, so I'll stop for now.
Thanks,
-Derek
Comment #16
darvanenThanks so much for the review @dww :) you've raised some great points.
You're right, that's a huge scope creep, they should be separate. I would argue however that if we do this issue first, the altering of the hash for the existing links becomes *much* easier because it will be self-contained and will allow us to change the hash method's signature if we need to, since it shouldn't be exposed by the service the way it currently is.
Thoughts?
@larowlan questioned this too, ok.
I wasn't under this impression but I have no particular reason I can point to so I will recalibrate my thinking based on that and put the three links in.
What would we prefer?:
or
The latter could farm out the hash generation to a protected method based on the type key.
I'm not particularly wedded to either idea, but left to my own devices I'd probably go with the latter.
Comment #20
brad.bulger CreditAttribution: brad.bulger commentedI'm not sure if this is moot at this point, but in regards to comment #13 above, why include either login or changed time? To use the email change example, why shouldn't I be able to change my email, then later change my picture or timezone or password, and then subsequently click on the verification link in my email to confirm the email change request?
If there is going to be a general rule like "You can't log in before using this" or "You can't make any other changes" I'd think we'd want to communicate that to users right away. It would never occur to me to assume either of those conditions, at least.
Comment #21
darvanen#20 I don't think it's moot, I'm just no cryptography or cybersecurity expert so the idea of reducing the number of variables in the hash gives me pause.
I wholeheartedly agree with your points.