-
Notifications
You must be signed in to change notification settings - Fork 359
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
Enable password recovery when using ILS Authentication #3997
base: dev
Are you sure you want to change the base?
Conversation
…tion. Updated based on the PHP 8.3 test failures.
…tion. Updated based on the PHP 8.3 test failures. When I ran php cbf it made changes to the if statements that caused this failure.
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.
Thanks, @oharacj! See below for a few initial thoughts and questions.
@@ -2079,9 +2125,12 @@ public function newPasswordAction() | |||
} | |||
// Update hash to prevent reusing hash | |||
$this->getAuthManager()->updateUserVerifyHash($user); | |||
// Login | |||
if ($followUp = $this->followup()->retrieve('url')) { | |||
$newUrl = strstr($followUp, 'Verify', true) . 'Home'; |
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'm not sure I understand the purpose of this; maybe a comment is in order.
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.
Thanks, that makes sense, though I'm not sure I understand exactly what $followUpUrl = strstr($followUp, 'Verify', true) . 'Home';
is trying to do. I wonder if it might be better to do something like $followUpUrl = str_contains($followUp, 'Verify') ? $this->url()->fromRoute('home') : $followUp;
(so we're using the router instead of string manipulation to pick the destination URL... but maybe I'm misunderstanding which target route is actually desired).
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.
This should work without an existing VuFind account. My proposal would be to add another ILS driver method for looking up the patron with email address and card number. Since we already have a local implementation that works across ILS systems, our experience is that this allows recovery with any ILS that can do the lookup and update the password. See https://github.com/NatLibFi/NDL-VuFind2/blob/dev/module/Finna/src/Finna/ILS/Driver/KohaRest.php#L747 for our implementation with Koha.
The recovery token data and any other information needed (e.g. target ILS for MultiILS) from the lookup method can be stored in access_token table with a random hash as the id. This hash should be included in the recovery link in the email along.
Since this functionality is somewhat different from internal password recovery, it might make sense to separate the implementation. That's why we chose locally to use different method names etc.
* | ||
* @return bool | ||
*/ | ||
public function supportsPasswordRecovery() |
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.
This needs a counterpart in MultiILS that supports checking the selected target. For inspiration:
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.
My initial work is only supposed to be for the ILS auth. MultiILS is something I'll look at in the future but I'm not sure it's part of the scope of this work. Is this piece a necessity? If so, I can start working on this and I'll resubmit when I have it done. I only have one ILS and I can't really test but I'm happy to look into it.
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.
You could potentially use the Demo driver as a second ILS for testing purposes, if that helps! Let me know if you need more details/help to get that set up.
{ | ||
$driver = $this->getCatalog()->getDriver(); | ||
if ( | ||
method_exists($driver, 'changePassword') |
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'd suggest a new method for updating the password during recovery, since changePassword requires the patron from patronLogin method. Our custom code uses this:
https://github.com/NatLibFi/NDL-VuFind2/blob/dev/module/Finna/src/Finna/ILS/Driver/KohaRest.php#L792
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've updated the code to use a resetPassword method for this. I've added my resetPassword to the SierraRest Driver.
Updated comments, line endings, follow-up edits have been put in abstractbase, explained the redirect.
updated formatting.
added trailing comma to return array
Object-Oriented Programming is a thing.
I have an ILS method called getPatronFromUsername which provides this function exactly. It has been added to the SierraRest driver.
I am partially utilising the internal password recovery methods in order to keep from duplicating functionality |
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.
Thanks for the progress, @oharacj -- see below for a few more tips, questions and ideas. :-)
config/vufind/SierraRest.ini
Outdated
@@ -59,6 +59,9 @@ use_prefixed_ids = false | |||
; used (redirect_uri above is not set). | |||
username_field = "code" | |||
password_field = "pin" | |||
; Some library systems use a four digit access pin. Others prefer to have alpha numeric | |||
; passwords. set digits_only to true for pin functionality. Default is false (alpha-num). | |||
digits_only = true |
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 are higher-level password rules defined in config.ini. I wonder if it would be beneficial to make this setting more parallel with those (especially if we could refactor code to make the password validation logic reusable -- I haven't inspected to see how feasible this is, but it seems likely to be an option).
Alternatively, if there are really only two different options in Sierra, maybe it would be more clear to call this setting four_digit_pin
(to account for both the length restriction and the content restriction), or to have a setting like password_mode = pin|password
(if we want to allow for more possibilities in the future than a binary setting can provide.
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.
Just checking to see if you have any thoughts on this point, since there hasn't been further discussion on it since October. (Also happy to hear @EreMaijala's thoughts, if any).
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 we should use the same rules as when changing a password, i.e. the [changePassword] section in ILS driver's ini:
; Uncomment the following lines to enable password (PIN) change
;[changePassword]
; PIN change parameters. The default limits are taken from the interface documentation.
;minLength = 4
;maxLength = 4
; See the password_pattern/password_hint settings in the [Authentication] section
; of config.ini for notes on these settings. When set here, these will override the
; config.ini defaults when Sierra is used for authentication.
;pattern = "numeric"
;hint = "Your optional custom hint can go 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.
I'm still not sure how I've managed to miss this section of the ini so many times. I had to have looked over it hundreds of times without seeing that there was a [changePassword] section. I'll remove the custom stuff I did but I'll also have to make a change to the changePassword function in the SierraRest driver in order to remove the hard-coded digits only section of password change.
* | ||
* @return bool | ||
*/ | ||
public function supportsPasswordRecovery() |
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.
You could potentially use the Demo driver as a second ILS for testing purposes, if that helps! Let me know if you need more details/help to get that set up.
@@ -309,11 +309,12 @@ public function getStoredCatalogCredentials() | |||
* fails, clear the user's stored credentials so they can enter new, corrected | |||
* ones. | |||
* | |||
* Returns associative array of patron data on success, false on failure. | |||
* @param $user_name - the username/barcode for ILS password reset |
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.
Minor points of style: I'd suggest $username
or $userName
instead of $user_name
-- we rarely use snake_case in our code, so the other options would be more consistent. Also, you don't need the "-" separator in this comment.
@@ -2079,9 +2125,12 @@ public function newPasswordAction() | |||
} | |||
// Update hash to prevent reusing hash | |||
$this->getAuthManager()->updateUserVerifyHash($user); | |||
// Login | |||
if ($followUp = $this->followup()->retrieve('url')) { | |||
$newUrl = strstr($followUp, 'Verify', true) . 'Home'; |
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.
Thanks, that makes sense, though I'm not sure I understand exactly what $followUpUrl = strstr($followUp, 'Verify', true) . 'Home';
is trying to do. I wonder if it might be better to do something like $followUpUrl = str_contains($followUp, 'Verify') ? $this->url()->fromRoute('home') : $followUp;
(so we're using the router instead of string manipulation to pick the destination URL... but maybe I'm misunderstanding which target route is actually desired).
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.
Thanks for the ongoing work, @oharacj -- I started my review this morning but ran into some questions that I think will be resolved by merging the dev branch into this PR, so I decided to stop until that's done to be sure I'm reviewing everything in an up-to-date and working state. Please let me know if you run into any trouble with this!
config/vufind/SierraRest.ini
Outdated
@@ -59,6 +59,9 @@ use_prefixed_ids = false | |||
; used (redirect_uri above is not set). | |||
username_field = "code" | |||
password_field = "pin" | |||
; Some library systems use a four digit access pin. Others prefer to have alpha numeric | |||
; passwords. set digits_only to true for pin functionality. Default is false (alpha-num). | |||
digits_only = true |
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.
Just checking to see if you have any thoughts on this point, since there hasn't been further discussion on it since October. (Also happy to hear @EreMaijala's thoughts, if any).
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 didn't have time for a full review yet, but I noticed one thing that looks like a problem and had a couple of very minor style suggestions that you can apply by just committing my suggestions from within the comments below. :-)
Co-authored-by: Demian Katz <[email protected]>
Co-authored-by: Demian Katz <[email protected]>
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.
@oharacj, I've given this another relatively high-level review (since much of this code is outside of my area of expertise, it would take me longer to really refresh all my memory on the details, so I'm just trying to chip away at issues while waiting for others with more experience to find time to review more deeply). Please feel free to ignore any comments that are not relevant due to my naivete. I hope the bombardment of small reviews is not a nuisance! ;-)
* @throws AuthException | ||
* @throws \Exception | ||
*/ | ||
public function newPassword($request) |
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'm not entirely sure (without spending more time digging deeper into the code and the history) why we need to split updatePassword into two methods... but if this is unavoidable, I wonder if resetPassword
would be a better name than newPassword
since it's more verb-based. Also, if we make this change here, do we need to make parallel changes to VuFind\Auth\Database
since it is currently implemented to do password resets using only updatePassword, and some of the other code changes here might conflict with that?
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'm pretty sure that updatePassword is able to do everything needed, so this method shouldn't be needed.
* | ||
* @return bool | ||
*/ | ||
public function multiSupportsPasswordRecovery($target) |
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.
Might it be possible to add a second optional argument to supportsPasswordRecovery instead of a whole new method here? Or is this really unavoidable?
@@ -712,6 +712,14 @@ protected function setFollowupUrlToReferer(bool $allowCurrentUrl = true, array $ | |||
if ($mrhuNorm === $refererNorm) { | |||
return; | |||
} | |||
// if is the stored current url of the lightbox |
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'm not sure I understand this comment, and if is
suggests to me that maybe there are extra or missing words.
//ILS Driver: | ||
//if the user hasn't logged in yet, but is found by the ILS, call function | ||
//getPatronFromUsername |
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.
Another minor style tweak that you can commit directly from this comment:
//ILS Driver: | |
//if the user hasn't logged in yet, but is found by the ILS, call function | |
//getPatronFromUsername | |
// ILS Driver: | |
// if the user hasn't logged in yet, but is found by the ILS, call function | |
// getPatronFromUsername |
@@ -23,6 +23,9 @@ | |||
<input type="hidden" value="<?=$this->escapeHtmlAttr($this->hash) ?>" name="hash"> | |||
<input type="hidden" value="<?=$this->escapeHtmlAttr($this->username) ?>" name="username"> | |||
<input type="hidden" value="<?=$this->escapeHtmlAttr($this->auth_method) ?>" name="auth_method"> | |||
<?php if (!empty($this->target)): ?> | |||
<input type="hidden" value="<?=$this->escapeHtmlAttr($this->target) ?>" name="target"> |
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 it make sense to use a more descriptive name here, like target_ils
? At a glance, it's not immediately obvious to me what target means, so a little context might help.
@@ -1766,14 +1783,18 @@ protected function sendRecoveryEmail(UserEntityInterface $user, $config) | |||
$config = $this->getConfig(); | |||
$renderer = $this->getViewRenderer(); | |||
$method = $this->getAuthManager()->getAuthMethod(); | |||
// If target exists create query string to include it as part of reset url | |||
$target = $this->getRequest()->getQuery('target') ? '&target=' | |||
. $this->getRequest()->getQuery('target') : 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.
Since you concatenate this to a string down below, I don't think you ever want it to be null -- should probably be empty string instead. Also, it's probably best to encode the value for safety. And it might make sense to refactor to avoid calling getQuery so many times, e.g.:
$target = $this->getRequest()->getQuery('target');
$targetParam = $target ? '&target=' . urlencode($target) : '';
Then down below, you can use $targetParam in your concatenation, and you can assign $target to $view->target instead of fetching the value yet another time.
* @throws AuthException | ||
* @throws \Exception | ||
*/ | ||
public function newPassword($request) |
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'm pretty sure that updatePassword is able to do everything needed, so this method shouldn't be needed.
@@ -329,8 +330,22 @@ public function storedCatalogLogin() | |||
$this->ilsAccount[$username] = $patron; | |||
return $patron; | |||
} | |||
} elseif (!empty($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.
What's this for? I don't think this should be needed for password recovery.
* @throws AuthException | ||
* @return UserEntityInterface Updated user entity. | ||
*/ | ||
public function newPassword($request) |
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.
This should not be necessary either.
$driver = $catalog->getDriver(); | ||
$targetDriver = $driver; | ||
if ($target != null) { | ||
$targetDriver = $driver->getDriverFromTarget($target); |
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's better to leave the details inside MultiBackend and just pass enough information to it for it to be able to choose the correct driver. Something like:
public function ilsSupportsPasswordRecovery($target)
{
$catalog = $this->getCatalog();
$recoveryConfig = $catalog->checkFunction(
'recoverPassword',
['cat_username' => "$target.123"]
);
return $recoveryConfig ? true : false;
}
Admittedly not exactly beautiful, and it's my intention make it possible to just indicate the target more elegantly. Somewhere in future.
//ILS Driver: | ||
//if the user hasn't logged in yet, but is found by the ILS, call function | ||
//getPatronFromUsername | ||
if (!$user && $this->formWasSubmitted() && !empty($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.
I think this, as opposed to updatePassword, should be split to ILS-specific part. And because the ILS's have different functionality, we can't rely on always being able to fetch a user by username. It could also be email address.
It might also make sense to not even try to retrieve a full user in this situation, but just whatever information is needed when the password is reset. In some cases it could be that the ILS supports password reset by way of a recovery token instead of username, but e.g. for Koha we should just need patron id (borrowernumber).
@@ -18,11 +18,14 @@ | |||
<?php elseif (!isset($this->hash)): ?> | |||
<div class="error"><?=$this->transEsc('recovery_user_not_found') ?></div> | |||
<?php else: ?> | |||
<form id="newpassword" class="form-new-password" action="<?=$this->url('myresearch-newpassword') ?>" method="post" data-toggle="validator"> | |||
<form id="newpassword" class="form-new-password" action="<?=$this->url('myresearch-newpassword') ?>" method="post" data-bs-toggle="validator"> |
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 should be data-toggle, no data-bs-toggle in bootstrap3
Add handling to enable password recovery when using ILS Authentication method