-
-
Notifications
You must be signed in to change notification settings - Fork 168
Implement a reset password flow. #3041
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
Open
drgrice1
wants to merge
1
commit into
openwebwork:WeBWorK-2.21
Choose a base branch
from
drgrice1:password-reset-flow
base: WeBWorK-2.21
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+346
−9
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| package WeBWorK::ContentGenerator::ForgotPassword; | ||
| use Mojo::Base 'WeBWorK::ContentGenerator', -signatures; | ||
|
|
||
| =head1 NAME | ||
| WeBWorK::ContentGenerator::ForgotPassword - render the forgot password form | ||
| =cut | ||
|
|
||
| use Email::Stuffer; | ||
| use Email::Sender::Transport::SMTP; | ||
| use Digest::SHA qw(sha256_hex); | ||
| use Math::Random::Secure qw(irand); | ||
|
|
||
| use WeBWorK::Debug qw(debug); | ||
| use WeBWorK::Utils qw(createEmailSenderTransportSMTP); | ||
| use WeBWorK::Utils::Logs qw(writeCourseLog); | ||
|
|
||
| # Override the can method to disable links for the forgot password page. | ||
| sub can ($c, $arg) { | ||
| return $arg eq 'links' ? 0 : $c->SUPER::can($arg); | ||
| } | ||
|
|
||
| sub initialize ($c) { | ||
| my $ce = $c->ce; | ||
|
|
||
| $c->stash->{footerWidthClass} = 'col-xl-5 col-lg-6 col-md-7 col-sm-8'; | ||
|
|
||
| return unless $c->param('request_password_reset') && $ce->{password_reset_sender_email}; | ||
|
|
||
| my $user = $c->db->getUser($c->param('user')); | ||
|
|
||
| # Send a password reset email to the user if the user exists, the user has an email address set, the user has the | ||
| # permission to change their password, and the user already has a password (the user cannot create a password via | ||
| # the password reset flow). | ||
| return | ||
| unless $user | ||
| && $user->email_address =~ /\S/ | ||
| && $c->authz->hasPermissions($user->user_id, 'change_password'); | ||
|
|
||
| my $password = $c->db->getPassword($user->user_id); | ||
| return unless $password && $password->password; | ||
|
|
||
| my $resetToken = join('', map { [ 0 .. 9, 'a' .. 'z' ]->[ irand(36) ] } 1 .. 64); | ||
| $password->reset_token(sha256_hex($resetToken)); | ||
| $password->reset_token_expiration(time + 900); | ||
| eval { $c->db->putPassword($password) }; | ||
| if ($@) { | ||
| $c->log->error('Unable to save password reset token to database for user ' . $user->user_id . ": $@"); | ||
| return; | ||
| } | ||
|
|
||
| my $resetURL = $c->url_for('reset_password')->to_abs->query(user => $user->user_id, token => $resetToken); | ||
|
|
||
| my $email = | ||
| Email::Stuffer->to($user->rfc822_mailbox) | ||
| ->from($ce->{password_reset_sender_email}) | ||
| ->subject($c->maketext('Reset Password')) | ||
| ->text_body($c->maketext( | ||
| "Use the following link to reset your password: [_1]\n\n" | ||
| . "That link will only be valid for 15 minus (until [_2]).", | ||
| $resetURL, | ||
| $c->formatDateTime($password->reset_token_expiration, 'datetime_format_long') | ||
| )) | ||
| ->html_body($c->tag( | ||
| 'html', | ||
| $c->tag( | ||
| 'body', | ||
| $c->c( | ||
| $c->tag('div', $c->link_to($c->maketext('Reset password') => $resetURL)), | ||
| $c->tag( | ||
| 'div', | ||
| $c->maketext( | ||
| 'That link will only be valid for 15 minutes (until [_1]).', | ||
| $c->formatDateTime($password->reset_token_expiration, 'datetime_format_long') | ||
| ) | ||
| ) | ||
| )->join('') | ||
| ) | ||
| ))->header('X-Remote-Host' => $c->tx->remote_address || 'UNKNOWN'); | ||
|
|
||
| eval { | ||
| $email->send_or_die({ | ||
| transport => createEmailSenderTransportSMTP($ce), | ||
| $ce->{mail}{set_return_path} ? (from => $ce->{mail}{set_return_path}) : () | ||
| }); | ||
| debug 'Successfully sent password reset email to ' . $user->email_address; | ||
| }; | ||
| if ($@) { | ||
| my $exception_message = ref($@) ? $@->message : $@; | ||
| $c->log->error('Error sending password reset email to ' . $user->email_address . ": $exception_message"); | ||
| } | ||
|
|
||
| writeCourseLog($ce, 'login_log', 'PASSWORD RESET REQUEST user_id=' . $user->user_id); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| 1; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| package WeBWorK::ContentGenerator::ResetPassword; | ||
| use Mojo::Base 'WeBWorK::ContentGenerator', -signatures; | ||
|
|
||
| =head1 NAME | ||
|
|
||
| WeBWorK::ContentGenerator::ResetPassword - reset a user's password if requested to do so | ||
|
|
||
| =cut | ||
|
|
||
| use Digest::SHA qw(sha256_hex); | ||
|
|
||
| use WeBWorK::Utils qw(cryptPassword); | ||
| use WeBWorK::Utils::Logs qw(writeCourseLog); | ||
|
|
||
| # Override the can method to disable links for the password reset page. | ||
| sub can ($c, $arg) { | ||
| return $arg eq 'links' ? 0 : $c->SUPER::can($arg); | ||
| } | ||
|
|
||
| sub initialize ($c) { | ||
| $c->stash->{footerWidthClass} = 'col-xl-6 col-lg-7 col-md-8 col-12'; | ||
| $c->stash->{userID} = $c->param('user') // ''; | ||
| $c->stash->{errorMessage} = ''; | ||
| $c->stash->{entryErrorMessage} = ''; | ||
|
|
||
| unless ($c->ce->{password_reset_sender_email}) { | ||
| $c->stash->{errorMessage} = $c->maketext('Password reset is not enabled for this course.'); | ||
| return; | ||
| } | ||
|
|
||
| my $user = $c->db->getUser($c->stash->{userID}); | ||
| my $password = $c->db->getPassword($c->stash->{userID}); | ||
| unless ($user | ||
| && $password | ||
| && $password->password | ||
| && $password->reset_token | ||
| && $password->reset_token_expiration ne '' | ||
| && $password->reset_token_expiration > time | ||
| && sha256_hex($c->param('token') // '') eq $password->reset_token) | ||
| { | ||
| $c->deleteResetToken($password) if $password; | ||
| writeCourseLog($c->ce, 'login_log', | ||
| 'PASSWORD RESET FAILURE user_id=' . $c->stash->{userID} . ': Invalid user or token.') | ||
| if $c->stash->{userID}; | ||
| $c->stash->{errorMessage} = $c->maketext('An invalid or expired password reset URL was used.'); | ||
| return; | ||
| } | ||
|
|
||
| my $twoFactorAuthenticationEnabled = $c->ce->two_factor_authentication_enabled | ||
| && $c->authz->hasPermissions($user->user_id, 'use_two_factor_auth'); | ||
|
|
||
| if ($twoFactorAuthenticationEnabled && !$password->otp_secret) { | ||
| $c->deleteResetToken($password); | ||
| writeCourseLog($c->ce, 'login_log', | ||
| 'PASSWORD RESET FAILURE user_id=' . $user->user_id . ': Two factor authentication not enabled.'); | ||
| $c->stash->{errorMessage} = $c->maketext('Two factor authentication has not been set up for this account. ' | ||
| . 'Password reset is not allowed until that is done.'); | ||
| return; | ||
| } | ||
|
|
||
| return unless $c->param('reset_password'); | ||
|
|
||
| my $newPassword = $c->param('newPassword'); | ||
| unless ($newPassword && $newPassword =~ /\S/) { | ||
| $c->stash->{entryErrorMessage} = $c->maketext('You must enter a new password.'); | ||
| return; | ||
| } | ||
|
|
||
| if ($newPassword ne ($c->param('confirmPassword') // '')) { | ||
| $c->stash->{entryErrorMessage} = $c->maketext( | ||
| 'The passwords you entered in the "New Password" and "Confirm New Password" fields do not match. ' | ||
| . 'Please retype your new password and try again.',); | ||
| return; | ||
| } | ||
|
|
||
| if ($twoFactorAuthenticationEnabled) { | ||
| my $otpCode = ($c->param('otp_code') // '') =~ s/(^\s+|\s+$)//gr; | ||
| if (!$otpCode) { | ||
| $c->stash->{entryErrorMessage} = | ||
| $c->maketext('You must enter the security code from your authenticator app.'); | ||
| return; | ||
| } | ||
| if (!WeBWorK::Utils::TOTP->new(secret => $password->otp_secret, tolerance => 1)->validate_otp($otpCode)) { | ||
| $c->stash->{entryErrorMessage} = $c->maketext('You have entered an invalid one-time security code.'); | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| $password->password(cryptPassword($newPassword)); | ||
| $password->reset_token(undef); | ||
| $password->reset_token_expiration(undef); | ||
| eval { $c->db->putPassword($password) }; | ||
| if ($@) { | ||
| $c->log->error('Error resetting the password for ' . $user->user_id . ": $@"); | ||
| $c->stash->{errorMessage} = $c->maketext('Your password was not changed due to an internal error.'); | ||
| } | ||
|
|
||
| writeCourseLog($c->ce, 'login_log', 'PASSWORD RESET SUCCESS user_id=' . $user->user_id); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| sub deleteResetToken ($c, $password) { | ||
| $password->reset_token(undef); | ||
| $password->reset_token_expiration(undef); | ||
| eval { $c->db->putPassword($password) }; | ||
| return; | ||
| } | ||
|
|
||
| 1; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| <div class="row"> | ||
| % if (!$ce->{password_reset_sender_email}) { | ||
| <div class="col-xl-5 col-lg-6 col-md-7 col-sm-8"> | ||
| <div class="alert alert-danger"><%= maketext('Password reset is not enabled for this course.') %></div> | ||
| </div> | ||
| % } elsif (param('request_password_reset')) { | ||
| <div class="col-xl-5 col-lg-6 col-md-7 col-sm-8 my-3"> | ||
| <div class="alert alert-info"> | ||
| <%= maketext('If the username that was entered is valid, that user has a valid email address set, ' | ||
| . 'and that user has permission to change their password, then a reset link has been emailed ' | ||
| . 'to that address. That link will only be valid for the next 15 minutes.') %> | ||
| </div> | ||
| <%= link_to maketext('Login') => 'set_list', class => 'btn btn-primary' =%> | ||
| </div> | ||
| % } else { | ||
| <p> | ||
| <%== maketext('Please enter your username for [_1] below to reset your password:', tag('b', $courseID)) %> | ||
| </p> | ||
| <%= form_for current_route, method => 'POST', begin =%> | ||
| <div class="col-xl-5 col-lg-6 col-md-7 col-sm-8 my-3"> | ||
| <div class="form-floating mb-2"> | ||
| <%= text_field user => '', required => undef, class => 'form-control', placeholder => '', | ||
| autocapitalize => 'none', spellcheck => 'false' =%> | ||
| <%= label_for uname => maketext('Username') =%> | ||
| </div> | ||
| <div class="submit-buttons-container"> | ||
| <%= submit_button maketext('Request Password Reset'), name => 'request_password_reset', | ||
| class => 'btn btn-primary' =%> | ||
| <%= link_to maketext('Cancel') => 'set_list', class => 'btn btn-primary' =%> | ||
| </div> | ||
| </div> | ||
| <% end =%> | ||
| % } | ||
| </div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.