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.
# 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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

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

View File

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

View File

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

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