-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Allow password reset with token alone #1295
Merged
MaicolBen
merged 9 commits into
lynndylanhurley:master
from
jkeen:forgot_pass_with_reset_password_token
Sep 12, 2019
Merged
Changes from 6 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
be33d63
Allow user send new password with reset pasword token without auth he…
MaicolBen e5623c4
Handle case where require_client_password_reset_token is true and tok…
jkeen 0075413
Update create_token call that returns an object instead of an array
jkeen 5fe8cfe
Don't double parse the uri
jkeen 84c984d
Tidy up the code path for an easier read, and fix the test teardowns …
jkeen c93de06
Use faker for generating unique email addresses
jkeen a47a82b
Use built in DeviseTokenAuth::Url.generate instead of handrolled one.…
jkeen 56f1b1f
Remove line that was added to combat strange intermittent test valida…
jkeen c49d72e
Make sure generated email addresses are unique
jkeen 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 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 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 |
---|---|---|
|
@@ -2,12 +2,10 @@ | |
|
||
module DeviseTokenAuth | ||
class PasswordsController < DeviseTokenAuth::ApplicationController | ||
before_action :set_user_by_token, only: [:update] | ||
before_action :validate_redirect_url_param, only: [:create, :edit] | ||
skip_after_action :update_auth_header, only: [:create, :edit] | ||
|
||
# this action is responsible for generating password reset tokens and | ||
# sending emails | ||
# this action is responsible for generating password reset tokens and sending emails | ||
def create | ||
return render_create_error_missing_email unless resource_params[:email] | ||
|
||
|
@@ -39,31 +37,43 @@ def edit | |
@resource = resource_class.with_reset_password_token(resource_params[:reset_password_token]) | ||
|
||
if @resource && @resource.reset_password_period_valid? | ||
token = @resource.create_token | ||
token = @resource.create_token unless require_client_password_reset_token? | ||
|
||
# ensure that user is confirmed | ||
@resource.skip_confirmation! if confirmable_enabled? && [email protected]_at | ||
|
||
# allow user to change password once without current_password | ||
@resource.allow_password_change = true if recoverable_enabled? | ||
|
||
@resource.save! | ||
|
||
yield @resource if block_given? | ||
|
||
redirect_header_options = { reset_password: true } | ||
redirect_headers = build_redirect_headers(token.token, | ||
token.client, | ||
redirect_header_options) | ||
redirect_to(@resource.build_auth_url(@redirect_url, | ||
redirect_headers)) | ||
if require_client_password_reset_token? | ||
redirect_to build_callback_url(resource_params[:reset_password_token]) | ||
else | ||
redirect_header_options = { reset_password: true } | ||
redirect_headers = build_redirect_headers(token.token, | ||
token.client, | ||
redirect_header_options) | ||
redirect_to(@resource.build_auth_url(@redirect_url, | ||
redirect_headers)) | ||
end | ||
else | ||
render_edit_error | ||
end | ||
end | ||
|
||
def update | ||
# make sure user is authorized | ||
if require_client_password_reset_token? && resource_params[:reset_password_token] | ||
@resource = resource_class.with_reset_password_token(resource_params[:reset_password_token]) | ||
return render_update_error_unauthorized unless @resource | ||
|
||
@token = @resource.create_token | ||
else | ||
@resource = set_user_by_token | ||
end | ||
|
||
return render_update_error_unauthorized unless @resource | ||
|
||
# make sure account doesn't use oauth2 provider | ||
|
@@ -90,7 +100,7 @@ def update | |
protected | ||
|
||
def resource_update_method | ||
allow_password_change = recoverable_enabled? && @resource.allow_password_change == true | ||
allow_password_change = recoverable_enabled? && @resource.allow_password_change == true || require_client_password_reset_token? | ||
if DeviseTokenAuth.check_current_password_before_update == false || allow_password_change | ||
'update' | ||
else | ||
|
@@ -184,5 +194,23 @@ def validate_redirect_url_param | |
return render_create_error_missing_redirect_url unless @redirect_url | ||
return render_error_not_allowed_redirect_url if blacklisted_redirect_url? | ||
end | ||
|
||
def build_callback_url(reset_password_token) | ||
url = URI.parse(@redirect_url) | ||
query_params = Rack::Utils.parse_nested_query(url.query) | ||
query_params = query_params.merge(reset_password_token: reset_password_token) | ||
query_string = query_params.collect { |k, v| "#{k}=#{v}" }.join('&') | ||
url.query = query_string | ||
|
||
url.to_s | ||
end | ||
|
||
def reset_password_token_as_raw?(recoverable) | ||
recoverable && recoverable.reset_password_token.present? && !require_client_password_reset_token? | ||
end | ||
|
||
def require_client_password_reset_token? | ||
DeviseTokenAuth.require_client_password_reset_token | ||
end | ||
end | ||
end |
This file contains 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 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 |
---|---|---|
|
@@ -108,40 +108,6 @@ You may be interested in [GrapeTokenAuth](https://github.com/mcordell/grape_toke | |
|
||
You may be interested in [solidus_devise_token_auth](https://github.com/skycocker/solidus_devise_token_auth). | ||
|
||
### What's the reset password flow? | ||
|
||
This is the overall workflow for a User to reset their password: | ||
|
||
- user goes to a page on the front end site which contains a form with a single text field, they type their email address into this field and click a button to submit the form | ||
|
||
- that form submission sends a request to the API: `POST /auth/password` with some parameters: `email` (the email supplied in the field) & `redirect_url` (a page in the front end site that will contain a form with `password` and `password_confirmation` fields) | ||
|
||
- the API responds to this request by generating a `reset_password_token` and sending an email (the `reset_password_instructions.html.erb` file from devise) to the email address provided within the `email` parameter | ||
- we need to modify the `reset_password_instructions.html.erb` file to point to the API: `GET /auth/password/edit` | ||
- for example, if you have your API under the `api/v1` namespaces: `<%= link_to 'Change my password', edit_api_v1_user_password_url(reset_password_token: @token, config: message['client-config'].to_s, redirect_url: message['redirect-url'].to_s) %>` (I came up with this `link_to` by referring to [this line](https://github.com/lynndylanhurley/devise_token_auth/blob/15bf7857eca2d33602c7a9cb9d08db8a160f8ab8/app/views/devise/mailer/reset_password_instructions.html.erb#L5)) | ||
|
||
- the user clicks the link in the email, which brings them to the 'Verify user by password reset token' endpoint (`GET /password/edit`) | ||
|
||
- this endpoint verifies the user and redirects them to the `redirect_url` (or the one you set in an initializer as default_password_reset_url) with the auth headers if they are who they claim to be (if their `reset_password_token` matches a User record) | ||
|
||
- this `redirect_url` is a page on the frontend which contains a `password` and `password_confirmation` field | ||
|
||
- the user submits the form on this frontend page, which sends a request to API: `PUT /auth/password` with the `password` and `password_confirmation` parameters. In addition headers need to be included from the url params (you get these from the url as query params). A side note, ensure that the header names follow the convention outlined in `config/initializers/devise_token_auth.rb`; at this time of writing it is: `uid`, `client` and `access-token`. | ||
- _Ensure that the `uid` sent in the headers is not URL-escaped. e.g. it should be [email protected], not bob%40example.com_ | ||
|
||
- the API changes the user's password and responds back with a success message | ||
|
||
- the front end needs to manually redirect the user to its login page after receiving this success response | ||
|
||
- the user logs in | ||
|
||
The next diagram shows how it works: | ||
|
||
![password reset flow](password_diagram_reset.jpg) | ||
|
||
If you get in any trouble configuring or overriding the behavior, you can check the [issue #604](https://github.com/lynndylanhurley/devise_token_auth/issues/604). | ||
|
||
|
||
### I already have a user, how can I add the new fields? | ||
|
||
1. First, remove the migration generated by the following command`rails g devise_token_auth:install [USER_CLASS] [MOUNT_PATH]` and then:. | ||
|
This file contains 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,47 @@ | ||
## Reset password flow | ||
|
||
As a requirement you need to have `allow_password_change` field for any flow. There are 2 flows for reseting the password, one more web focused and other more mobile focused: | ||
|
||
### Main reset password flow (use auth headers) | ||
|
||
This is the overall workflow for a User to reset their password: | ||
|
||
- user goes to a page on the front end site which contains a form with a single text field, they type their email address into this field and click a button to submit the form | ||
|
||
- that form submission sends a request to the API: `POST /auth/password` with some parameters: `email` (the email supplied in the field) & `redirect_url` (a page in the front end site that will contain a form with `password` and `password_confirmation` fields) | ||
|
||
- the API responds to this request by generating a `reset_password_token` and sending an email (the `reset_password_instructions.html.erb` file from devise) to the email address provided within the `email` parameter | ||
- we need to modify the `reset_password_instructions.html.erb` file to point to the API: `GET /auth/password/edit` | ||
- for example, if you have your API under the `api/v1` namespaces: `<%= link_to 'Change my password', edit_api_v1_user_password_url(reset_password_token: @token, config: message['client-config'].to_s, redirect_url: message['redirect-url'].to_s) %>` (I came up with this `link_to` by referring to [this line](https://github.com/lynndylanhurley/devise_token_auth/blob/15bf7857eca2d33602c7a9cb9d08db8a160f8ab8/app/views/devise/mailer/reset_password_instructions.html.erb#L5)) | ||
|
||
- the user clicks the link in the email, which brings them to the 'Verify user by password reset token' endpoint (`GET /password/edit`) | ||
|
||
- this endpoint verifies the user and redirects them to the `redirect_url` (or the one you set in an initializer as default_password_reset_url) with the auth headers if they are who they claim to be (if their `reset_password_token` matches a User record) | ||
|
||
- this `redirect_url` is a page on the frontend which contains a `password` and `password_confirmation` field | ||
|
||
- the user submits the form on this frontend page, which sends a request to API: `PUT /auth/password` with the `password` and `password_confirmation` parameters. In addition headers need to be included from the url params (you get these from the url as query params). A side note, ensure that the header names follow the convention outlined in `config/initializers/devise_token_auth.rb`; at this time of writing it is: `uid`, `client` and `access-token`. | ||
- _Ensure that the `uid` sent in the headers is not URL-escaped. e.g. it should be [email protected], not bob%40example.com_ | ||
|
||
- the API changes the user's password and responds back with a success message | ||
|
||
- the front end needs to manually redirect the user to its login page after receiving this success response | ||
|
||
- the user logs in | ||
|
||
The next diagram shows how it works: | ||
|
||
![password reset flow](password_diagram_reset.jpg) | ||
|
||
If you get in any trouble configuring or overriding the behavior, you can check the [issue #604](https://github.com/lynndylanhurley/devise_token_auth/issues/604). | ||
|
||
### Mobile alternative flow (use reset_password_token) | ||
|
||
This flow is enabled with `require_client_password_reset_token` (by default is false), it is also useful for webs. This flow was done because the main one doesn't support deep linking (if you want to reset the password in the mobile app). It works like the main one but instead of receiving and sending the auth headers, you need to send the `reset_password_token`, but just in case, we can explain it step by step: | ||
|
||
1. User fills out password reset request form (this POST `/auth/password`) | ||
2. User is sent an email | ||
3. User clicks confirmation link (this GET `/auth/password/edit`) | ||
4. Link leads to the client to the `redirect_url` (instead of the API) with a `reset_password_token` | ||
5. User submits password along with reset_password_token (this PUT `/auth/password`) | ||
6. User is now authorized and has a new password |
This file contains 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 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 |
---|---|---|
|
@@ -239,10 +239,10 @@ class DeviseTokenAuth::PasswordsControllerTest < ActionController::TestCase | |
end | ||
end | ||
|
||
describe 'Cheking reset_password_token' do | ||
describe 'Checking reset_password_token' do | ||
before do | ||
post :create, params: { | ||
email: @resource.email, | ||
email: @resource.email, | ||
redirect_url: @redirect_url | ||
} | ||
|
||
|
@@ -440,6 +440,7 @@ class DeviseTokenAuth::PasswordsControllerTest < ActionController::TestCase | |
|
||
describe 'success' do | ||
before do | ||
DeviseTokenAuth.require_client_password_reset_token = false | ||
@auth_headers = @resource.create_new_auth_token | ||
request.headers.merge!(@auth_headers) | ||
@new_password = Faker::Internet.password | ||
|
@@ -504,6 +505,7 @@ class DeviseTokenAuth::PasswordsControllerTest < ActionController::TestCase | |
|
||
describe 'current password mismatch error' do | ||
before do | ||
DeviseTokenAuth.require_client_password_reset_token = false | ||
@auth_headers = @resource.create_new_auth_token | ||
request.headers.merge!(@auth_headers) | ||
@new_password = Faker::Internet.password | ||
|
@@ -520,7 +522,7 @@ class DeviseTokenAuth::PasswordsControllerTest < ActionController::TestCase | |
end | ||
|
||
describe 'change password' do | ||
describe 'success' do | ||
describe 'with valid headers' do | ||
before do | ||
@auth_headers = @resource.create_new_auth_token | ||
request.headers.merge!(@auth_headers) | ||
|
@@ -567,19 +569,93 @@ class DeviseTokenAuth::PasswordsControllerTest < ActionController::TestCase | |
end | ||
end | ||
|
||
describe 'unauthorized user' do | ||
describe 'without valid headers' do | ||
before do | ||
@auth_headers = @resource.create_new_auth_token | ||
@new_password = Faker::Internet.password | ||
@resource.create_new_auth_token | ||
new_password = Faker::Internet.password | ||
|
||
put :update, params: { password: @new_password, | ||
password_confirmation: @new_password } | ||
put :update, params: { password: new_password, | ||
password_confirmation: new_password } | ||
end | ||
|
||
test 'response should fail' do | ||
assert_equal 401, response.status | ||
end | ||
end | ||
|
||
describe 'with valid reset password token' do | ||
before do | ||
reset_password_token = @resource.send_reset_password_instructions | ||
@new_password = Faker::Internet.password | ||
@params = { password: @new_password, | ||
password_confirmation: @new_password, | ||
reset_password_token: reset_password_token } | ||
end | ||
|
||
describe 'with require_client_password_reset_token disabled' do | ||
before do | ||
DeviseTokenAuth.require_client_password_reset_token = false | ||
put :update, params: @params | ||
|
||
@data = JSON.parse(response.body) | ||
@resource.reload | ||
end | ||
|
||
test 'request should be not be successful' do | ||
assert_equal 401, response.status | ||
end | ||
end | ||
|
||
describe 'with require_client_password_reset_token enabled' do | ||
before do | ||
DeviseTokenAuth.require_client_password_reset_token = true | ||
put :update, params: @params | ||
|
||
@data = JSON.parse(response.body) | ||
@resource.reload | ||
end | ||
|
||
test 'request should be successful' do | ||
assert_equal 200, response.status | ||
end | ||
|
||
test 'request should return success message' do | ||
assert @data['message'] | ||
assert_equal @data['message'], | ||
I18n.t('devise_token_auth.passwords.successfully_updated') | ||
end | ||
|
||
test 'new password should authenticate user' do | ||
assert @resource.valid_password?(@new_password) | ||
end | ||
|
||
teardown do | ||
DeviseTokenAuth.require_client_password_reset_token = false | ||
end | ||
end | ||
end | ||
|
||
describe 'with invalid reset password token' do | ||
before do | ||
DeviseTokenAuth.require_client_password_reset_token = true | ||
@resource.update reset_password_token: 'koskoskoskos' | ||
put :update, params: @params | ||
@data = JSON.parse(response.body) | ||
@resource.reload | ||
end | ||
|
||
test 'request should fail' do | ||
assert_equal 401, response.status | ||
end | ||
|
||
test 'new password should not authenticate user' do | ||
assert [email protected]_password?(@new_password) | ||
end | ||
|
||
teardown do | ||
DeviseTokenAuth.require_client_password_reset_token = false | ||
end | ||
end | ||
end | ||
end | ||
|
||
|
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.
can't you use
DeviseTokenAuth::Url.generate
for this?