diff --git a/README.md b/README.md index 6aa3dd01b..2e3be0b72 100644 --- a/README.md +++ b/README.md @@ -134,14 +134,14 @@ The following routes are available for use by your client. These routes live rel |:-----|:-------|:--------| | / | POST | Email registration. Accepts **`email`**, **`password`**, and **`password_confirmation`** params. A verification email will be sent to the email address provided. Accepted params can be customized using the [`devise_parameter_sanitizer`](https://github.com/plataformatec/devise#strong-parameters) system. | | / | DELETE | Account deletion. This route will destroy users identified by their **`uid`** and **`auth_token`** headers. | -| / | PUT | Account updates. This route will update an existing user's account settings. The default accepted params are **`password`** and **`password_confirmation`**, but this can be customized using the [`devise_parameter_sanitizer`](https://github.com/plataformatec/devise#strong-parameters) system. | +| / | PUT | Account updates. This route will update an existing user's account settings. The default accepted params are **`password`** and **`password_confirmation`**, but this can be customized using the [`devise_parameter_sanitizer`](https://github.com/plataformatec/devise#strong-parameters) system. If **`config.check_current_password_before_update`** is set to `:attributes` the **`current_password`** param is checked before any update, if it is set to `:password` the **`current_password`** param is checked only if the request updates user password. | | /sign_in | POST | Email authentication. Accepts **`email`** and **`password`** as params. This route will return a JSON representation of the `User` model on successful login. | | /sign_out | DELETE | Use this route to end the user's current session. This route will invalidate the user's authentication token. | | /:provider | GET | Set this route as the destination for client authentication. Ideally this will happen in an external window or popup. [Read more](#omniauth-authentication). | | /:provider/callback | GET/POST | Destination for the oauth2 provider's callback uri. `postMessage` events containing the authenticated user's data will be sent back to the main client window from this page. [Read more](#omniauth-authentication). | | /validate_token | GET | Use this route to validate tokens on return visits to the client. Accepts **`uid`** and **`access-token`** as params. These values should correspond to the columns in your `User` table of the same names. | | /password | POST | Use this route to send a password reset confirmation email to users that registered by email. Accepts **`email`** and **`redirect_url`** as params. The user matching the `email` param will be sent instructions on how to reset their password. `redirect_url` is the url to which the user will be redirected after visiting the link contained in the email. | -| /password | PUT | Use this route to change users' passwords. Accepts **`password`** and **`password_confirmation`** as params. This route is only valid for users that registered by email (OAuth2 users will receive an error). | +| /password | PUT | Use this route to change users' passwords. Accepts **`password`** and **`password_confirmation`** as params. This route is only valid for users that registered by email (OAuth2 users will receive an error). It also checks **`current_password`** if **`config.check_current_password_before_update`** is not set `false` (disabled by default). | | /password/edit | GET | Verify user by password reset token. This route is the destination URL for password reset confirmation. This route must contain **`reset_password_token`** and **`redirect_url`** params. These values will be set automatically by the confirmation email that is generated by the password reset request. | [Jump here](#usage-cont) for more usage information. @@ -608,7 +608,7 @@ For example, the default behavior of the [`validate_token`](https://github.com/l ~~~ruby # config/routes.rb Rails.application.routes.draw do - ... + ... mount_devise_token_auth_for 'User', at: 'auth', controllers: { token_validations: 'overrides/token_validations' } diff --git a/app/controllers/devise_token_auth/passwords_controller.rb b/app/controllers/devise_token_auth/passwords_controller.rb index ea146cfe0..1e4c25fb4 100644 --- a/app/controllers/devise_token_auth/passwords_controller.rb +++ b/app/controllers/devise_token_auth/passwords_controller.rb @@ -148,7 +148,7 @@ def update }, status: 422 end - if @resource.update_attributes(password_resource_params) + if @resource.send(resource_update_method, password_resource_params) yield if block_given? return render json: { success: true, @@ -165,12 +165,20 @@ def update end end + def resource_update_method + if DeviseTokenAuth.check_current_password_before_update != false + "update_with_password" + else + "update_attributes" + end + end + def password_resource_params params.permit(devise_parameter_sanitizer.for(:account_update)) end def resource_params - params.permit(:email, :password, :password_confirmation, :reset_password_token) + params.permit(:email, :password, :password_confirmation, :current_password, :reset_password_token) end end diff --git a/app/controllers/devise_token_auth/registrations_controller.rb b/app/controllers/devise_token_auth/registrations_controller.rb index 4216f5198..2df300f6e 100644 --- a/app/controllers/devise_token_auth/registrations_controller.rb +++ b/app/controllers/devise_token_auth/registrations_controller.rb @@ -142,7 +142,11 @@ def account_update_params private def resource_update_method - if account_update_params.has_key?(:current_password) + if DeviseTokenAuth.check_current_password_before_update == :attributes + "update_with_password" + elsif DeviseTokenAuth.check_current_password_before_update == :password and account_update_params.has_key?(:password) + "update_with_password" + elsif account_update_params.has_key?(:current_password) "update_with_password" else "update_attributes" diff --git a/lib/devise_token_auth/engine.rb b/lib/devise_token_auth/engine.rb index c7703f769..baf126332 100644 --- a/lib/devise_token_auth/engine.rb +++ b/lib/devise_token_auth/engine.rb @@ -15,15 +15,17 @@ class Engine < ::Rails::Engine :omniauth_prefix, :default_confirm_success_url, :default_password_reset_url, - :redirect_whitelist + :redirect_whitelist, + :check_current_password_before_update - self.change_headers_on_each_request = true - self.token_lifespan = 2.weeks - self.batch_request_buffer_throttle = 5.seconds - self.omniauth_prefix = '/omniauth' - self.default_confirm_success_url = nil - self.default_password_reset_url = nil - self.redirect_whitelist = nil + self.change_headers_on_each_request = true + self.token_lifespan = 2.weeks + self.batch_request_buffer_throttle = 5.seconds + self.omniauth_prefix = '/omniauth' + self.default_confirm_success_url = nil + self.default_password_reset_url = nil + self.redirect_whitelist = nil + self.check_current_password_before_update = false def self.setup(&block) yield self diff --git a/lib/generators/devise_token_auth/templates/devise_token_auth.rb b/lib/generators/devise_token_auth/templates/devise_token_auth.rb index a34435e15..e158e784f 100644 --- a/lib/generators/devise_token_auth/templates/devise_token_auth.rb +++ b/lib/generators/devise_token_auth/templates/devise_token_auth.rb @@ -19,4 +19,10 @@ # example, using the default '/omniauth', the github oauth2 provider will # redirect successful authentications to '/omniauth/github/callback' #config.omniauth_prefix = "/omniauth" + + # By defult sending current password is not needed for the password update. + # Uncomment to enforce current_password param to be checked before all + # attribute updates. Set it to :password if you want it to be checked only if + # password is updated. + # config.check_current_password_before_update = :attributes end diff --git a/test/controllers/devise_token_auth/passwords_controller_test.rb b/test/controllers/devise_token_auth/passwords_controller_test.rb index 866023659..a2962143c 100644 --- a/test/controllers/devise_token_auth/passwords_controller_test.rb +++ b/test/controllers/devise_token_auth/passwords_controller_test.rb @@ -269,6 +269,56 @@ class DeviseTokenAuth::PasswordsControllerTest < ActionController::TestCase end end + describe "change password with current password required" do + before do + DeviseTokenAuth.check_current_password_before_update = :password + end + + after do + DeviseTokenAuth.check_current_password_before_update = false + end + + describe 'success' do + before do + @auth_headers = @resource.create_new_auth_token + request.headers.merge!(@auth_headers) + @new_password = Faker::Internet.password + @resource.update password: 'secret123', password_confirmation: 'secret123' + + xhr :put, :update, { + password: @new_password, + password_confirmation: @new_password, + current_password: 'secret123' + } + + @data = JSON.parse(response.body) + @resource.reload + end + + test "request should be successful" do + assert_equal 200, response.status + end + end + + describe 'current password mismatch error' do + before do + @auth_headers = @resource.create_new_auth_token + request.headers.merge!(@auth_headers) + @new_password = Faker::Internet.password + + xhr :put, :update, { + password: @new_password, + password_confirmation: @new_password, + current_password: 'not_very_secret321' + } + end + + test 'response should fail unauthorized' do + assert_equal 422, response.status + end + end + end + describe "change password" do describe 'success' do before do diff --git a/test/controllers/devise_token_auth/registrations_controller_test.rb b/test/controllers/devise_token_auth/registrations_controller_test.rb index fef52206e..574e437ff 100644 --- a/test/controllers/devise_token_auth/registrations_controller_test.rb +++ b/test/controllers/devise_token_auth/registrations_controller_test.rb @@ -472,101 +472,207 @@ class DeviseTokenAuth::RegistrationsControllerTest < ActionDispatch::Integration age_token(@existing_user, @client_id) end - describe "success" do - before do - # test valid update param - @resource_class = User - @new_operating_thetan = 1000000 - @email = "AlternatingCase2@example.com" - @request_params = { - operating_thetan: @new_operating_thetan, - email: @email - } + describe "without password check" do + describe "success" do + before do + # test valid update param + @resource_class = User + @new_operating_thetan = 1000000 + @email = "AlternatingCase2@example.com" + @request_params = { + operating_thetan: @new_operating_thetan, + email: @email + } + end + + test "Request was successful" do + put "/auth", @request_params, @auth_headers + assert_equal 200, response.status + end + + test "Case sensitive attributes update" do + @resource_class.case_insensitive_keys = [] + put "/auth", @request_params, @auth_headers + @data = JSON.parse(response.body) + @existing_user.reload + assert_equal @new_operating_thetan, @existing_user.operating_thetan + assert_equal @email, @existing_user.email + assert_equal @email, @existing_user.uid + end + + test "Case insensitive attributes update" do + @resource_class.case_insensitive_keys = [:email] + put "/auth", @request_params, @auth_headers + @data = JSON.parse(response.body) + @existing_user.reload + assert_equal @new_operating_thetan, @existing_user.operating_thetan + assert_equal @email.downcase, @existing_user.email + assert_equal @email.downcase, @existing_user.uid + end + + test "Supply current password" do + @request_params.merge!( + current_password: "secret123", + email: "new.email@example.com", + ) + + put "/auth", @request_params, @auth_headers + @data = JSON.parse(response.body) + @existing_user.reload + assert_equal @existing_user.email, "new.email@example.com" + end end - test "Request was successful" do - put "/auth", @request_params, @auth_headers - assert_equal 200, response.status - end + describe 'validate non-empty body' do + before do + # get the email so we can check it wasn't updated + @email = @existing_user.email + put '/auth', {}, @auth_headers - test "Case sensitive attributes update" do - @resource_class.case_insensitive_keys = [] - put "/auth", @request_params, @auth_headers - @data = JSON.parse(response.body) - @existing_user.reload - assert_equal @new_operating_thetan, @existing_user.operating_thetan - assert_equal @email, @existing_user.email - assert_equal @email, @existing_user.uid - end + @data = JSON.parse(response.body) + @existing_user.reload + end - test "Case insensitive attributes update" do - @resource_class.case_insensitive_keys = [:email] - put "/auth", @request_params, @auth_headers - @data = JSON.parse(response.body) - @existing_user.reload - assert_equal @new_operating_thetan, @existing_user.operating_thetan - assert_equal @email.downcase, @existing_user.email - assert_equal @email.downcase, @existing_user.uid - end + test 'request should fail' do + assert_equal 422, response.status + end + + test 'returns error message' do + assert_not_empty @data['errors'] + end - test "Supply current password" do - @request_params.merge!( - current_password: "secret123", - email: "new.email@example.com", - ) + test 'return error status' do + assert_equal 'error', @data['status'] + end - put "/auth", @request_params, @auth_headers - @data = JSON.parse(response.body) - @existing_user.reload - assert_equal @existing_user.email, "new.email@example.com" + test 'user should not have been saved' do + assert_equal @email, @existing_user.email + end + end + + describe "error" do + before do + # test invalid update param + @new_operating_thetan = "blegh" + put "/auth", { + operating_thetan: @new_operating_thetan + }, @auth_headers + + @data = JSON.parse(response.body) + @existing_user.reload + end + + test "Request was NOT successful" do + assert_equal 403, response.status + end + + test "Errors were provided with response" do + assert @data["errors"].length + end end end - describe 'validate non-empty body' do + describe "with password check for password update only" do before do - # get the email so we can check it wasn't updated - @email = @existing_user.email - put '/auth', {}, @auth_headers - - @data = JSON.parse(response.body) - @existing_user.reload + DeviseTokenAuth.check_current_password_before_update = :password end - test 'request should fail' do - assert_equal 422, response.status + after do + DeviseTokenAuth.check_current_password_before_update = false end - test 'returns error message' do - assert_not_empty @data['errors'] + describe "success without password update" do + before do + # test valid update param + @resource_class = User + @new_operating_thetan = 1000000 + @email = "AlternatingCase2@example.com" + @request_params = { + operating_thetan: @new_operating_thetan, + email: @email + } + end + + test "Request was successful" do + put "/auth", @request_params, @auth_headers + assert_equal 200, response.status + end end - test 'return error status' do - assert_equal 'error', @data['status'] + describe "success with password update" do + before do + @existing_user.update password: 'secret123', password_confirmation: 'secret123' + @request_params = { + password: 'the_new_secret456', + password_confirmation: 'the_new_secret456', + current_password: 'secret123' + } + end + + test "Request was successful" do + put "/auth", @request_params, @auth_headers + assert_equal 200, response.status + end end - test 'user should not have been saved' do - assert_equal @email, @existing_user.email + describe "error with password mismatch" do + before do + @existing_user.update password: 'secret123', password_confirmation: 'secret123' + @request_params = { + password: 'the_new_secret456', + password_confirmation: 'the_new_secret456', + current_password: 'not_so_secret321' + } + end + + test "Request was NOT successful" do + put "/auth", @request_params, @auth_headers + assert_equal 403, response.status + end end end - describe "error" do + describe "with password check for all attributes" do before do - # test invalid update param - @new_operating_thetan = "blegh" - put "/auth", { - operating_thetan: @new_operating_thetan - }, @auth_headers - - @data = JSON.parse(response.body) - @existing_user.reload + DeviseTokenAuth.check_current_password_before_update = :password + @new_operating_thetan = 1000000 + @email = "AlternatingCase2@example.com" + end + + after do + DeviseTokenAuth.check_current_password_before_update = false end - test "Request was NOT successful" do - assert_equal 403, response.status + describe "success with password update" do + before do + @existing_user.update password: 'secret123', password_confirmation: 'secret123' + @request_params = { + operating_thetan: @new_operating_thetan, + email: @email, + current_password: 'secret123' + } + end + + test "Request was successful" do + put "/auth", @request_params, @auth_headers + assert_equal 200, response.status + end end - test "Errors were provided with response" do - assert @data["errors"].length + describe "error with password mismatch" do + before do + @existing_user.update password: 'secret123', password_confirmation: 'secret123' + @request_params = { + operating_thetan: @new_operating_thetan, + email: @email, + current_password: 'not_so_secret321' + } + end + + test "Request was NOT successful" do + put "/auth", @request_params, @auth_headers + assert_equal 403, response.status + end end end end