1
0
Fork 0
mirror of https://github.com/heartcombo/devise.git synced 2022-11-09 12:18:31 -05:00

Initial work on making the authentication stack more flexible.

This commit is contained in:
José Valim 2010-03-29 16:13:19 +02:00
parent 604b7ef61c
commit 1c5d4771ff
26 changed files with 109 additions and 99 deletions

View file

@ -25,8 +25,8 @@ module Devise
# Constants which holds devise configuration for extensions. Those should
# not be modified by the "end user".
ALL = []
CONTROLLERS = {}
ROUTES = []
CONTROLLERS = ActiveSupport::OrderedHash.new
ROUTES = ActiveSupport::OrderedHash.new
STRATEGIES = ActiveSupport::OrderedHash.new
FLASH_MESSAGES = [:unauthenticated]

View file

@ -150,9 +150,9 @@ module Devise
# access that specific controller/action.
# Example:
#
# Maps:
# User => :authenticatable
# Admin => :authenticatable
# Roles:
# User
# Admin
#
# Generated methods:
# authenticate_user! # Signs user in or redirect

View file

@ -19,7 +19,7 @@ module Devise
# Those helpers are added to your ApplicationController.
module UrlHelpers
Devise::ROUTES.each do |module_name|
Devise::ROUTES.values.uniq.each do |module_name|
[:path, :url].each do |path_or_url|
actions = [ nil, :new_ ]
actions << :edit_ if [:password, :registration].include?(module_name)

View file

@ -3,13 +3,6 @@ Warden::Manager.after_set_user do |record, warden, options|
if record && record.respond_to?(:active?) && !record.active?
scope = options[:scope]
warden.logout(scope)
# If winning strategy was set, this is being called after authenticate and
# there is no need to force a redirect.
if warden.winning_strategy
warden.winning_strategy.fail!(record.inactive_message)
else
throw :warden, :scope => scope, :message => record.inactive_message
end
throw :warden, :scope => scope, :message => record.inactive_message
end
end

View file

@ -78,7 +78,11 @@ module Devise
end
def strategies
@strategies ||= STRATEGIES.values_at(*self.modules).compact.reverse
@strategies ||= STRATEGIES.values_at(*self.modules).compact.uniq.reverse
end
def routes
@routes ||= ROUTES.values_at(*self.modules).compact.uniq
end
# Keep a list of allowed controllers for this mapping. It's useful to ensure
@ -101,6 +105,10 @@ module Devise
path_prefix + as.to_s
end
def authenticatable?
@authenticatable ||= self.modules.any? { |m| m.to_s =~ /authenticatable/ }
end
# Create magic predicates for verifying what module is activated by this map.
# Example:
#

View file

@ -38,7 +38,7 @@ module Devise
# Include the chosen devise modules in your model:
#
# devise :authenticatable, :confirmable, :recoverable
# devise :database_authenticatable, :confirmable, :recoverable
#
# You can also give any of the devise configuration values in form of a hash,
# with specific values for this model. Please check your Devise initializer
@ -46,12 +46,17 @@ module Devise
#
def devise(*modules)
raise "You need to give at least one Devise module" if modules.empty?
options = modules.extract_options!
if modules.delete(:authenticatable)
ActiveSupport::Deprecation.warn ":authenticatable as module is deprecated. Please give :database_authenticatable instead.", caller
modules << :database_authenticatable
end
@devise_modules = Devise::ALL & modules.map(&:to_sym).uniq
devise_modules_hook! do
devise_modules.each { |m| include Devise::Models.const_get(m.to_s.classify) }
@devise_modules.each { |m| include Devise::Models.const_get(m.to_s.classify) }
options.each { |key, value| send(:"#{key}=", value) }
end
end

View file

@ -127,7 +127,7 @@ module Devise
# this token is being generated
def generate_confirmation_token
self.confirmed_at = nil
self.confirmation_token = Devise.friendly_token
self.confirmation_token = self.class.confirmation_token
self.confirmation_sent_at = Time.now.utc
end
@ -152,6 +152,10 @@ module Devise
confirmable
end
def confirmation_token
Devise.friendly_token
end
Devise::Models.config(self, :confirm_within)
end
end

View file

@ -1,4 +1,4 @@
require 'devise/strategies/authenticatable'
require 'devise/strategies/database_authenticatable'
module Devise
module Models
@ -26,7 +26,7 @@ module Devise
# User.authenticate('email@test.com', 'password123') # returns authenticated user or nil
# User.find(1).valid_password?('password123') # returns true/false
#
module Authenticatable
module DatabaseAuthenticatable
extend ActiveSupport::Concern
included do
@ -81,11 +81,6 @@ module Devise
result
end
# Allows you to overwrite mail messages headers for a given mail.
def headers_for(action)
{}
end
protected
# Digests the password using the configured encryptor.

