Merge with master

This commit is contained in:
Carlos Antonio da Silva
2010-02-06 09:24:00 -02:00
22 changed files with 264 additions and 130 deletions

View File

@@ -1,3 +1,12 @@
* enhancements
* Added Http Basic Authentication support
== 0.9.2
* bug fix
* Ensure inactive user cannot sign in
* Ensure redirect to proper url after sign up
* enhancements
* Added gemspec to repo
* Added token authenticatable (by github.com/grimen)

1
TODO
View File

@@ -1,4 +1,3 @@
* Make test run with DataMapper
* Add Registerable support
* Add http authentication support
* Extract Activatable tests from Confirmable

View File

@@ -5,11 +5,11 @@
Gem::Specification.new do |s|
s.name = %q{devise}
s.version = "0.9.1"
s.version = "0.9.2"
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["Jos\303\251 Valim", "Carlos Ant\303\264nio"]
s.date = %q{2010-02-01}
s.date = %q{2010-02-05}
s.description = %q{Flexible authentication solution for Rails with Warden}
s.email = %q{contact@plataformatec.com.br}
s.extra_rdoc_files = [
@@ -75,6 +75,7 @@ Gem::Specification.new do |s|
"lib/devise/models/recoverable.rb",
"lib/devise/models/rememberable.rb",
"lib/devise/models/timeoutable.rb",
"lib/devise/models/token_authenticatable.rb",
"lib/devise/models/trackable.rb",
"lib/devise/models/validatable.rb",
"lib/devise/orm/active_record.rb",
@@ -87,6 +88,7 @@ Gem::Specification.new do |s|
"lib/devise/strategies/authenticatable.rb",
"lib/devise/strategies/base.rb",
"lib/devise/strategies/rememberable.rb",
"lib/devise/strategies/token_authenticatable.rb",
"lib/devise/test_helpers.rb",
"lib/devise/version.rb"
]
@@ -108,6 +110,7 @@ Gem::Specification.new do |s|
"test/integration/recoverable_test.rb",
"test/integration/rememberable_test.rb",
"test/integration/timeoutable_test.rb",
"test/integration/token_authenticatable_test.rb",
"test/integration/trackable_test.rb",
"test/mailers/confirmation_instructions_test.rb",
"test/mailers/reset_password_instructions_test.rb",
@@ -119,6 +122,7 @@ Gem::Specification.new do |s|
"test/models/recoverable_test.rb",
"test/models/rememberable_test.rb",
"test/models/timeoutable_test.rb",
"test/models/token_authenticatable_test.rb",
"test/models/trackable_test.rb",
"test/models/validatable_test.rb",
"test/models_test.rb",
@@ -148,6 +152,7 @@ Gem::Specification.new do |s|
"test/support/integration_tests_helper.rb",
"test/support/model_tests_helper.rb",
"test/support/test_silencer.rb",
"test/support/tests_helper.rb",
"test/test_helper.rb",
"test/test_helpers_test.rb"
]

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

@@ -26,9 +26,19 @@ module Devise
autoload :MongoMapper, 'devise/orm/mongo_mapper'
end
ALL = [:authenticatable, :activatable, :confirmable, :lockable, :recoverable,
:registerable, :rememberable, :timeoutable, :token_authenticatable,
:trackable, :validatable]
ALL = []
# Authentication ones first
ALL.push :authenticatable, :token_authenticatable, :rememberable
# Misc after
ALL.push :recoverable, :registerable, :validatable
# The ones which can sign out after
ALL.push :activatable, :confirmable, :lockable, :timeoutable
# Stats for last, so we make sure the user is really signed in
ALL.push :trackable
# Maps controller names to devise modules.
CONTROLLERS = {
@@ -45,7 +55,7 @@ module Devise
# Path names used in routes.
PATH_NAMES = [:sign_in, :sign_out, :password, :confirmation, :registration, :unlock]
STRATEGIES = [:rememberable, :token_authenticatable, :authenticatable]
STRATEGIES = [:rememberable, :http_authenticatable, :token_authenticatable, :authenticatable]
TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE']
@@ -144,6 +154,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

@@ -36,7 +36,10 @@ module Devise
# Find a mapping by a given class. It takes into account single table inheritance as well.
def self.find_by_class(klass)
Devise.mappings.values.find { |m| return m if klass <= m.to }
Devise.mappings.each_value do |mapping|
return mapping if klass <= mapping.to
end
nil
end
# Receives an object and find a scope for it. If a scope cannot be found,

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

@@ -6,7 +6,7 @@ module Devise
# Redirects to sign_in page if it's not authenticated
class Authenticatable < Base
def valid?
super && params[scope] && params[scope][:password].present?
params[scope] && params[scope][:password].present? && mapping.to.respond_to?(:authenticate)
end
# Authenticate a user based on email and password params, returning to warden

View File

@@ -2,22 +2,14 @@ module Devise
module Strategies
# Base strategy for Devise. Responsible for verifying correct scope and mapping.
class Base < ::Warden::Strategies::Base
# Validate strategy. By default will raise an error if no scope or an
# invalid mapping is found.
def valid?
raise "Could not find mapping for #{scope}" unless mapping
mapping.for.include?(klass_type)
end
# Checks if a valid scope was given for devise and find mapping based on
# this scope.
def mapping
Devise.mappings[scope]
end
# Store this class type.
def klass_type
@klass_type ||= self.class.name.split("::").last.underscore.to_sym
@mapping ||= begin
mapping = Devise.mappings[scope]
raise "Could not find mapping for #{scope}" unless mapping
mapping
end
end
end
end

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? && mapping.to.respond_to?(:authenticate_with_http)
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

@@ -10,7 +10,7 @@ module Devise
# A valid strategy for rememberable needs a remember token in the cookies.
def valid?
super && remember_me_cookie.present?
remember_me_cookie.present? && mapping.to.respond_to?(:serialize_from_cookie)
end
# To authenticate a user we deserialize the cookie and attempt finding

View File

@@ -6,7 +6,7 @@ module Devise
# Redirects to sign_in page if it's not authenticated.
class TokenAuthenticatable < Base
def valid?
super && authentication_token(scope).present?
mapping.to.respond_to?(:authenticate_with_token) && authentication_token(scope).present?
end
# Authenticate a user based on authenticatable token params, returning to warden
@@ -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

@@ -1,3 +1,3 @@
module Devise
VERSION = "0.9.1".freeze
VERSION = "0.9.2".freeze
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

@@ -128,4 +128,14 @@ class PasswordTest < ActionController::IntegrationTest
assert warden.authenticated?(:user)
end
test 'does not sign in user automatically after changing it\'s password if it\'s not active' do
user = create_user(:confirm => false)
request_forgot_password
reset_password :reset_password_token => user.reload.reset_password_token
assert_redirected_to new_user_session_path(:unconfirmed => true)
assert !warden.authenticated?(: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