diff --git a/CHANGELOG.rdoc b/CHANGELOG.rdoc index 2e2ddceb..0376ed74 100644 --- a/CHANGELOG.rdoc +++ b/CHANGELOG.rdoc @@ -5,6 +5,7 @@ * enhancements * Added OAuth 2 support * sign_out_via is available in the router to configure the method used for sign out (by github.com/martinrehfeld) + * Improved Ajax requests handling in failure app (by github.com/spastorino) * bugfix * after_sign_in_path_for always receives a resource diff --git a/lib/devise/failure_app.rb b/lib/devise/failure_app.rb index 4705f108..e253f54e 100644 --- a/lib/devise/failure_app.rb +++ b/lib/devise/failure_app.rb @@ -33,7 +33,7 @@ module Devise def http_auth self.status = 401 - self.headers["WWW-Authenticate"] = %(Basic realm=#{Devise.http_authentication_realm.inspect}) unless request.xhr? + self.headers["WWW-Authenticate"] = %(Basic realm=#{Devise.http_authentication_realm.inspect}) if http_auth_header? self.content_type = request.format.to_s self.response_body = http_auth_body end @@ -67,6 +67,14 @@ module Devise send(:"new_#{scope}_session_path") end + # Choose whether we should respond in a http authentication fashion, + # including 401 and optional headers. + # + # This method allows the user to explicitly disable http authentication + # on ajax requests in case they want to redirect on failures instead of + # handling the errors on their own. This is useful in case your ajax API + # is the same as your public API and uses a format like JSON (so you + # cannot mark JSON as a navigational format). def http_auth? if request.xhr? Devise.http_authenticatable_on_xhr @@ -75,6 +83,12 @@ module Devise end end + # It does not make sense to send authenticate headers in ajax requests + # or if the user disabled them. + def http_auth_header? + Devise.mappings[scope].to.http_authenticatable && !request.xhr? + end + def http_auth_body method = :"to_#{request.format.to_sym}" {}.respond_to?(method) ? { :error => i18n_message }.send(method) : i18n_message diff --git a/test/failure_app_test.rb b/test/failure_app_test.rb index a614d8ed..86a8c681 100644 --- a/test/failure_app_test.rb +++ b/test/failure_app_test.rb @@ -17,7 +17,7 @@ class FailureTest < ActiveSupport::TestCase 'rack.input' => "", 'warden' => OpenStruct.new(:message => nil) }.merge!(env_params) - + @response = Devise::FailureApp.call(env).to_a @request = ActionDispatch::Request.new(env) end @@ -72,48 +72,65 @@ class FailureTest < ActiveSupport::TestCase assert_equal 401, @response.first end - test 'return WWW-authenticate headers' do + test 'return WWW-authenticate headers if model allows' do call_failure('formats' => :xml) assert_equal 'Basic realm="Application"', @response.second["WWW-Authenticate"] end - test 'dont return WWW-authenticate on ajax call if http_authenticatable_on_xhr false' do - swap Devise, :http_authenticatable_on_xhr => false do - call_failure('formats' => :html, 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest') - assert_equal 302, @response.first - assert_equal 'http://test.host/users/sign_in', @response.second["Location"] - assert_nil @response.second['WWW-Authenticate'] + test 'does not return WWW-authenticate headers if model does not allow' do + swap Devise, :http_authenticatable => false do + call_failure('formats' => :xml) + assert_nil @response.second["WWW-Authenticate"] end end - test 'dont return WWW-authenticate on ajax call with formats => json if http_authenticatable_on_xhr false' do - swap Devise, :http_authenticatable_on_xhr => false do - call_failure('formats' => :json, 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest') - assert_equal 302, @response.first - assert_equal 'http://test.host/users/sign_in', @response.second["Location"] - assert_nil @response.second['WWW-Authenticate'] - end - end - - test 'return WWW-authenticate on ajax call if http_authenticatable_on_xhr true' do - swap Devise, :http_authenticatable_on_xhr => true do - call_failure('formats' => :html, 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest') - assert_equal 401, @response.first - assert_nil @response.second['WWW-Authenticate'] - end - end - - test 'uses the proxy failure message as response body' do - call_failure('formats' => :xml, 'warden' => OpenStruct.new(:message => :invalid)) - assert_match 'Invalid email or password.', @response.third.body - end - test 'works for any non navigational format' do swap Devise, :navigational_formats => [] do call_failure('formats' => :html) assert_equal 401, @response.first end end + + test 'uses the failure message as response body' do + call_failure('formats' => :xml, 'warden' => OpenStruct.new(:message => :invalid)) + assert_match 'Invalid email or password.', @response.third.body + end + + context 'on ajax call' do + context 'when http_authenticatable_on_xhr is false' do + test 'dont return 401 with navigational formats' do + swap Devise, :http_authenticatable_on_xhr => false do + call_failure('formats' => :html, 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest') + assert_equal 302, @response.first + assert_equal 'http://test.host/users/sign_in', @response.second["Location"] + end + end + + test 'dont return 401 with non navigational formats' do + swap Devise, :http_authenticatable_on_xhr => false do + call_failure('formats' => :json, 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest') + assert_equal 302, @response.first + assert_equal 'http://test.host/users/sign_in', @response.second["Location"] + end + end + end + + context 'when http_authenticatable_on_xhr is true' do + test 'return 401' do + swap Devise, :http_authenticatable_on_xhr => true do + call_failure('formats' => :html, 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest') + assert_equal 401, @response.first + end + end + + test 'skip WWW-Authenticate header' do + swap Devise, :http_authenticatable_on_xhr => true do + call_failure('formats' => :html, 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest') + assert_nil @response.second['WWW-Authenticate'] + end + end + end + end end context 'With recall' do