Add Http Basic Authentication support.

This commit is contained in:
José Valim 2010-02-06 01:33:32 +01:00
parent 2f441fb60b
commit 1cf4dc798d
14 changed files with 213 additions and 107 deletions

View File

@ -26,6 +26,9 @@ Devise.setup do |config|
# session. If you need permissions, you should implement that in a before filter. # session. If you need permissions, you should implement that in a before filter.
# config.authentication_keys = [ :email ] # config.authentication_keys = [ :email ]
# The realm used in Http Basic Authentication
# config.http_authentication_realm = "Application"
# ==> Configuration for :confirmable # ==> Configuration for :confirmable
# The time you want give to your user to confirm his account. During this time # 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. # 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 # Configure default_url_options if you are using dynamic segments in :path_prefix
# for devise_for. # for devise_for.
#
# config.default_url_options do # config.default_url_options do
# { :locale => I18n.locale } # { :locale => I18n.locale }
# end # end

View File

@ -48,7 +48,7 @@ module Devise
:unlocks => [:lockable] :unlocks => [:lockable]
} }
STRATEGIES = [:rememberable, :token_authenticatable, :authenticatable] STRATEGIES = [:rememberable, :http_authenticatable, :token_authenticatable, :authenticatable]
TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'] TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE']
@ -147,6 +147,10 @@ module Devise
mattr_accessor :token_authentication_key mattr_accessor :token_authentication_key
@@token_authentication_key = :auth_token @@token_authentication_key = :auth_token
# The realm used in Http Basic Authentication
mattr_accessor :http_authentication_realm
@@http_authentication_realm = "Application"
class << self class << self
# Default way to setup Devise. Run script/generate devise_install to create # Default way to setup Devise. Run script/generate devise_install to create
# a fresh initializer with all configuration values. # a fresh initializer with all configuration values.

View File

@ -129,7 +129,7 @@ module Devise
# Configure default path names, allowing the user overwrite defaults by # Configure default path names, allowing the user overwrite defaults by
# passing a hash in :path_names. # passing a hash in :path_names.
def setup_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 @path_names[path_name] ||= path_name.to_s
end end
end end

View File

@ -1,4 +1,5 @@
require 'devise/strategies/authenticatable' require 'devise/strategies/authenticatable'
require 'devise/strategies/http_authenticatable'
module Devise module Devise
module Models module Models
@ -82,12 +83,10 @@ module Devise
end end
module ClassMethods module ClassMethods
Devise::Models.config(self, :pepper, :stretches, :encryptor, :authentication_keys) Devise::Models.config(self, :pepper, :stretches, :encryptor, :authentication_keys)
# Authenticate a user based on configured attribute keys. Returns the # Authenticate a user based on configured attribute keys. Returns the
# authenticated user if it's valid or nil. Attributes are by default # authenticated user if it's valid or nil.
# :email and :password, but the latter is always required.
def authenticate(attributes={}) def authenticate(attributes={})
return unless authentication_keys.all? { |k| attributes[k].present? } return unless authentication_keys.all? { |k| attributes[k].present? }
conditions = attributes.slice(*authentication_keys) conditions = attributes.slice(*authentication_keys)
@ -95,6 +94,11 @@ module Devise
resource if resource.try(:valid_for_authentication?, attributes) resource if resource.try(:valid_for_authentication?, attributes)
end 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. # Returns the class for the configured encryptor.
def encryptor_class def encryptor_class
@encryptor_class ||= ::Devise::Encryptors.const_get(encryptor.to_s.classify) @encryptor_class ||= ::Devise::Encryptors.const_get(encryptor.to_s.classify)

View File

@ -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)

View File

@ -30,7 +30,6 @@ module Devise
params[mapping.to.token_authentication_key] params[mapping.to.token_authentication_key]
end end
end end
end end
end end
end end

View File

@ -25,7 +25,7 @@ class DeviseTest < ActiveSupport::TestCase
Devise.configure_warden(config) Devise.configure_warden(config)
assert_equal Devise::FailureApp, config.failure_app 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_equal :user, config.default_scope
assert config.silence_missing_strategies? assert config.silence_missing_strategies?
end end

View File

