From c5043c760c99d5a94b11844bb278f6a69c5240de Mon Sep 17 00:00:00 2001 From: Matt Langston Date: Mon, 4 Jan 2021 09:01:49 -0600 Subject: [PATCH 1/7] Add support for sending and receiving the token via a cookie --- .gitignore | 1 + .../concerns/set_user_by_token.rb | 29 ++++++++++++++++--- .../devise_token_auth/sessions_controller.rb | 10 +++++++ lib/devise_token_auth/engine.rb | 6 ++++ 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index c46971770..650982cc0 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ test/dummy/.sass-cache test/dummy/config/application.yml coverage .idea +.byebug_history .irb_history .ruby-version .ruby-gemset diff --git a/app/controllers/devise_token_auth/concerns/set_user_by_token.rb b/app/controllers/devise_token_auth/concerns/set_user_by_token.rb index c03bca6ba..a829be8ed 100644 --- a/app/controllers/devise_token_auth/concerns/set_user_by_token.rb +++ b/app/controllers/devise_token_auth/concerns/set_user_by_token.rb @@ -35,11 +35,18 @@ def set_user_by_token(mapping = nil) access_token_name = DeviseTokenAuth.headers_names[:'access-token'] client_name = DeviseTokenAuth.headers_names[:'client'] + # gets values from cookie if configured + auth_cookie_name = DeviseTokenAuth.cookie_config[:name] + auth_cookie = {} + if DeviseTokenAuth.cookie_config[:enabled] && request.cookies[auth_cookie_name].present? + auth_cookie = JSON.parse(request.cookies[auth_cookie_name]) + end + # parse header for values necessary for authentication - uid = request.headers[uid_name] || params[uid_name] + uid = request.headers[uid_name] || params[uid_name] || auth_cookie[uid_name] @token = DeviseTokenAuth::TokenFactory.new unless @token - @token.token ||= request.headers[access_token_name] || params[access_token_name] - @token.client ||= request.headers[client_name] || params[client_name] + @token.token ||= request.headers[access_token_name] || params[access_token_name] || auth_cookie[access_token_name] + @token.client ||= request.headers[client_name] || params[client_name] || auth_cookie[client_name] # client isn't required, set to 'default' if absent @token.client ||= 'default' @@ -101,6 +108,12 @@ def update_auth_header # update the response header response.headers.merge!(auth_header) + # set a server cookie if configured + if DeviseTokenAuth.cookie_config[:enabled] + auth_cookie_name = DeviseTokenAuth.cookie_config[:name] + cookies[auth_cookie_name] = DeviseTokenAuth.cookie_config[:attributes].merge(value: auth_header.to_json) + end + else unless @resource.reload.valid? @resource = @resource.class.find(@resource.to_param) # errors remain after reload @@ -123,8 +136,16 @@ def refresh_headers # cleared by sign out in the meantime return if @used_auth_by_token && @resource.tokens[@token.client].nil? + _auth_header_from_batch_request = auth_header_from_batch_request + # update the response header - response.headers.merge!(auth_header_from_batch_request) + response.headers.merge!(_auth_header_from_batch_request) + + # set a server cookie if configured + if DeviseTokenAuth.cookie_config[:enabled] + auth_cookie_name = DeviseTokenAuth.cookie_config[:name] + cookies[auth_cookie_name] = DeviseTokenAuth.cookie_config[:attributes].merge(value: _auth_header_from_batch_request.to_json) + end end # end lock end diff --git a/app/controllers/devise_token_auth/sessions_controller.rb b/app/controllers/devise_token_auth/sessions_controller.rb index 5d34eaf69..711054406 100644 --- a/app/controllers/devise_token_auth/sessions_controller.rb +++ b/app/controllers/devise_token_auth/sessions_controller.rb @@ -55,6 +55,16 @@ def destroy user.tokens.delete(client) user.save! + if DeviseTokenAuth.cookie_config[:enabled] + if DeviseTokenAuth.cookie_config[:attributes][:domain] + # If a cookie is set with a domain specified then it must be deleted with that domain specified + # See https://stackoverflow.com/a/6244724/1747491 + cookies.delete(DeviseTokenAuth.cookie_config[:name], domain: DeviseTokenAuth.cookie_config[:attributes][:domain]) + else + cookies.delete(DeviseTokenAuth.cookie_config[:name]) + end + end + yield user if block_given? render_destroy_success diff --git a/lib/devise_token_auth/engine.rb b/lib/devise_token_auth/engine.rb index 4e8bc8ab6..782d59eca 100644 --- a/lib/devise_token_auth/engine.rb +++ b/lib/devise_token_auth/engine.rb @@ -25,6 +25,7 @@ class Engine < ::Rails::Engine :remove_tokens_after_password_reset, :default_callbacks, :headers_names, + :cookie_config, :bypass_sign_in, :send_confirmation_email, :require_client_password_reset_token @@ -47,6 +48,11 @@ class Engine < ::Rails::Engine 'expiry': 'expiry', 'uid': 'uid', 'token-type': 'token-type' } + self.cookie_config = { + enabled: false, + name: 'auth_cookie', + attributes: {} + } self.bypass_sign_in = true self.send_confirmation_email = false self.require_client_password_reset_token = false From d20d24fc47bd35b492976c029838bec9687cfafb Mon Sep 17 00:00:00 2001 From: Matt Langston Date: Mon, 4 Jan 2021 11:56:23 -0600 Subject: [PATCH 2/7] tweak config and add to docs --- .../devise_token_auth/concerns/set_user_by_token.rb | 8 ++++---- app/controllers/devise_token_auth/sessions_controller.rb | 2 +- docs/config/initialization.md | 2 ++ lib/devise_token_auth/engine.rb | 8 +++----- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/controllers/devise_token_auth/concerns/set_user_by_token.rb b/app/controllers/devise_token_auth/concerns/set_user_by_token.rb index a829be8ed..961287fb0 100644 --- a/app/controllers/devise_token_auth/concerns/set_user_by_token.rb +++ b/app/controllers/devise_token_auth/concerns/set_user_by_token.rb @@ -36,9 +36,9 @@ def set_user_by_token(mapping = nil) client_name = DeviseTokenAuth.headers_names[:'client'] # gets values from cookie if configured - auth_cookie_name = DeviseTokenAuth.cookie_config[:name] auth_cookie = {} - if DeviseTokenAuth.cookie_config[:enabled] && request.cookies[auth_cookie_name].present? + if DeviseTokenAuth.cookie_enabled && request.cookies[auth_cookie_name].present? + auth_cookie_name = DeviseTokenAuth.cookie_config[:name] auth_cookie = JSON.parse(request.cookies[auth_cookie_name]) end @@ -109,7 +109,7 @@ def update_auth_header response.headers.merge!(auth_header) # set a server cookie if configured - if DeviseTokenAuth.cookie_config[:enabled] + if DeviseTokenAuth.cookie_enabled auth_cookie_name = DeviseTokenAuth.cookie_config[:name] cookies[auth_cookie_name] = DeviseTokenAuth.cookie_config[:attributes].merge(value: auth_header.to_json) end @@ -142,7 +142,7 @@ def refresh_headers response.headers.merge!(_auth_header_from_batch_request) # set a server cookie if configured - if DeviseTokenAuth.cookie_config[:enabled] + if DeviseTokenAuth.cookie_enabled auth_cookie_name = DeviseTokenAuth.cookie_config[:name] cookies[auth_cookie_name] = DeviseTokenAuth.cookie_config[:attributes].merge(value: _auth_header_from_batch_request.to_json) end diff --git a/app/controllers/devise_token_auth/sessions_controller.rb b/app/controllers/devise_token_auth/sessions_controller.rb index 711054406..ed7406e96 100644 --- a/app/controllers/devise_token_auth/sessions_controller.rb +++ b/app/controllers/devise_token_auth/sessions_controller.rb @@ -55,7 +55,7 @@ def destroy user.tokens.delete(client) user.save! - if DeviseTokenAuth.cookie_config[:enabled] + if DeviseTokenAuth.cookie_enabled if DeviseTokenAuth.cookie_config[:attributes][:domain] # If a cookie is set with a domain specified then it must be deleted with that domain specified # See https://stackoverflow.com/a/6244724/1747491 diff --git a/docs/config/initialization.md b/docs/config/initialization.md index aa6b35866..8d370dd00 100644 --- a/docs/config/initialization.md +++ b/docs/config/initialization.md @@ -15,6 +15,8 @@ The following settings are available for configuration in `config/initializers/d | **`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. | | **`default_callbacks`** (`true`) | By default User model will include the `DeviseTokenAuth::Concerns::UserOmniauthCallbacks` concern, which has `email`, `uid` validations & `uid` synchronization callbacks. | +| **`cookie_enabled` (`false`)** | By default DeviseTokenAuth will not send nor receive the token in a cookie. Set this to `true` and optionally set `cookie_config` to transfer the token via a cookie. +| **`cookie_config`** | If enabling transfer of the token via a cookie then this config can be used to set the cookie name (which defaults to `'auth_header'`) and attributes (ex. `domain`, `secure`, `httponly`, `same_site`, `expires`, `encrypt`). See [this MDN doc](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) for additional documentation on cookie attributes. | **`bypass_sign_in`** (`true`) | By default DeviseTokenAuth will not check user's `#active_for_authentication?` which includes confirmation check on each call (it will do it only on sign in). If you want it to be validated on each request (for example, to be able to deactivate logged in users on the fly), set it to false. | | **`send_confirmation_email`** (`false`) | By default DeviseTokenAuth will not send confirmation email, even when including devise confirmable module. If you want to use devise confirmable module and send email, set it to true. (This is a setting for compatibility) | | **`require_client_password_reset_token`** (`false`) | By default, the password-reset confirmation link redirects to the client with valid session credentials as querystring params. With this option enabled, the redirect will NOT include the valid session credentials. Instead the redirect will include a password_reset_token querystring param that can be used to reset the users password. Once the user has reset their password, the password-reset success response headers will contain valid session credentials. | diff --git a/lib/devise_token_auth/engine.rb b/lib/devise_token_auth/engine.rb index 782d59eca..5690f7ea3 100644 --- a/lib/devise_token_auth/engine.rb +++ b/lib/devise_token_auth/engine.rb @@ -25,6 +25,7 @@ class Engine < ::Rails::Engine :remove_tokens_after_password_reset, :default_callbacks, :headers_names, + :cookie_enabled, :cookie_config, :bypass_sign_in, :send_confirmation_email, @@ -48,11 +49,8 @@ class Engine < ::Rails::Engine 'expiry': 'expiry', 'uid': 'uid', 'token-type': 'token-type' } - self.cookie_config = { - enabled: false, - name: 'auth_cookie', - attributes: {} - } + self.cookie_enabled = false + self.cookie_config = { name: 'auth_cookie', attributes: {} } self.bypass_sign_in = true self.send_confirmation_email = false self.require_client_password_reset_token = false From c623d51dfbf633d9667177601442152b9ae08994 Mon Sep 17 00:00:00 2001 From: Matt Langston Date: Mon, 4 Jan 2021 20:04:02 -0600 Subject: [PATCH 3/7] test work and flatten cookie config --- .../concerns/set_user_by_token.rb | 11 ++--- .../devise_token_auth/sessions_controller.rb | 6 +-- docs/config/initialization.md | 5 +- lib/devise_token_auth/engine.rb | 6 ++- .../registrations_controller_test.rb | 43 ++++++++++------ .../sessions_controller_test.rb | 49 +++++++++++++++---- 6 files changed, 81 insertions(+), 39 deletions(-) diff --git a/app/controllers/devise_token_auth/concerns/set_user_by_token.rb b/app/controllers/devise_token_auth/concerns/set_user_by_token.rb index 961287fb0..14744b32c 100644 --- a/app/controllers/devise_token_auth/concerns/set_user_by_token.rb +++ b/app/controllers/devise_token_auth/concerns/set_user_by_token.rb @@ -37,9 +37,8 @@ def set_user_by_token(mapping = nil) # gets values from cookie if configured auth_cookie = {} - if DeviseTokenAuth.cookie_enabled && request.cookies[auth_cookie_name].present? - auth_cookie_name = DeviseTokenAuth.cookie_config[:name] - auth_cookie = JSON.parse(request.cookies[auth_cookie_name]) + if DeviseTokenAuth.cookie_enabled + auth_cookie = JSON.parse(request.cookies[DeviseTokenAuth.cookie_name]) end # parse header for values necessary for authentication @@ -110,8 +109,7 @@ def update_auth_header # set a server cookie if configured if DeviseTokenAuth.cookie_enabled - auth_cookie_name = DeviseTokenAuth.cookie_config[:name] - cookies[auth_cookie_name] = DeviseTokenAuth.cookie_config[:attributes].merge(value: auth_header.to_json) + cookies[DeviseTokenAuth.cookie_name] = DeviseTokenAuth.cookie_attributes.merge(value: auth_header.to_json) end else @@ -143,8 +141,7 @@ def refresh_headers # set a server cookie if configured if DeviseTokenAuth.cookie_enabled - auth_cookie_name = DeviseTokenAuth.cookie_config[:name] - cookies[auth_cookie_name] = DeviseTokenAuth.cookie_config[:attributes].merge(value: _auth_header_from_batch_request.to_json) + cookies[DeviseTokenAuth.cookie_name] = DeviseTokenAuth.cookie_attributes.merge(value: _auth_header_from_batch_request.to_json) end end # end lock end diff --git a/app/controllers/devise_token_auth/sessions_controller.rb b/app/controllers/devise_token_auth/sessions_controller.rb index ed7406e96..a141dd00c 100644 --- a/app/controllers/devise_token_auth/sessions_controller.rb +++ b/app/controllers/devise_token_auth/sessions_controller.rb @@ -56,12 +56,12 @@ def destroy user.save! if DeviseTokenAuth.cookie_enabled - if DeviseTokenAuth.cookie_config[:attributes][:domain] + if DeviseTokenAuth.cookie_attributes[:domain] # If a cookie is set with a domain specified then it must be deleted with that domain specified # See https://stackoverflow.com/a/6244724/1747491 - cookies.delete(DeviseTokenAuth.cookie_config[:name], domain: DeviseTokenAuth.cookie_config[:attributes][:domain]) + cookies.delete(DeviseTokenAuth.cookie_name, domain: DeviseTokenAuth.cookie_attributes[:domain]) else - cookies.delete(DeviseTokenAuth.cookie_config[:name]) + cookies.delete(DeviseTokenAuth.cookie_name) end end diff --git a/docs/config/initialization.md b/docs/config/initialization.md index 8d370dd00..386c93828 100644 --- a/docs/config/initialization.md +++ b/docs/config/initialization.md @@ -15,8 +15,9 @@ The following settings are available for configuration in `config/initializers/d | **`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. | | **`default_callbacks`** (`true`) | By default User model will include the `DeviseTokenAuth::Concerns::UserOmniauthCallbacks` concern, which has `email`, `uid` validations & `uid` synchronization callbacks. | -| **`cookie_enabled` (`false`)** | By default DeviseTokenAuth will not send nor receive the token in a cookie. Set this to `true` and optionally set `cookie_config` to transfer the token via a cookie. -| **`cookie_config`** | If enabling transfer of the token via a cookie then this config can be used to set the cookie name (which defaults to `'auth_header'`) and attributes (ex. `domain`, `secure`, `httponly`, `same_site`, `expires`, `encrypt`). See [this MDN doc](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) for additional documentation on cookie attributes. +| **`cookie_enabled`** (`false`) | Specifies if DeviseTokenAuth should send and receive the auth token in a cookie. +| **`cookie_name`** (`"auth_cookie"`) | Sets the name of the cookie containing the auth token. +| **`cookie_attributes`** (`{}`) | Sets attributes on the cookie containing the auth token (ex. `domain`, `secure`, `httponly`, `same_site`, `expires`, `encrypt`). See [this MDN doc](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) for additional documentation on cookie attributes. | **`bypass_sign_in`** (`true`) | By default DeviseTokenAuth will not check user's `#active_for_authentication?` which includes confirmation check on each call (it will do it only on sign in). If you want it to be validated on each request (for example, to be able to deactivate logged in users on the fly), set it to false. | | **`send_confirmation_email`** (`false`) | By default DeviseTokenAuth will not send confirmation email, even when including devise confirmable module. If you want to use devise confirmable module and send email, set it to true. (This is a setting for compatibility) | | **`require_client_password_reset_token`** (`false`) | By default, the password-reset confirmation link redirects to the client with valid session credentials as querystring params. With this option enabled, the redirect will NOT include the valid session credentials. Instead the redirect will include a password_reset_token querystring param that can be used to reset the users password. Once the user has reset their password, the password-reset success response headers will contain valid session credentials. | diff --git a/lib/devise_token_auth/engine.rb b/lib/devise_token_auth/engine.rb index 5690f7ea3..e6a921aa9 100644 --- a/lib/devise_token_auth/engine.rb +++ b/lib/devise_token_auth/engine.rb @@ -26,7 +26,8 @@ class Engine < ::Rails::Engine :default_callbacks, :headers_names, :cookie_enabled, - :cookie_config, + :cookie_name, + :cookie_attributes, :bypass_sign_in, :send_confirmation_email, :require_client_password_reset_token @@ -50,7 +51,8 @@ class Engine < ::Rails::Engine 'uid': 'uid', 'token-type': 'token-type' } self.cookie_enabled = false - self.cookie_config = { name: 'auth_cookie', attributes: {} } + self.cookie_name = 'auth_cookie' + self.cookie_attributes = {} self.bypass_sign_in = true self.send_confirmation_email = false self.require_client_password_reset_token = false diff --git a/test/controllers/devise_token_auth/registrations_controller_test.rb b/test/controllers/devise_token_auth/registrations_controller_test.rb index 718b5897b..8a0eadf4e 100644 --- a/test/controllers/devise_token_auth/registrations_controller_test.rb +++ b/test/controllers/devise_token_auth/registrations_controller_test.rb @@ -10,6 +10,17 @@ class DeviseTokenAuth::RegistrationsControllerTest < ActionDispatch::IntegrationTest describe DeviseTokenAuth::RegistrationsController do + + def mock_registration_params + { + email: Faker::Internet.email, + password: 'secret123', + password_confirmation: 'secret123', + confirm_success_url: Faker::Internet.url, + unpermitted_param: '(x_x)' + } + end + describe 'Validate non-empty body' do before do # need to post empty data @@ -41,13 +52,7 @@ class DeviseTokenAuth::RegistrationsControllerTest < ActionDispatch::Integration @mails_sent = ActionMailer::Base.deliveries.count post '/auth', - params: { - email: Faker::Internet.email, - password: 'secret123', - password_confirmation: 'secret123', - confirm_success_url: Faker::Internet.url, - unpermitted_param: '(x_x)' - } + params: mock_registration_params @resource = assigns(:resource) @data = JSON.parse(response.body) @@ -87,17 +92,10 @@ class DeviseTokenAuth::RegistrationsControllerTest < ActionDispatch::Integration before do @original_duration = Devise.allow_unconfirmed_access_for Devise.allow_unconfirmed_access_for = nil - post '/auth', - params: { - email: Faker::Internet.email, - password: 'secret123', - password_confirmation: 'secret123', - confirm_success_url: Faker::Internet.url, - unpermitted_param: '(x_x)' - } end test 'auth headers were returned in response' do + post '/auth', params: mock_registration_params assert response.headers['access-token'] assert response.headers['token-type'] assert response.headers['client'] @@ -105,6 +103,21 @@ class DeviseTokenAuth::RegistrationsControllerTest < ActionDispatch::Integration assert response.headers['uid'] end + describe 'using auth cookie' do + before do + DeviseTokenAuth.cookie_enabled = true + end + + test 'auth cookie was returned in response' do + post '/auth', params: mock_registration_params + assert response.cookies[DeviseTokenAuth.cookie_name] + end + + after do + DeviseTokenAuth.cookie_enabled = false + end + end + after do Devise.allow_unconfirmed_access_for = @original_duration end diff --git a/test/controllers/devise_token_auth/sessions_controller_test.rb b/test/controllers/devise_token_auth/sessions_controller_test.rb index 507daf102..8a2a45b61 100644 --- a/test/controllers/devise_token_auth/sessions_controller_test.rb +++ b/test/controllers/devise_token_auth/sessions_controller_test.rb @@ -17,11 +17,12 @@ class DeviseTokenAuth::SessionsControllerTest < ActionController::TestCase describe 'success' do before do - post :create, - params: { - email: @existing_user.email, - password: @existing_user.password - } + @user_session_params = { + email: @existing_user.email, + password: @existing_user.password + } + + post :create, params: @user_session_params @resource = assigns(:resource) @data = JSON.parse(response.body) @@ -35,17 +36,27 @@ class DeviseTokenAuth::SessionsControllerTest < ActionController::TestCase assert_equal @existing_user.email, @data['data']['email'] end + describe 'using auth cookie' do + before do + DeviseTokenAuth.cookie_enabled = true + end + + test 'request should return auth cookie' do + post :create, params: @user_session_params + assert response.cookies[DeviseTokenAuth.cookie_name] + end + + after do + DeviseTokenAuth.cookie_enabled = false + end + end + describe "with multiple clients and headers don't change in each request" do before do # Set the max_number_of_devices to a lower number # to expedite tests! (Default is 10) DeviseTokenAuth.max_number_of_devices = 2 DeviseTokenAuth.change_headers_on_each_request = false - - @user_session_params = { - email: @existing_user.email, - password: @existing_user.password - } end test 'should limit the maximum number of concurrent devices' do @@ -159,6 +170,24 @@ def @controller.reset_session test 'session was destroyed' do assert_equal true, @controller.reset_session_called end + + describe 'using auth cookie' do + before do + DeviseTokenAuth.cookie_enabled = true + @auth_token = @existing_user.create_new_auth_token + @controller.send(:cookies)[DeviseTokenAuth.cookie_name] = { value: @auth_token.to_json } + end + + test 'auth cookie was destroyed' do + assert_equal @auth_token.to_json, @controller.send(:cookies)[DeviseTokenAuth.cookie_name] # sanity check + delete :destroy, format: :json + assert_nil @controller.send(:cookies)[DeviseTokenAuth.cookie_name] + end + + after do + DeviseTokenAuth.cookie_enabled = false + end + end end describe 'unauthed user sign out' do From 3cdc09bdcfe29d57e489a212584858911c5252d0 Mon Sep 17 00:00:00 2001 From: Matt Langston Date: Tue, 5 Jan 2021 09:01:14 -0600 Subject: [PATCH 4/7] be a little more defensive --- .../concerns/set_user_by_token.rb | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/controllers/devise_token_auth/concerns/set_user_by_token.rb b/app/controllers/devise_token_auth/concerns/set_user_by_token.rb index 14744b32c..58b0b8604 100644 --- a/app/controllers/devise_token_auth/concerns/set_user_by_token.rb +++ b/app/controllers/devise_token_auth/concerns/set_user_by_token.rb @@ -35,17 +35,20 @@ def set_user_by_token(mapping = nil) access_token_name = DeviseTokenAuth.headers_names[:'access-token'] client_name = DeviseTokenAuth.headers_names[:'client'] - # gets values from cookie if configured - auth_cookie = {} + # gets values from cookie if configured and present + parsed_auth_cookie = {} if DeviseTokenAuth.cookie_enabled - auth_cookie = JSON.parse(request.cookies[DeviseTokenAuth.cookie_name]) + auth_cookie = request.cookies[DeviseTokenAuth.cookie_name] + if auth_cookie.present? + parsed_auth_cookie = JSON.parse(auth_cookie) + end end # parse header for values necessary for authentication - uid = request.headers[uid_name] || params[uid_name] || auth_cookie[uid_name] + uid = request.headers[uid_name] || params[uid_name] || parsed_auth_cookie[uid_name] @token = DeviseTokenAuth::TokenFactory.new unless @token - @token.token ||= request.headers[access_token_name] || params[access_token_name] || auth_cookie[access_token_name] - @token.client ||= request.headers[client_name] || params[client_name] || auth_cookie[client_name] + @token.token ||= request.headers[access_token_name] || params[access_token_name] || parsed_auth_cookie[access_token_name] + @token.client ||= request.headers[client_name] || params[client_name] || parsed_auth_cookie[client_name] # client isn't required, set to 'default' if absent @token.client ||= 'default' From 7592336caa29f8a1c4d9173f409993becba6e1ef Mon Sep 17 00:00:00 2001 From: Matt Langston Date: Tue, 5 Jan 2021 19:04:49 -0600 Subject: [PATCH 5/7] allow a Proc being passed the request as a domain config --- .../concerns/set_user_by_token.rb | 29 +++++++++++++++++-- .../devise_token_auth/sessions_controller.rb | 5 ++-- docs/config/initialization.md | 2 +- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/app/controllers/devise_token_auth/concerns/set_user_by_token.rb b/app/controllers/devise_token_auth/concerns/set_user_by_token.rb index 58b0b8604..dcb984d77 100644 --- a/app/controllers/devise_token_auth/concerns/set_user_by_token.rb +++ b/app/controllers/devise_token_auth/concerns/set_user_by_token.rb @@ -112,9 +112,8 @@ def update_auth_header # set a server cookie if configured if DeviseTokenAuth.cookie_enabled - cookies[DeviseTokenAuth.cookie_name] = DeviseTokenAuth.cookie_attributes.merge(value: auth_header.to_json) + set_cookie(auth_header) end - else unless @resource.reload.valid? @resource = @resource.class.find(@resource.to_param) # errors remain after reload @@ -127,6 +126,18 @@ def update_auth_header end end + def get_cookie_domain + # Most people will just set a string for the domain attribute config. But if you need + # more flexibility, such as to dynamically choose the domain when serving multiple domains + # from a single server, you can set a Proc that will be passed the request. + config_value = DeviseTokenAuth.cookie_attributes[:domain] + if config_value.is_a?(String) + config_value + elsif config_value.is_a?(Proc) + config_value.call(request) + end + end + private def refresh_headers @@ -144,11 +155,23 @@ def refresh_headers # set a server cookie if configured if DeviseTokenAuth.cookie_enabled - cookies[DeviseTokenAuth.cookie_name] = DeviseTokenAuth.cookie_attributes.merge(value: _auth_header_from_batch_request.to_json) + set_cookie(_auth_header_from_batch_request) end end # end lock end + def set_cookie(auth_header) + cookie_attributes = DeviseTokenAuth.cookie_attributes + cookie_attributes.merge!(value: auth_header.to_json) + + cookie_domain = get_cookie_domain + if cookie_domain.present? + cookie_attributes.merge!(domain: cookie_domain) + end + + cookies[DeviseTokenAuth.cookie_name] = cookie_attributes + end + def is_batch_request?(user, client) !params[:unbatch] && user.tokens[client] && diff --git a/app/controllers/devise_token_auth/sessions_controller.rb b/app/controllers/devise_token_auth/sessions_controller.rb index a141dd00c..8cd7a0da5 100644 --- a/app/controllers/devise_token_auth/sessions_controller.rb +++ b/app/controllers/devise_token_auth/sessions_controller.rb @@ -56,10 +56,11 @@ def destroy user.save! if DeviseTokenAuth.cookie_enabled - if DeviseTokenAuth.cookie_attributes[:domain] + cookie_domain = get_cookie_domain + if cookie_domain.present? # If a cookie is set with a domain specified then it must be deleted with that domain specified # See https://stackoverflow.com/a/6244724/1747491 - cookies.delete(DeviseTokenAuth.cookie_name, domain: DeviseTokenAuth.cookie_attributes[:domain]) + cookies.delete(DeviseTokenAuth.cookie_name, domain: cookie_domain) else cookies.delete(DeviseTokenAuth.cookie_name) end diff --git a/docs/config/initialization.md b/docs/config/initialization.md index 386c93828..e2e7a7409 100644 --- a/docs/config/initialization.md +++ b/docs/config/initialization.md @@ -17,7 +17,7 @@ The following settings are available for configuration in `config/initializers/d | **`default_callbacks`** (`true`) | By default User model will include the `DeviseTokenAuth::Concerns::UserOmniauthCallbacks` concern, which has `email`, `uid` validations & `uid` synchronization callbacks. | | **`cookie_enabled`** (`false`) | Specifies if DeviseTokenAuth should send and receive the auth token in a cookie. | **`cookie_name`** (`"auth_cookie"`) | Sets the name of the cookie containing the auth token. -| **`cookie_attributes`** (`{}`) | Sets attributes on the cookie containing the auth token (ex. `domain`, `secure`, `httponly`, `same_site`, `expires`, `encrypt`). See [this MDN doc](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) for additional documentation on cookie attributes. +| **`cookie_attributes`** (`{}`) | Sets attributes on the cookie containing the auth token (ex. `domain`, `secure`, `httponly`, `same_site`, `expires`, `encrypt`). See [this MDN doc](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) for additional documentation on cookie attributes. NOTE: If you need more flexibility with the domain attribute config, such as to dynamically choose the domain when serving multiple domains from a single server, you can set a Proc that will be passed the request. | **`bypass_sign_in`** (`true`) | By default DeviseTokenAuth will not check user's `#active_for_authentication?` which includes confirmation check on each call (it will do it only on sign in). If you want it to be validated on each request (for example, to be able to deactivate logged in users on the fly), set it to false. | | **`send_confirmation_email`** (`false`) | By default DeviseTokenAuth will not send confirmation email, even when including devise confirmable module. If you want to use devise confirmable module and send email, set it to true. (This is a setting for compatibility) | | **`require_client_password_reset_token`** (`false`) | By default, the password-reset confirmation link redirects to the client with valid session credentials as querystring params. With this option enabled, the redirect will NOT include the valid session credentials. Instead the redirect will include a password_reset_token querystring param that can be used to reset the users password. Once the user has reset their password, the password-reset success response headers will contain valid session credentials. | From 536340b3e1861fef0e595158b3588b747d765898 Mon Sep 17 00:00:00 2001 From: Matt Langston Date: Thu, 7 Jan 2021 09:57:45 -0600 Subject: [PATCH 6/7] fix bug with mutating the original cookie_attributes config. fix oddity with setting domain to the Proc if the Proc returns nil. clean up unncessary present conditionals. --- .../devise_token_auth/concerns/set_user_by_token.rb | 11 ++--------- .../devise_token_auth/sessions_controller.rb | 11 +++-------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/app/controllers/devise_token_auth/concerns/set_user_by_token.rb b/app/controllers/devise_token_auth/concerns/set_user_by_token.rb index dcb984d77..b32cc0d1d 100644 --- a/app/controllers/devise_token_auth/concerns/set_user_by_token.rb +++ b/app/controllers/devise_token_auth/concerns/set_user_by_token.rb @@ -161,15 +161,8 @@ def refresh_headers end def set_cookie(auth_header) - cookie_attributes = DeviseTokenAuth.cookie_attributes - cookie_attributes.merge!(value: auth_header.to_json) - - cookie_domain = get_cookie_domain - if cookie_domain.present? - cookie_attributes.merge!(domain: cookie_domain) - end - - cookies[DeviseTokenAuth.cookie_name] = cookie_attributes + cookies[DeviseTokenAuth.cookie_name] = DeviseTokenAuth.cookie_attributes + .merge(value: auth_header.to_json, domain: get_cookie_domain) end def is_batch_request?(user, client) diff --git a/app/controllers/devise_token_auth/sessions_controller.rb b/app/controllers/devise_token_auth/sessions_controller.rb index 8cd7a0da5..fa77ba65d 100644 --- a/app/controllers/devise_token_auth/sessions_controller.rb +++ b/app/controllers/devise_token_auth/sessions_controller.rb @@ -56,14 +56,9 @@ def destroy user.save! if DeviseTokenAuth.cookie_enabled - cookie_domain = get_cookie_domain - if cookie_domain.present? - # If a cookie is set with a domain specified then it must be deleted with that domain specified - # See https://stackoverflow.com/a/6244724/1747491 - cookies.delete(DeviseTokenAuth.cookie_name, domain: cookie_domain) - else - cookies.delete(DeviseTokenAuth.cookie_name) - end + # If a cookie is set with a domain specified then it must be deleted with that domain specified + # See https://api.rubyonrails.org/classes/ActionDispatch/Cookies.html + cookies.delete(DeviseTokenAuth.cookie_name, domain: get_cookie_domain) end yield user if block_given? From 66f8fa98cfbefbeaa5f098f3efa482744f9e668d Mon Sep 17 00:00:00 2001 From: Matt Langston Date: Thu, 7 Jan 2021 14:17:53 -0600 Subject: [PATCH 7/7] remove the Proc option since domain is more robust than I realized --- .../concerns/set_user_by_token.rb | 15 +-------------- .../devise_token_auth/sessions_controller.rb | 2 +- docs/config/initialization.md | 2 +- 3 files changed, 3 insertions(+), 16 deletions(-) diff --git a/app/controllers/devise_token_auth/concerns/set_user_by_token.rb b/app/controllers/devise_token_auth/concerns/set_user_by_token.rb index b32cc0d1d..a2221b010 100644 --- a/app/controllers/devise_token_auth/concerns/set_user_by_token.rb +++ b/app/controllers/devise_token_auth/concerns/set_user_by_token.rb @@ -126,18 +126,6 @@ def update_auth_header end end - def get_cookie_domain - # Most people will just set a string for the domain attribute config. But if you need - # more flexibility, such as to dynamically choose the domain when serving multiple domains - # from a single server, you can set a Proc that will be passed the request. - config_value = DeviseTokenAuth.cookie_attributes[:domain] - if config_value.is_a?(String) - config_value - elsif config_value.is_a?(Proc) - config_value.call(request) - end - end - private def refresh_headers @@ -161,8 +149,7 @@ def refresh_headers end def set_cookie(auth_header) - cookies[DeviseTokenAuth.cookie_name] = DeviseTokenAuth.cookie_attributes - .merge(value: auth_header.to_json, domain: get_cookie_domain) + cookies[DeviseTokenAuth.cookie_name] = DeviseTokenAuth.cookie_attributes.merge(value: auth_header.to_json) end def is_batch_request?(user, client) diff --git a/app/controllers/devise_token_auth/sessions_controller.rb b/app/controllers/devise_token_auth/sessions_controller.rb index fa77ba65d..96dc295cd 100644 --- a/app/controllers/devise_token_auth/sessions_controller.rb +++ b/app/controllers/devise_token_auth/sessions_controller.rb @@ -58,7 +58,7 @@ def destroy if DeviseTokenAuth.cookie_enabled # If a cookie is set with a domain specified then it must be deleted with that domain specified # See https://api.rubyonrails.org/classes/ActionDispatch/Cookies.html - cookies.delete(DeviseTokenAuth.cookie_name, domain: get_cookie_domain) + cookies.delete(DeviseTokenAuth.cookie_name, domain: DeviseTokenAuth.cookie_attributes[:domain]) end yield user if block_given? diff --git a/docs/config/initialization.md b/docs/config/initialization.md index e2e7a7409..040ff37bd 100644 --- a/docs/config/initialization.md +++ b/docs/config/initialization.md @@ -17,7 +17,7 @@ The following settings are available for configuration in `config/initializers/d | **`default_callbacks`** (`true`) | By default User model will include the `DeviseTokenAuth::Concerns::UserOmniauthCallbacks` concern, which has `email`, `uid` validations & `uid` synchronization callbacks. | | **`cookie_enabled`** (`false`) | Specifies if DeviseTokenAuth should send and receive the auth token in a cookie. | **`cookie_name`** (`"auth_cookie"`) | Sets the name of the cookie containing the auth token. -| **`cookie_attributes`** (`{}`) | Sets attributes on the cookie containing the auth token (ex. `domain`, `secure`, `httponly`, `same_site`, `expires`, `encrypt`). See [this MDN doc](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) for additional documentation on cookie attributes. NOTE: If you need more flexibility with the domain attribute config, such as to dynamically choose the domain when serving multiple domains from a single server, you can set a Proc that will be passed the request. +| **`cookie_attributes`** (`{}`) | Sets attributes for the cookie containing the auth token (ex. `domain`, `secure`, `httponly`, `same_site`, `expires`, `encrypt`). See [this Rails doc](https://api.rubyonrails.org/classes/ActionDispatch/Cookies.html) for what values can be passed to `ActionDispatch::Cookies`. See [this MDN doc](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) for additional information on HTTP cookie attributes. | **`bypass_sign_in`** (`true`) | By default DeviseTokenAuth will not check user's `#active_for_authentication?` which includes confirmation check on each call (it will do it only on sign in). If you want it to be validated on each request (for example, to be able to deactivate logged in users on the fly), set it to false. | | **`send_confirmation_email`** (`false`) | By default DeviseTokenAuth will not send confirmation email, even when including devise confirmable module. If you want to use devise confirmable module and send email, set it to true. (This is a setting for compatibility) | | **`require_client_password_reset_token`** (`false`) | By default, the password-reset confirmation link redirects to the client with valid session credentials as querystring params. With this option enabled, the redirect will NOT include the valid session credentials. Instead the redirect will include a password_reset_token querystring param that can be used to reset the users password. Once the user has reset their password, the password-reset success response headers will contain valid session credentials. |