diff --git a/README.md b/README.md index ddcf52014..0f64e710c 100644 --- a/README.md +++ b/README.md @@ -161,10 +161,11 @@ The following settings are available for configuration in `config/initializers/d | **`token_lifespan`** | `2.weeks` | Set the length of your tokens' lifespans. Users will need to re-authenticate after this duration of time has passed since their last login. | | **`batch_request_buffer_throttle`** | `5.seconds` | Sometimes it's necessary to make several requests to the API at the same time. In this case, each request in the batch will need to share the same auth token. This setting determines how far apart the requests can be while still using the same auth token. [Read more](#about-batch-requests). | | **`omniauth_prefix`** | `"/omniauth"` | This route will be the prefix for all oauth2 redirect callbacks. For example, using the default '/omniauth' setting, the github oauth2 provider will redirect successful authentications to '/omniauth/github/callback'. [Read more](#omniauth-provider-settings). | -| **`default_confirm_success_url`** | `nil` | By default this value is expected to be sent by the client so that the API knows where to redirect users after successful email confirmation. If this param is set, the API will redirect to this value when no value is provided by the cilent. | -| **`default_password_reset_url`** | `nil` | By default this value is expected to be sent by the client so that the API knows where to redirect users after successful password resets. If this param is set, the API will redirect to this value when no value is provided by the cilent. | +| **`default_confirm_success_url`** | `nil` | By default this value is expected to be sent by the client so that the API knows where to redirect users after successful email confirmation. If this param is set, the API will redirect to this value when no value is provided by the client. | +| **`default_password_reset_url`** | `nil` | By default this value is expected to be sent by the client so that the API knows where to redirect users after successful password resets. If this param is set, the API will redirect to this value when no value is provided by the client. | | **`redirect_whitelist`** | `nil` | As an added security measure, you can limit the URLs to which the API will redirect after email token validation (password reset, email confirmation, etc.). This value should be an array containing exact matches to the client URLs to be visited after validation. | | **`enable_standard_devise_support`** | `false` | By default, only Bearer Token authentication is implemented out of the box. If, however, you wish to integrate with legacy Devise authentication, you can do so by enabling this flag. NOTE: This feature is highly experimental! | +| **`remove_tokens_after_password_reset`** | `false` | By default, old tokens are not invalidated when password is changed. Enable this option if you want to make passwords updates to logout other devices. | Additionally, you can configure other aspects of devise by manually creating the traditional devise.rb file at `config/initializers/devise.rb`. Here are some examples of what you can do in this file: diff --git a/app/models/devise_token_auth/concerns/user.rb b/app/models/devise_token_auth/concerns/user.rb index ea841e0ee..3dcc5a02b 100644 --- a/app/models/devise_token_auth/concerns/user.rb +++ b/app/models/devise_token_auth/concerns/user.rb @@ -44,6 +44,9 @@ def self.tokens_match?(token_hash, token) # get rid of dead tokens before_save :destroy_expired_tokens + # remove old tokens if password has changed + before_save :remove_tokens_after_password_reset + # allows user to change password without current_password attr_writer :allow_password_change def allow_password_change @@ -260,4 +263,15 @@ def destroy_expired_tokens end end + def remove_tokens_after_password_reset + there_is_more_than_one_token = self.tokens && self.tokens.keys.length > 1 + should_remove_old_tokens = DeviseTokenAuth.remove_tokens_after_password_reset && + encrypted_password_changed? && there_is_more_than_one_token + + if should_remove_old_tokens + latest_token = self.tokens.max_by { |cid, v| v[:expiry] || v["expiry"] } + self.tokens = {latest_token.first => latest_token.last} + end + end + end diff --git a/lib/devise_token_auth/engine.rb b/lib/devise_token_auth/engine.rb index a88bbaa54..fc1cbd855 100644 --- a/lib/devise_token_auth/engine.rb +++ b/lib/devise_token_auth/engine.rb @@ -18,7 +18,8 @@ class Engine < ::Rails::Engine :default_password_reset_url, :redirect_whitelist, :check_current_password_before_update, - :enable_standard_devise_support + :enable_standard_devise_support, + :remove_tokens_after_password_reset self.change_headers_on_each_request = true self.max_number_of_devices = 10 @@ -30,6 +31,7 @@ class Engine < ::Rails::Engine self.redirect_whitelist = nil self.check_current_password_before_update = false self.enable_standard_devise_support = false + self.remove_tokens_after_password_reset = false def self.setup(&block) yield self diff --git a/test/controllers/demo_user_controller_test.rb b/test/controllers/demo_user_controller_test.rb index d10b12a25..cfa6c206f 100644 --- a/test/controllers/demo_user_controller_test.rb +++ b/test/controllers/demo_user_controller_test.rb @@ -284,6 +284,48 @@ class DemoUserControllerTest < ActionDispatch::IntegrationTest end end + describe 'successful password change' do + before do + DeviseTokenAuth.remove_tokens_after_password_reset = true + + # adding one more token to simulate another logged in device + @old_auth_headers = @auth_headers + @auth_headers = @resource.create_new_auth_token + age_token(@resource, @client_id) + assert @resource.tokens.count > 1 + + # password changed from new device + @resource.update_attributes({ + password: 'newsecret123', + password_confirmation: 'newsecret123' + }) + + get '/demo/members_only', {}, @auth_headers + end + + after do + DeviseTokenAuth.remove_tokens_after_password_reset = false + end + + it 'should have only one token' do + assert_equal 1, @resource.tokens.count + end + + it 'new request should be successful' do + assert 200, response.status + end + + describe 'another device should not be abble to login' do + + it 'should return forbidden status' do + get '/demo/members_only', {}, @old_auth_headers + assert 401, response.status + end + + end + + end + end describe 'enable_standard_devise_support' do