View file

@ -89,7 +89,7 @@ module Devise
# Generates unlock token
def generate_unlock_token
self.unlock_token = Devise.friendly_token
self.unlock_token = self.class.unlock_token
end
# Tells if the lock is expired if :time unlock strategy is active
@ -138,6 +138,10 @@ module Devise
[:both, strategy].include?(self.unlock_strategy)
end
def unlock_token
Devise.friendly_token
end
Devise::Models.config(self, :maximum_attempts, :unlock_strategy, :unlock_in)
end
end

View file

@ -45,43 +45,35 @@ module Devise
self.reset_authentication_token! if self.authentication_token.blank?
end
# Verifies whether an +incoming_authentication_token+ (i.e. from single access URL)
# is the user authentication token.
def valid_authentication_token?(incoming_auth_token)
incoming_auth_token.present? && incoming_auth_token == self.authentication_token
end
module ClassMethods
::Devise::Models.config(self, :token_authentication_key)
# Authenticate a user based on authentication token.
def authenticate_with_token(attributes)
token = attributes[self.token_authentication_key]
resource = self.find_for_token_authentication(token)
resource if resource.try(:valid_authentication_token?, token)
self.find_for_token_authentication(token)
end
def authentication_token
::Devise.friendly_token
end
protected
# Find first record based on conditions given (ie by the sign in form).
# Overwrite to add customized conditions, create a join, or maybe use a
# namedscope to filter records while authenticating.
#
# == Example:
#
# def self.find_for_token_authentication(token, conditions = {})
# conditions = {:active => true}
# self.find_by_authentication_token(token, :conditions => conditions)
# end
#
def find_for_token_authentication(token)
self.find(:first, :conditions => { :authentication_token => token})
end
protected
# Find first record based on conditions given (ie by the sign in form).
# Overwrite to add customized conditions, create a join, or maybe use a
# namedscope to filter records while authenticating.
#
# == Example:
#
# def self.find_for_token_authentication(token, conditions = {})
# conditions = {:active => true}
# self.find_by_authentication_token(token, :conditions => conditions)
# end
#
def find_for_token_authentication(token)
self.find(:first, :conditions => { :authentication_token => token})
end
end
end
end

View file

@ -3,9 +3,9 @@ require 'active_support/core_ext/object/with_options'
Devise.with_options :model => true do |d|
# Strategies first
d.with_options :strategy => true do |s|
s.add_module :authenticatable, :controller => :sessions, :flash => :invalid, :route => :session
s.add_module :database_authenticatable, :controller => :sessions, :flash => :invalid, :route => :session
s.add_module :http_authenticatable
s.add_module :token_authenticatable, :controller => :sessions, :flash => :invalid_token, :route => :session
s.add_module :token_authenticatable, :controller => :sessions, :flash => :invalid_token, :route => :session
s.add_module :rememberable
end

View file

@ -3,7 +3,7 @@ module Devise
# This module contains some helpers and handle schema (migrations):
#
# create_table :accounts do |t|
# t.authenticatable
# t.database_authenticatable
# t.confirmable
# t.recoverable
# t.rememberable

View file

