Skip to content

Commit

Permalink
Merge pull request #474 from paulosoares86/feature/token_invalidation
Browse files Browse the repository at this point in the history
removing old tokens when user changes passwords
  • Loading branch information
booleanbetrayal committed Dec 17, 2015
2 parents 118fdd2 + 6bff99b commit 57cb7f9
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 3 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
14 changes: 14 additions & 0 deletions app/models/devise_token_auth/concerns/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
4 changes: 3 additions & 1 deletion lib/devise_token_auth/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
42 changes: 42 additions & 0 deletions test/controllers/demo_user_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 57cb7f9

Please sign in to comment.