From 1cf4dc798d9f7ed8166695544afe10d150af8d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 6 Feb 2010 01:33:32 +0100 Subject: [PATCH] Add Http Basic Authentication support. --- generators/devise_install/templates/devise.rb | 4 +- lib/devise.rb | 6 +- lib/devise/mapping.rb | 2 +- lib/devise/models/authenticatable.rb | 10 +- lib/devise/strategies/http_authenticatable.rb | 49 ++++++++++ .../strategies/token_authenticatable.rb | 17 ++-- test/devise_test.rb | 2 +- test/integration/authenticatable_test.rb | 98 ++++++++++--------- test/integration/http_authenticatable_test.rb | 44 +++++++++ .../integration/token_authenticatable_test.rb | 8 +- test/models/authenticatable_test.rb | 2 +- test/support/integration_tests_helper.rb | 6 +- test/support/model_tests_helper.rb | 36 ------- test/support/tests_helper.rb | 36 ++++++- 14 files changed, 213 insertions(+), 107 deletions(-) create mode 100644 lib/devise/strategies/http_authenticatable.rb create mode 100644 test/integration/http_authenticatable_test.rb delete mode 100644 test/support/model_tests_helper.rb diff --git a/generators/devise_install/templates/devise.rb b/generators/devise_install/templates/devise.rb index 07a81df2..257a8f92 100644 --- a/generators/devise_install/templates/devise.rb +++ b/generators/devise_install/templates/devise.rb @@ -26,6 +26,9 @@ Devise.setup do |config| # session. If you need permissions, you should implement that in a before filter. # config.authentication_keys = [ :email ] + # The realm used in Http Basic Authentication + # config.http_authentication_realm = "Application" + # ==> Configuration for :confirmable # The time you want give to your user to confirm his account. During this time # he will be able to access your application without confirming. Default is nil. @@ -93,7 +96,6 @@ Devise.setup do |config| # Configure default_url_options if you are using dynamic segments in :path_prefix # for devise_for. - # # config.default_url_options do # { :locale => I18n.locale } # end diff --git a/lib/devise.rb b/lib/devise.rb index 9eb0a6be..4abd74cf 100644 --- a/lib/devise.rb +++ b/lib/devise.rb @@ -48,7 +48,7 @@ module Devise :unlocks => [:lockable] } - STRATEGIES = [:rememberable, :token_authenticatable, :authenticatable] + STRATEGIES = [:rememberable, :http_authenticatable, :token_authenticatable, :authenticatable] TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'] @@ -147,6 +147,10 @@ module Devise mattr_accessor :token_authentication_key @@token_authentication_key = :auth_token + # The realm used in Http Basic Authentication + mattr_accessor :http_authentication_realm + @@http_authentication_realm = "Application" + class << self # Default way to setup Devise. Run script/generate devise_install to create # a fresh initializer with all configuration values. diff --git a/lib/devise/mapping.rb b/lib/devise/mapping.rb index fe868411..7a4a3ae7 100644 --- a/lib/devise/mapping.rb +++ b/lib/devise/mapping.rb @@ -129,7 +129,7 @@ module Devise # Configure default path names, allowing the user overwrite defaults by # passing a hash in :path_names. def setup_path_names - [:sign_in, :sign_out, :password, :confirmation].each do |path_name| + [:sign_in, :sign_out, :password, :confirmation, :unlock].each do |path_name| @path_names[path_name] ||= path_name.to_s end end diff --git a/lib/devise/models/authenticatable.rb b/lib/devise/models/authenticatable.rb index 5d7c120e..8fada9f0 100644 --- a/lib/devise/models/authenticatable.rb +++ b/lib/devise/models/authenticatable.rb @@ -1,4 +1,5 @@ require 'devise/strategies/authenticatable' +require 'devise/strategies/http_authenticatable' module Devise module Models @@ -82,12 +83,10 @@ module Devise end module ClassMethods - Devise::Models.config(self, :pepper, :stretches, :encryptor, :authentication_keys) # Authenticate a user based on configured attribute keys. Returns the - # authenticated user if it's valid or nil. Attributes are by default - # :email and :password, but the latter is always required. + # authenticated user if it's valid or nil. def authenticate(attributes={}) return unless authentication_keys.all? { |k| attributes[k].present? } conditions = attributes.slice(*authentication_keys) @@ -95,6 +94,11 @@ module Devise resource if resource.try(:valid_for_authentication?, attributes) end + # Authenticate an user using http. + def authenticate_with_http(username, password) + authenticate(authentication_keys.first => username, :password => password) + end + # Returns the class for the configured encryptor. def encryptor_class @encryptor_class ||= ::Devise::Encryptors.const_get(encryptor.to_s.classify) diff --git a/lib/devise/strategies/http_authenticatable.rb b/lib/devise/strategies/http_authenticatable.rb new file mode 100644 index 00000000..d2eaef3b --- /dev/null +++ b/lib/devise/strategies/http_authenticatable.rb @@ -0,0 +1,49 @@ +require 'devise/strategies/base' + +module Devise + module Strategies + # Sign in an user using HTTP authentication. + class HttpAuthenticatable < Base + def valid? + http_authentication? + end + + def authenticate! + username, password = username_and_password + + if resource = mapping.to.authenticate_with_http(username, password) + success!(resource) + else + custom!([401, custom_headers, ["HTTP Basic: Access denied.\n"]]) + end + end + + private + + def username_and_password + decode_credentials(request).split(/:/, 2) + end + + def http_authentication + request.env['HTTP_AUTHORIZATION'] || + request.env['X-HTTP_AUTHORIZATION'] || + request.env['X_HTTP_AUTHORIZATION'] || + request.env['REDIRECT_X_HTTP_AUTHORIZATION'] + end + alias :http_authentication? :http_authentication + + def decode_credentials(request) + ActiveSupport::Base64.decode64(http_authentication.split(' ', 2).last || '') + end + + def custom_headers + { + "Content-Type" => "text/plain", + "WWW-Authenticate" => %(Basic realm="#{Devise.http_authentication_realm.gsub(/"/, "")}") + } + end + end + end +end + +Warden::Strategies.add(:http_authenticatable, Devise::Strategies::HttpAuthenticatable) diff --git a/lib/devise/strategies/token_authenticatable.rb b/lib/devise/strategies/token_authenticatable.rb index 1eea4d08..3de18af9 100644 --- a/lib/devise/strategies/token_authenticatable.rb +++ b/lib/devise/strategies/token_authenticatable.rb @@ -20,17 +20,16 @@ module Devise end end - private + private - # Detect authentication token in params: scoped or not. - def authentication_token(scope) - if params[scope] - params[scope][mapping.to.token_authentication_key] - else - params[mapping.to.token_authentication_key] - end + # Detect authentication token in params: scoped or not. + def authentication_token(scope) + if params[scope] + params[scope][mapping.to.token_authentication_key] + else + params[mapping.to.token_authentication_key] end - + end end end end diff --git a/test/devise_test.rb b/test/devise_test.rb index d518dc44..6b2269fa 100644 --- a/test/devise_test.rb +++ b/test/devise_test.rb @@ -25,7 +25,7 @@ class DeviseTest < ActiveSupport::TestCase Devise.configure_warden(config) assert_equal Devise::FailureApp, config.failure_app - assert_equal [:rememberable, :token_authenticatable, :authenticatable], config.default_strategies + assert_equal [:rememberable, :http_authenticatable, :token_authenticatable, :authenticatable], config.default_strategies assert_equal :user, config.default_scope assert config.silence_missing_strategies? end diff --git a/test/integration/authenticatable_test.rb b/test/integration/authenticatable_test.rb index 0ea878bf..92a909c7 100644 --- a/test/integration/authenticatable_test.rb +++ b/test/integration/authenticatable_test.rb @@ -1,8 +1,7 @@ require 'test/test_helper' -class AuthenticationTest < ActionController::IntegrationTest - - test 'home should be accessible without signed in' do +class AuthenticationSanityTest < ActionController::IntegrationTest + test 'home should be accessible without sign in' do visit '/' assert_response :success assert_template 'home/index' @@ -76,43 +75,6 @@ class AuthenticationTest < ActionController::IntegrationTest assert_contain 'Welcome Admin' end - test 'sign in as user should not authenticate if not using proper authentication keys' do - swap Devise, :authentication_keys => [:username] do - sign_in_as_user - assert_not warden.authenticated?(:user) - end - end - - test 'admin signing in with invalid email should return to sign in form with error message' do - sign_in_as_admin do - fill_in 'email', :with => 'wrongemail@test.com' - end - - assert_contain 'Invalid email or password' - assert_not warden.authenticated?(:admin) - end - - test 'admin signing in with invalid pasword should return to sign in form with error message' do - sign_in_as_admin do - fill_in 'password', :with => 'abcdef' - end - - assert_contain 'Invalid email or password' - assert_not warden.authenticated?(:admin) - end - - test 'error message is configurable by resource name' do - store_translations :en, :devise => { - :sessions => { :admin => { :invalid => "Invalid credentials" } } - } do - sign_in_as_admin do - fill_in 'password', :with => 'abcdef' - end - - assert_contain 'Invalid credentials' - end - end - test 'authenticated admin should not be able to sign as admin again' do sign_in_as_admin get new_admin_session_path @@ -143,6 +105,45 @@ class AuthenticationTest < ActionController::IntegrationTest get root_path assert_not_contain 'Signed out successfully' end +end + +class AuthenticationTest < ActionController::IntegrationTest + test 'sign in should not authenticate if not using proper authentication keys' do + swap Devise, :authentication_keys => [:username] do + sign_in_as_user + assert_not warden.authenticated?(:user) + end + end + + test 'sign in with invalid email should return to sign in form with error message' do + sign_in_as_admin do + fill_in 'email', :with => 'wrongemail@test.com' + end + + assert_contain 'Invalid email or password' + assert_not warden.authenticated?(:admin) + end + + test 'sign in with invalid pasword should return to sign in form with error message' do + sign_in_as_admin do + fill_in 'password', :with => 'abcdef' + end + + assert_contain 'Invalid email or password' + assert_not warden.authenticated?(:admin) + end + + test 'error message is configurable by resource name' do + store_translations :en, :devise => { + :sessions => { :admin => { :invalid => "Invalid credentials" } } + } do + sign_in_as_admin do + fill_in 'password', :with => 'abcdef' + end + + assert_contain 'Invalid credentials' + end + end test 'redirect from warden shows sign in or sign up message' do get admins_path @@ -194,20 +195,21 @@ class AuthenticationTest < ActionController::IntegrationTest assert_equal "/admin_area/home", @request.path end + test 'destroyed account is signed out' do + sign_in_as_user + visit 'users/index' + + User.destroy_all + visit 'users/index' + assert_redirected_to '/users/sign_in?unauthenticated=true' + end + test 'allows session to be set by a given scope' do sign_in_as_user visit 'users/index' assert_equal "Cart", @controller.user_session[:cart] end - test 'destroyed account is logged out' do - sign_in_as_user - visit 'users/index' - User.destroy_all - visit 'users/index' - assert_redirected_to '/users/sign_in?unauthenticated=true' - end - test 'renders the scoped view if turned on and view is available' do swap Devise, :scoped_views => true do assert_raise Webrat::NotFoundError do diff --git a/test/integration/http_authenticatable_test.rb b/test/integration/http_authenticatable_test.rb new file mode 100644 index 00000000..3a775804 --- /dev/null +++ b/test/integration/http_authenticatable_test.rb @@ -0,0 +1,44 @@ +require 'test/test_helper' + +class HttpAuthenticationTest < ActionController::IntegrationTest + + test 'sign in should authenticate with http' do + sign_in_as_new_user_with_http + assert_response :success + assert_template 'users/index' + assert_contain 'Welcome' + assert warden.authenticated?(:user) + end + + test 'returns a custom response with www-authenticate header on failures' do + sign_in_as_new_user_with_http("unknown") + assert_equal 401, status + assert_equal 'Basic realm="Application"', headers["WWW-Authenticate"] + end + + test 'returns a custom response with www-authenticate and chosen realm' do + swap Devise, :http_authentication_realm => "MyApp" do + sign_in_as_new_user_with_http("unknown") + assert_equal 401, status + assert_equal 'Basic realm="MyApp"', headers["WWW-Authenticate"] + end + end + + test 'sign in should authenticate with http even with specific authentication keys' do + swap Devise, :authentication_keys => [:username] do + sign_in_as_new_user_with_http "usertest" + assert_response :success + assert_template 'users/index' + assert_contain 'Welcome' + assert warden.authenticated?(:user) + end + end + + private + + def sign_in_as_new_user_with_http(username="user@test.com", password="123456") + user = create_user + get users_path, {}, :authorization => "Basic #{ActiveSupport::Base64.encode64("#{username}:#{password}")}" + user + end +end \ No newline at end of file diff --git a/test/integration/token_authenticatable_test.rb b/test/integration/token_authenticatable_test.rb index ea878267..5189f74d 100644 --- a/test/integration/token_authenticatable_test.rb +++ b/test/integration/token_authenticatable_test.rb @@ -2,7 +2,7 @@ require 'test/test_helper' class TokenAuthenticationTest < ActionController::IntegrationTest - test 'sign in user should authenticate with valid authentication token and proper authentication token key' do + test 'sign in should authenticate with valid authentication token and proper authentication token key' do swap Devise, :token_authentication_key => :secret_token do sign_in_as_new_user_with_token(:auth_token_key => :secret_token) @@ -13,7 +13,7 @@ class TokenAuthenticationTest < ActionController::IntegrationTest end end - test 'user signing in with valid authentication token - but improper authentication token key - return to sign in form with error message' do + test 'signing in with valid authentication token - but improper authentication token key - return to sign in form with error message' do swap Devise, :token_authentication_key => :donald_duck_token do sign_in_as_new_user_with_token(:auth_token_key => :secret_token) assert_redirected_to new_user_session_path(:unauthenticated => true) @@ -25,7 +25,7 @@ class TokenAuthenticationTest < ActionController::IntegrationTest end end - test 'user signing in with invalid authentication token should return to sign in form with error message' do + test 'signing in with invalid authentication token should return to sign in form with error message' do store_translations :en, :devise => {:sessions => {:invalid_token => 'LOL, that was not a single character correct.'}} do sign_in_as_new_user_with_token(:auth_token => '*** INVALID TOKEN ***') assert_redirected_to new_user_session_path(:invalid_token => true) @@ -40,7 +40,7 @@ class TokenAuthenticationTest < ActionController::IntegrationTest private - def sign_in_as_new_user_with_token(options = {}, &block) + def sign_in_as_new_user_with_token(options = {}) options[:auth_token_key] ||= Devise.token_authentication_key options[:auth_token] ||= VALID_AUTHENTICATION_TOKEN diff --git a/test/models/authenticatable_test.rb b/test/models/authenticatable_test.rb index 86ddabe5..d0c23e95 100644 --- a/test/models/authenticatable_test.rb +++ b/test/models/authenticatable_test.rb @@ -119,7 +119,7 @@ class AuthenticatableTest < ActiveSupport::TestCase test 'should use authentication keys to retrieve users' do swap Devise, :authentication_keys => [:username] do - user = create_user(:username => "josevalim") + user = create_user assert_nil User.authenticate(:email => user.email, :password => user.password) assert_not_nil User.authenticate(:username => user.username, :password => user.password) end diff --git a/test/support/integration_tests_helper.rb b/test/support/integration_tests_helper.rb index 6f747785..d69fbd96 100644 --- a/test/support/integration_tests_helper.rb +++ b/test/support/integration_tests_helper.rb @@ -7,7 +7,11 @@ class ActionController::IntegrationTest def create_user(options={}) @user ||= begin user = User.create!( - :email => 'user@test.com', :password => '123456', :password_confirmation => '123456', :created_at => Time.now.utc + :username => 'usertest', + :email => 'user@test.com', + :password => '123456', + :password_confirmation => '123456', + :created_at => Time.now.utc ) user.confirm! unless options[:confirm] == false user.lock! if options[:locked] == true diff --git a/test/support/model_tests_helper.rb b/test/support/model_tests_helper.rb deleted file mode 100644 index 610ff7bd..00000000 --- a/test/support/model_tests_helper.rb +++ /dev/null @@ -1,36 +0,0 @@ -class ActiveSupport::TestCase - def setup_mailer - ActionMailer::Base.deliveries = [] - end - - def store_translations(locale, translations, &block) - begin - I18n.backend.store_translations locale, translations - yield - ensure - I18n.reload! - end - end - - # Helpers for creating new users - # - def generate_unique_email - @@email_count ||= 0 - @@email_count += 1 - "test#{@@email_count}@email.com" - end - - def valid_attributes(attributes={}) - { :email => generate_unique_email, - :password => '123456', - :password_confirmation => '123456' }.update(attributes) - end - - def new_user(attributes={}) - User.new(valid_attributes(attributes)) - end - - def create_user(attributes={}) - User.create!(valid_attributes(attributes)) - end -end diff --git a/test/support/tests_helper.rb b/test/support/tests_helper.rb index 92228971..eb92b091 100644 --- a/test/support/tests_helper.rb +++ b/test/support/tests_helper.rb @@ -1,5 +1,39 @@ class ActiveSupport::TestCase - VALID_AUTHENTICATION_TOKEN = 'AbCdEfGhIjKlMnOpQrSt'.freeze + def setup_mailer + ActionMailer::Base.deliveries = [] + end + + def store_translations(locale, translations, &block) + begin + I18n.backend.store_translations locale, translations + yield + ensure + I18n.reload! + end + end + + # Helpers for creating new users + # + def generate_unique_email + @@email_count ||= 0 + @@email_count += 1 + "test#{@@email_count}@email.com" + end + + def valid_attributes(attributes={}) + { :username => "usertest", + :email => generate_unique_email, + :password => '123456', + :password_confirmation => '123456' }.update(attributes) + end + + def new_user(attributes={}) + User.new(valid_attributes(attributes)) + end + + def create_user(attributes={}) + User.create!(valid_attributes(attributes)) + end end \ No newline at end of file