@ -83,9 +83,9 @@ module ActionDispatch::Routing
#
# devise_for :users, :controllers => { :sessions => "users/sessions" }
#
# * :skip => tell which modules you want to skip routes from being created:
# * :skip => tell which controller you want to skip routes from being created:
#
# devise_for :users, :skip => :authenticatable
# devise_for :users, :skip => :sessions
#
def devise_for(*resources)
options = resources.extract_options!
@ -100,16 +100,18 @@ module ActionDispatch::Routing
"inside 'config/initializers/devise.rb' or before your application definition in 'config/application.rb'"
end
routes_modules = mapping.modules - Array(options.delete(:skip))
routes_modules.each do |mod|
send(mod, mapping, mapping.controllers) if self.respond_to?(mod, true)
routes = mapping.routes
routes -= Array(options.delete(:skip)).map { |s| s.to_s.singularize.to_sym }
routes.each do |mod|
send(:"devise_#{mod}", mapping, mapping.controllers)
end
end
end
protected
def authenticatable(mapping, controllers)
def devise_session(mapping, controllers)
scope mapping.path do
get mapping.path_names[:sign_in], :to => "#{controllers[:sessions]}#new", :as => :"new_#{mapping.name}_session"
post mapping.path_names[:sign_in], :to => "#{controllers[:sessions]}#create", :as => :"#{mapping.name}_session"
@ -117,25 +119,25 @@ module ActionDispatch::Routing
end
end
def recoverable(mapping, controllers)
def devise_password(mapping, controllers)
scope mapping.path, :name_prefix => mapping.name do
resource :password, :only => [:new, :create, :edit, :update], :as => mapping.path_names[:password], :controller => controllers[:passwords]
end
end
def confirmable(mapping, controllers)
def devise_confirmation(mapping, controllers)
scope mapping.path, :name_prefix => mapping.name do
resource :confirmation, :only => [:new, :create, :show], :as => mapping.path_names[:confirmation], :controller => controllers[:confirmations]
end
end
def lockable(mapping, controllers)
def devise_unlock(mapping, controllers)
scope mapping.path, :name_prefix => mapping.name do
resource :unlock, :only => [:new, :create, :show], :as => mapping.path_names[:unlock], :controller => controllers[:unlocks]
end
end
def registerable(mapping, controllers)
def devise_registration(mapping, controllers)
scope mapping.path[1..-1], :name_prefix => "#{mapping.name}_registration" do
resource :registration, :only => [:new, :create, :edit, :update, :destroy], :as => "",
:path_names => { :new => mapping.path_names[:sign_up] }, :controller => controllers[:registrations]

View file

@ -3,12 +3,17 @@ module Devise
# and overwrite the apply_schema method.
module Schema
def authenticatable(*args)
ActiveSupport::Deprecation.warn "t.authenticatable in migrations is deprecated. Please use t.database_authenticatable instead.", caller
database_authenticatable(*args)
end
# Creates email, encrypted_password and password_salt.
#
# == Options
# * :null - When true, allow columns to be null.
# * :encryptor - The encryptor going to be used, necessary for setting the proper encrypter password length.
def authenticatable(options={})
def database_authenticatable(options={})
null = options[:null] || false
default = options[:default]
encryptor = options[:encryptor] || (respond_to?(:encryptor) ? self.encryptor : :sha1)
@ -16,7 +21,7 @@ module Devise
apply_schema :email, String, :null => null, :default => default
apply_schema :encrypted_password, String, :null => null, :default => default, :limit => Devise::ENCRYPTORS_LENGTH[encryptor]
apply_schema :password_salt, String, :null => null, :default => default
end
end
# Creates authentication_token.
def token_authenticatable

View file

@ -11,6 +11,14 @@ module Devise
mapping
end
end
def success!(record)
if record.respond_to?(:active?) && !record.active?
fail!(record.inactive_message)
else
super
end
end
end
end
end

View file

@ -4,7 +4,7 @@ module Devise
module Strategies
# Default strategy for signing in a user, based on his email and password.
# Redirects to sign_in page if it's not authenticated
class Authenticatable < Base
class DatabaseAuthenticatable < Base
def valid?
valid_controller? && valid_params?
end
@ -33,4 +33,4 @@ module Devise
end
end
Warden::Strategies.add(:authenticatable, Devise::Strategies::Authenticatable)
Warden::Strategies.add(:database_authenticatable, Devise::Strategies::DatabaseAuthenticatable)

View file

@ -37,9 +37,9 @@ class DeviseGenerator < Rails::Generators::NamedBase
def inject_devise_config_into_model
inject_into_class model_path, class_name, <<-CONTENT
# Include default devise modules. Others available are:
# :http_authenticatable, :token_authenticatable, :lockable, :timeoutable and :activatable
devise :registerable, :authenticatable, :confirmable, :recoverable,
:rememberable, :trackable, :validatable
# :token_authenticatable, :lockable, :timeoutable and :activatable
devise :database_authenticatable, :registerable, :confirmable,
:recoverable, :rememberable, :trackable, :validatable
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :password, :password_confirmation

View file

@ -1,7 +1,7 @@
class DeviseCreate<%= table_name.camelize %> < ActiveRecord::Migration
def self.up
create_table(:<%= table_name %>) do |t|
t.authenticatable :encryptor => :<%= Devise.encryptor %>, :null => false
t.database_authenticatable :encryptor => :<%= Devise.encryptor %>, :null => false
t.confirmable
t.recoverable
t.rememberable

View file