@ -1,8 +1,7 @@
require 'test/test_helper' require 'test/test_helper'
class AuthenticationTest < ActionController::IntegrationTest class AuthenticationSanityTest < ActionController::IntegrationTest
test 'home should be accessible without sign in' do
test 'home should be accessible without signed in' do
visit '/' visit '/'
assert_response :success assert_response :success
assert_template 'home/index' assert_template 'home/index'
@ -76,43 +75,6 @@ class AuthenticationTest < ActionController::IntegrationTest
assert_contain 'Welcome Admin' assert_contain 'Welcome Admin'
end 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 test 'authenticated admin should not be able to sign as admin again' do
sign_in_as_admin sign_in_as_admin
get new_admin_session_path get new_admin_session_path
@ -143,6 +105,45 @@ class AuthenticationTest < ActionController::IntegrationTest
get root_path get root_path
assert_not_contain 'Signed out successfully' assert_not_contain 'Signed out successfully'
end 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 test 'redirect from warden shows sign in or sign up message' do
get admins_path get admins_path
@ -194,20 +195,21 @@ class AuthenticationTest < ActionController::IntegrationTest
assert_equal "/admin_area/home", @request.path assert_equal "/admin_area/home", @request.path
end 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 test 'allows session to be set by a given scope' do
sign_in_as_user sign_in_as_user
visit 'users/index' visit 'users/index'
assert_equal "Cart", @controller.user_session[:cart] assert_equal "Cart", @controller.user_session[:cart]
end 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 test 'renders the scoped view if turned on and view is available' do
swap Devise, :scoped_views => true do swap Devise, :scoped_views => true do
assert_raise Webrat::NotFoundError do assert_raise Webrat::NotFoundError do

View File

@ -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

View File

@ -2,7 +2,7 @@ require 'test/test_helper'
class TokenAuthenticationTest < ActionController::IntegrationTest 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 swap Devise, :token_authentication_key => :secret_token do
sign_in_as_new_user_with_token(:auth_token_key => :secret_token) sign_in_as_new_user_with_token(:auth_token_key => :secret_token)
@ -13,7 +13,7 @@ class TokenAuthenticationTest < ActionController::IntegrationTest
end end
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 swap Devise, :token_authentication_key => :donald_duck_token do
sign_in_as_new_user_with_token(:auth_token_key => :secret_token) sign_in_as_new_user_with_token(:auth_token_key => :secret_token)
assert_redirected_to new_user_session_path(:unauthenticated => true) assert_redirected_to new_user_session_path(:unauthenticated => true)
@ -25,7 +25,7 @@ class TokenAuthenticationTest < ActionController::IntegrationTest
end end
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 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 ***') sign_in_as_new_user_with_token(:auth_token => '*** INVALID TOKEN ***')
assert_redirected_to new_user_session_path(:invalid_token => true) assert_redirected_to new_user_session_path(:invalid_token => true)
@ -40,7 +40,7 @@ class TokenAuthenticationTest < ActionController::IntegrationTest
private 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_key] ||= Devise.token_authentication_key
options[:auth_token] ||= VALID_AUTHENTICATION_TOKEN options[:auth_token] ||= VALID_AUTHENTICATION_TOKEN

View File

@ -119,7 +119,7 @@ class AuthenticatableTest < ActiveSupport::TestCase
test 'should use authentication keys to retrieve users' do test 'should use authentication keys to retrieve users' do
swap Devise, :authentication_keys => [:username] 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_nil User.authenticate(:email => user.email, :password => user.password)
assert_not_nil User.authenticate(:username => user.username, :password => user.password) assert_not_nil User.authenticate(:username => user.username, :password => user.password)
end end

View File

@ -7,7 +7,11 @@ class ActionController::IntegrationTest
def create_user(options={}) def create_user(options={})
@user ||= begin @user ||= begin
user = User.create!( 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.confirm! unless options[:confirm] == false
user.lock! if options[:locked] == true user.lock! if options[:locked] == true

View File

@ -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

View File

@ -1,5 +1,39 @@
class ActiveSupport::TestCase class ActiveSupport::TestCase
VALID_AUTHENTICATION_TOKEN = 'AbCdEfGhIjKlMnOpQrSt'.freeze 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 end