@ -4,7 +4,18 @@ Devise.setup do |config|
# Configure the e-mail address which will be shown in DeviseMailer.
config.mailer_sender = "please-change-me@config-initializers-devise.com"
# ==> Configuration for :authenticatable
# ==> Configuration for any authentication mechanism
# Configure which keys are used when authenticating an user. By default is
# just :email. You can configure it to use [:username, :subdomain], so for
# authenticating an user, both parameters are required. Remember that those
# parameters are used only when authenticating and not when retrieving from
# 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 :database_authenticatable
# Invoke `rake secret` and use the printed value to setup a pepper to generate
# the encrypted password. By default no pepper is used.
# config.pepper = "rake secret output"
@ -19,16 +30,6 @@ Devise.setup do |config|
# (then you should set stretches to 10, and copy REST_AUTH_SITE_KEY to pepper)
# config.encryptor = :sha1
# Configure which keys are used when authenticating an user. By default is
# just :email. You can configure it to use [:username, :subdomain], so for
# authenticating an user, both parameters are required. Remember that those
# parameters are used only when authenticating and not when retrieving from
# 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.

View file

@ -1,6 +1,6 @@
require 'test_helper'
class AuthenticationSanityTest < ActionController::IntegrationTest
class DatabaseAuthenticationSanityTest < ActionController::IntegrationTest
test 'home should be accessible without sign in' do
visit '/'
assert_response :success

View file

@ -31,8 +31,8 @@ class MappingTest < ActiveSupport::TestCase
test 'has strategies depending on the model declaration' do
assert_equal [:rememberable, :token_authenticatable,
:http_authenticatable, :authenticatable], Devise.mappings[:user].strategies
assert_equal [:authenticatable], Devise.mappings[:admin].strategies
:http_authenticatable, :database_authenticatable], Devise.mappings[:user].strategies
assert_equal [:database_authenticatable], Devise.mappings[:admin].strategies
end
test 'find mapping by path' do

View file

@ -1,7 +1,7 @@
require 'test_helper'
require 'digest/sha1'
class AuthenticatableTest < ActiveSupport::TestCase
class DatabaseAuthenticatableTest < ActiveSupport::TestCase
def encrypt_password(user, pepper=User.pepper, stretches=User.stretches, encryptor=::Devise::Encryptors::Sha1)
encryptor.digest('123456', stretches, user.password_salt, pepper)

View file

@ -25,13 +25,6 @@ class TokenAuthenticatableTest < ActiveSupport::TestCase
assert_equal previous_token, user.authentication_token
end
test 'should test for a valid authentication token' do
User.expects(:authentication_token).returns(VALID_AUTHENTICATION_TOKEN)
user = create_user
assert user.valid_authentication_token?(VALID_AUTHENTICATION_TOKEN)
assert_not user.valid_authentication_token?(VALID_AUTHENTICATION_TOKEN.reverse)
end
test 'should authenticate a valid user with authentication token and return it' do
User.expects(:authentication_token).returns(VALID_AUTHENTICATION_TOKEN)
user = create_user

View file

@ -23,12 +23,12 @@ class ActiveRecordTest < ActiveSupport::TestCase
end
test 'add modules cherry pick' do
assert_include_modules Admin, :authenticatable, :registerable, :timeoutable, :recoverable
assert_include_modules Admin, :database_authenticatable, :registerable, :timeoutable, :recoverable
end
test 'order of module inclusion' do
correct_module_order = [:authenticatable, :recoverable, :registerable, :timeoutable]
incorrect_module_order = [:authenticatable, :timeoutable, :registerable, :recoverable]
correct_module_order = [:database_authenticatable, :recoverable, :registerable, :timeoutable]
incorrect_module_order = [:database_authenticatable, :timeoutable, :registerable, :recoverable]
assert_include_modules Admin, *incorrect_module_order

View file

@ -6,7 +6,7 @@ Rails::Application.routes.draw do
resources :admins, :only => [:index]
devise_for :users
devise_for :admin, :as => "admin_area", :controllers => { :sessions => "sessions" }, :skip => :recoverable
devise_for :admin, :as => "admin_area", :controllers => { :sessions => "sessions" }, :skip => :passwords
devise_for :accounts, :scope => "manager", :path_prefix => ":locale",
:class_name => "User", :path_names => {
:sign_in => "login", :sign_out => "logout",

View file

@ -82,7 +82,7 @@ class MapRoutingTest < ActionController::TestCase
assert_recognizes({:controller => 'sessions', :action => 'new'}, {:path => 'admin_area/sign_in', :method => :get})
end
test 'does not map admin confirmation' do
test 'does not map admin password' do
assert_raise ActionController::RoutingError do
assert_recognizes({:controller => 'devise/passwords', :action => 'new'}, 'admin_area/password/new')
end