devise 0.6.1 → 0.6.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of devise might be problematic. Click here for more details.

@@ -1,3 +1,9 @@
1
+ == 0.6.2
2
+
3
+ * enhancements
4
+ * More DataMapper compatibility
5
+ * Devise::Trackable - track sign in count, timestamps and ips
6
+
1
7
  == 0.6.1
2
8
 
3
9
  * enhancements
@@ -7,13 +7,14 @@ Devise is a flexible authentication solution for Rails based on Warden. It:
7
7
  * Allows you to have multiple roles (or models/scopes) signed in at the same time;
8
8
  * Is based on a modularity concept: use just what you really need.
9
9
 
10
- Right now it's composed of five mainly modules:
10
+ Right now it's composed of seven mainly modules:
11
11
 
12
12
  * Authenticatable: responsible for encrypting password and validating authenticity of a user while signing in.
13
13
  * Confirmable: responsible for verifying whether an account is already confirmed to sign in, and to send emails with confirmation instructions.
14
14
  * Recoverable: takes care of reseting the user password and send reset instructions.
15
15
  * Rememberable: manages generating and clearing token for remember the user from a saved cookie.
16
16
  * Timeoutable: expires sessions without activity in a certain period of time.
17
+ * Trackable: tracks sign in count, timestamps and ip.
17
18
  * Validatable: creates all needed validations for email and password. It's totally optional, so you're able to to customize validations by yourself.
18
19
 
19
20
  There's an example application using Devise at http://github.com/plataformatec/devise_example .
data/TODO CHANGED
@@ -1,5 +1,2 @@
1
1
  * Create update_with_password
2
2
  * Make test run with different ORMs
3
- * Use request_ip in session cookies
4
- * Devise::BruteForceProtection
5
- * Devise::Trackeable
@@ -36,7 +36,7 @@ Devise.setup do |config|
36
36
 
37
37
  # The time you want to timeout the user session without activity. After this
38
38
  # time the user will be asked for credentials again.
39
- # config.timeout = 10.minutes
39
+ # config.timeout_in = 10.minutes
40
40
 
41
41
  # Configure the e-mail address which will be shown in DeviseMailer.
42
42
  # config.mailer_sender = "foo.bar@yourapp.com"
@@ -1,5 +1,6 @@
1
1
  module Devise
2
- ALL = [:authenticatable, :confirmable, :recoverable, :rememberable, :timeoutable, :validatable].freeze
2
+ ALL = [:authenticatable, :confirmable, :recoverable, :rememberable,
3
+ :timeoutable, :trackable, :validatable].freeze
3
4
 
4
5
  # Maps controller names to devise modules
5
6
  CONTROLLERS = {
@@ -46,8 +47,8 @@ module Devise
46
47
  @@confirm_within = 0.days
47
48
 
48
49
  # Time interval to timeout the user session without activity.
49
- mattr_accessor :timeout
50
- @@timeout = 30.minutes
50
+ mattr_accessor :timeout_in
51
+ @@timeout_in = 30.minutes
51
52
 
52
53
  # Used to define the password encryption algorithm.
53
54
  mattr_accessor :encryptor
@@ -4,16 +4,13 @@
4
4
  # record is set, we set the last request time inside it's scoped session to
5
5
  # verify timeout in the following request.
6
6
  Warden::Manager.after_set_user do |record, warden, options|
7
- if record && record.respond_to?(:timeout?)
8
- scope = options[:scope]
9
- # Record may have already been logged out by another hook (ie confirmable).
10
- if warden.authenticated?(scope)
11
- last_request_at = warden.session(scope)['last_request_at']
12
- if record.timeout?(last_request_at)
13
- warden.logout(scope)
14
- throw :warden, :scope => scope, :message => :timeout
15
- end
16
- warden.session(scope)['last_request_at'] = Time.now.utc
7
+ scope = options[:scope]
8
+ if record && record.respond_to?(:timeout?) && warden.authenticated?(scope)
9
+ last_request_at = warden.session(scope)['last_request_at']
10
+ if record.timeout?(last_request_at)
11
+ warden.logout(scope)
12
+ throw :warden, :scope => scope, :message => :timeout
17
13
  end
14
+ warden.session(scope)['last_request_at'] = Time.now.utc
18
15
  end
19
16
  end
@@ -0,0 +1,18 @@
1
+ # After each sign in, update sign in time, sign in count and sign in IP.
2
+ Warden::Manager.after_authentication do |record, warden, options|
3
+ scope = options[:scope]
4
+ if Devise.mappings[scope].try(:trackable?) && warden.authenticated?(scope)
5
+ old_current, new_current = record.current_sign_in_at, Time.now
6
+ record.last_sign_in_at = old_current || new_current
7
+ record.current_sign_in_at = new_current
8
+
9
+ old_current, new_current = record.current_sign_in_ip, warden.request.remote_ip
10
+ record.last_sign_in_ip = old_current || new_current
11
+ record.current_sign_in_ip = new_current
12
+
13
+ record.sign_in_count ||= 0
14
+ record.sign_in_count += 1
15
+
16
+ record.save(false)
17
+ end
18
+ end
@@ -75,6 +75,7 @@ module Devise
75
75
  options = modules.extract_options!
76
76
  modules = Devise.all if modules.include?(:all)
77
77
  modules -= Array(options.delete(:except))
78
+ modules = Devise::ALL & modules
78
79
 
79
80
  if !modules.include?(:authenticatable)
80
81
  modules = [:authenticatable] | modules
@@ -96,5 +97,26 @@ module Devise
96
97
  def devise_modules
97
98
  @devise_modules ||= []
98
99
  end
100
+
101
+ # Find an initialize a record setting an error if it can't be found
102
+ def find_or_initialize_with_error_by(attribute, value, error=:invalid)
103
+ if value.present?
104
+ conditions = { attribute => value }
105
+ record = find(:first, :conditions => conditions)
106
+ end
107
+
108
+ unless record
109
+ record = new
110
+
111
+ if value.present?
112
+ record.send(:"#{attribute}=", value)
113
+ record.errors.add(attribute, error, :default => error.to_s.gsub("_", " "))
114
+ else
115
+ record.errors.add(attribute, :blank)
116
+ end
117
+ end
118
+
119
+ record
120
+ end
99
121
  end
100
122
  end
@@ -38,16 +38,17 @@ module Devise
38
38
  end
39
39
  end
40
40
 
41
- # Regenerates password salt and encrypted password each time password is
42
- # setted.
41
+ # Regenerates password salt and encrypted password each time password is set.
43
42
  def password=(new_password)
44
43
  @password = new_password
45
- self.password_salt = Devise.friendly_token
46
- self.encrypted_password = password_digest(@password)
44
+
45
+ if @password.present?
46
+ self.password_salt = Devise.friendly_token
47
+ self.encrypted_password = password_digest(@password)
48
+ end
47
49
  end
48
50
 
49
- # Verifies whether an incoming_password (ie from login) is the user
50
- # password.
51
+ # Verifies whether an incoming_password (ie from login) is the user password.
51
52
  def valid_password?(incoming_password)
52
53
  password_digest(incoming_password) == encrypted_password
53
54
  end
@@ -66,31 +67,8 @@ module Devise
66
67
  def authenticate(attributes={})
67
68
  return unless authentication_keys.all? { |k| attributes[k].present? }
68
69
  conditions = attributes.slice(*authentication_keys)
69
- authenticatable = find_for_authentication(conditions)
70
- authenticatable if authenticatable.try(:valid_password?, attributes[:password])
71
- end
72
-
73
- # Find first record based on conditions given (ie by the sign in form).
74
- # Overwrite to add customized conditions, create a join, or maybe use a
75
- # namedscope to filter records while authenticating.
76
- # Example:
77
- #
78
- # def self.find_for_authentication(conditions={})
79
- # conditions[:active] = true
80
- # find(:first, :conditions => conditions)
81
- # end
82
- #
83
- def find_for_authentication(conditions)
84
- find(:first, :conditions => conditions)
85
- end
86
-
87
- # Attempt to find a user by it's email. If not user is found, returns a
88
- # new user with an email not found error.
89
- def find_or_initialize_with_error_by_email(email)
90
- attributes = { :email => email }
91
- record = find(:first, :conditions => attributes) || new(attributes)
92
- record.errors.add(:email, :not_found, :default => 'not found') if record.new_record?
93
- record
70
+ resource = find_for_authentication(conditions)
71
+ valid_for_authentication(resource, attributes) if resource
94
72
  end
95
73
 
96
74
  # Hook to serialize user into session. Overwrite if you want.
@@ -110,6 +88,27 @@ module Devise
110
88
  @encryptor_class ||= ::Devise::Encryptors.const_get(encryptor.to_s.classify)
111
89
  end
112
90
 
91
+ protected
92
+
93
+ # Find first record based on conditions given (ie by the sign in form).
94
+ # Overwrite to add customized conditions, create a join, or maybe use a
95
+ # namedscope to filter records while authenticating.
96
+ # Example:
97
+ #
98
+ # def self.find_for_authentication(conditions={})
99
+ # conditions[:active] = true
100
+ # find(:first, :conditions => conditions)
101
+ # end
102
+ #
103
+ def find_for_authentication(conditions)
104
+ find(:first, :conditions => conditions)
105
+ end
106
+
107
+ # Contains the logic used in authentication. Overwritten by other devise modules.
108
+ def valid_for_authentication(resource, attributes)
109
+ resource if resource.valid_password?(attributes[:password])
110
+ end
111
+
113
112
  Devise::Models.config(self, :pepper, :stretches, :encryptor, :authentication_keys)
114
113
  end
115
114
  end
@@ -100,8 +100,7 @@ module Devise
100
100
  # confirmation_period_valid? # will always return false
101
101
  #
102
102
  def confirmation_period_valid?
103
- confirmation_sent_at &&
104
- ((Time.now.utc - confirmation_sent_at.utc) < self.class.confirm_within)
103
+ confirmation_sent_at && confirmation_sent_at.utc >= self.class.confirm_within.ago
105
104
  end
106
105
 
107
106
  # Checks whether the record is confirmed or not, yielding to the block
@@ -129,7 +128,7 @@ module Devise
129
128
  # with an email not found error.
130
129
  # Options must contain the user email
131
130
  def send_confirmation_instructions(attributes={})
132
- confirmable = find_or_initialize_with_error_by_email(attributes[:email])
131
+ confirmable = find_or_initialize_with_error_by(:email, attributes[:email], :not_found)
133
132
  confirmable.reset_confirmation! unless confirmable.new_record?
134
133
  confirmable
135
134
  end
@@ -139,12 +138,8 @@ module Devise
139
138
  # If the user is already confirmed, create an error for the user
140
139
  # Options must have the confirmation_token
141
140
  def confirm!(attributes={})
142
- confirmable = find_or_initialize_by_confirmation_token(attributes[:confirmation_token])
143
- if confirmable.new_record?
144
- confirmable.errors.add(:confirmation_token, :invalid)
145
- else
146
- confirmable.confirm!
147
- end
141
+ confirmable = find_or_initialize_with_error_by(:confirmation_token, attributes[:confirmation_token])
142
+ confirmable.confirm! unless confirmable.new_record?
148
143
  confirmable
149
144
  end
150
145
 
@@ -6,9 +6,11 @@ module Devise
6
6
  #
7
7
  # # resets the user password and save the record, true if valid passwords are given, otherwise false
8
8
  # User.find(1).reset_password!('password123', 'password123')
9
+ #
9
10
  # # only resets the user password, without saving the record
10
11
  # user = User.find(1)
11
12
  # user.reset_password('password123', 'password123')
13
+ #
12
14
  # # creates a new token and send it with instructions about how to reset the password
13
15
  # User.find(1).send_reset_password_instructions
14
16
  module Recoverable
@@ -62,7 +64,7 @@ module Devise
62
64
  # with an email not found error.
63
65
  # Attributes must contain the user email
64
66
  def send_reset_password_instructions(attributes={})
65
- recoverable = find_or_initialize_with_error_by_email(attributes[:email])
67
+ recoverable = find_or_initialize_with_error_by(:email, attributes[:email], :not_found)
66
68
  recoverable.send_reset_password_instructions unless recoverable.new_record?
67
69
  recoverable
68
70
  end
@@ -73,12 +75,8 @@ module Devise
73
75
  # containing an error in reset_password_token attribute.
74
76
  # Attributes must contain reset_password_token, password and confirmation
75
77
  def reset_password!(attributes={})
76
- recoverable = find_or_initialize_by_reset_password_token(attributes[:reset_password_token])
77
- if recoverable.new_record?
78
- recoverable.errors.add(:reset_password_token, :invalid)
79
- else
80
- recoverable.reset_password!(attributes[:password], attributes[:password_confirmation])
81
- end
78
+ recoverable = find_or_initialize_with_error_by(:reset_password_token, attributes[:reset_password_token])
79
+ recoverable.reset_password!(attributes[:password], attributes[:password_confirmation]) unless recoverable.new_record?
82
80
  recoverable
83
81
  end
84
82
  end
@@ -82,7 +82,7 @@ module Devise
82
82
  # Recreate the user based on the stored cookie
83
83
  def serialize_from_cookie(cookie)
84
84
  rememberable_id, remember_token = cookie.split('::')
85
- rememberable = find_by_id(rememberable_id) if rememberable_id
85
+ rememberable = find(:first, :conditions => { :id => rememberable_id }) if rememberable_id
86
86
  rememberable if rememberable.try(:valid_remember_token?, remember_token)
87
87
  end
88
88
 
@@ -19,11 +19,11 @@ module Devise
19
19
 
20
20
  # Checks whether the user session has expired based on configured time.
21
21
  def timeout?(last_access)
22
- last_access && last_access <= self.class.timeout.ago.utc
22
+ last_access && last_access <= self.class.timeout_in.ago
23
23
  end
24
24
 
25
25
  module ClassMethods
26
- Devise::Models.config(self, :timeout)
26
+ Devise::Models.config(self, :timeout_in)
27
27
  end
28
28
  end
29
29
  end
@@ -0,0 +1,16 @@
1
+ require 'devise/hooks/trackable'
2
+
3
+ module Devise
4
+ module Models
5
+ # Track information about your user sign in. It tracks the following columns:
6
+ #
7
+ # * sign_in_count - Increased every time a sign in is made (by form, openid, oauth)
8
+ # * current_sign_in_at - A tiemstamp updated when the user signs in
9
+ # * last_sign_in_at - Holds the timestamp of the previous sign in
10
+ # * current_sign_in_ip - The remote ip updated when the user sign in
11
+ # * last_sign_in_at - Holds the remote ip of the previous sign in
12
+ #
13
+ module Trackable
14
+ end
15
+ end
16
+ end
@@ -10,7 +10,13 @@ module Devise
10
10
  # Email regex used to validate email formats. Retrieved from authlogic.
11
11
  EMAIL_REGEX = /\A[\w\.%\+\-]+@(?:[A-Z0-9\-]+\.)+(?:[A-Z]{2,4}|museum|travel)\z/i
12
12
 
13
+ # All validations used by this module.
14
+ VALIDATIONS = [ :validates_presence_of, :validates_uniqueness_of, :validates_format_of,
15
+ :validates_confirmation_of, :validates_length_of ].freeze
16
+
13
17
  def self.included(base)
18
+ assert_validations_api!(base)
19
+
14
20
  base.class_eval do
15
21
  attribute = authentication_keys.first
16
22
 
@@ -27,6 +33,15 @@ module Devise
27
33
  end
28
34
  end
29
35
 
36
+ def self.assert_validations_api!(base) #:nodoc:
37
+ unavailable_validations = VALIDATIONS.select { |v| !base.respond_to?(v) }
38
+
39
+ unless unavailable_validations.empty?
40
+ raise "Could not use :validatable module since #{base} does not respond " <<
41
+ "to the following methods: #{unavailable_validations.to_sentence}."
42
+ end
43
+ end
44
+
30
45
  protected
31
46
 
32
47
  # Checks whether a password is needed or not. For validations only.
@@ -5,11 +5,6 @@ module Devise
5
5
  klass.send :extend, self
6
6
  yield
7
7
 
8
- # DataMapper validations have a completely different API
9
- if modules.include?(:validatable) && !klass.respond_to?(:validates_presence_of)
10
- raise ":validatable is not supported in DataMapper, please craft your validations by hand"
11
- end
12
-
13
8
  modules.each do |mod|
14
9
  klass.send(mod) if klass.respond_to?(mod)
15
10
  end
@@ -44,6 +39,15 @@ module Devise
44
39
  end
45
40
  end
46
41
 
42
+ # In Datamapper, we need to call save! if we don't want to execute callbacks.
43
+ def save(flag=nil)
44
+ if flag == false
45
+ save!
46
+ else
47
+ super()
48
+ end
49
+ end
50
+
47
51
  # Tell how to apply schema methods. This automatically maps :limit to
48
52
  # :length and :null to :nullable.
49
53
  def apply_schema(name, type, options={})
@@ -41,6 +41,16 @@ module Devise
41
41
  apply_schema :remember_created_at, DateTime
42
42
  end
43
43
 
44
+ # Creates sign_in_count, current_sign_in_at, last_sign_in_at,
45
+ # current_sign_in_ip, last_sign_in_ip.
46
+ def trackable
47
+ apply_schema :sign_in_count, Integer
48
+ apply_schema :current_sign_in_at, DateTime
49
+ apply_schema :last_sign_in_at, DateTime
50
+ apply_schema :current_sign_in_ip, String
51
+ apply_schema :last_sign_in_ip, String
52
+ end
53
+
44
54
  # Overwrite with specific modification to create your own schema.
45
55
  def apply_schema(name, type, options={})
46
56
  raise NotImplementedError
@@ -1,3 +1,3 @@
1
1
  module Devise
2
- VERSION = "0.6.1".freeze
2
+ VERSION = "0.6.2".freeze
3
3
  end
@@ -43,7 +43,8 @@ class ConfirmationTest < ActionController::IntegrationTest
43
43
  end
44
44
 
45
45
  test 'user already confirmed user should not be able to confirm the account again' do
46
- user = create_user
46
+ user = create_user(:confirm => false)
47
+ user.update_attribute(:confirmed_at, Time.now)
47
48
  visit_user_confirmation_with_token(user.confirmation_token)
48
49
 
49
50
  assert_template 'confirmations/new'
@@ -10,17 +10,16 @@ class SessionTimeoutTest < ActionController::IntegrationTest
10
10
  sign_in_as_user
11
11
  old_last_request = last_request_at
12
12
  assert_not_nil last_request_at
13
+
13
14
  get users_path
14
15
  assert_not_nil last_request_at
15
16
  assert_not_equal old_last_request, last_request_at
16
17
  end
17
18
 
18
19
  test 'not time out user session before default limit time' do
19
- user = sign_in_as_user
20
-
21
- # Setup last_request_at to timeout
22
- get edit_user_path(user)
23
- assert_not_nil last_request_at
20
+ sign_in_as_user
21
+ assert_response :success
22
+ assert warden.authenticated?(:user)
24
23
 
25
24
  get users_path
26
25
  assert_response :success
@@ -28,12 +27,8 @@ class SessionTimeoutTest < ActionController::IntegrationTest
28
27
  end
29
28
 
30
29
  test 'time out user session after default limit time' do
31
- sign_in_as_user
32
- assert_response :success
33
- assert warden.authenticated?(:user)
34
-
35
- # Setup last_request_at to timeout
36
- get new_user_path
30
+ user = sign_in_as_user
31
+ get expire_user_path(user)
37
32
  assert_not_nil last_request_at
38
33
 
39
34
  get users_path
@@ -42,15 +37,15 @@ class SessionTimeoutTest < ActionController::IntegrationTest
42
37
  end
43
38
 
44
39
  test 'user configured timeout limit' do
45
- swap Devise, :timeout => 8.minutes do
40
+ swap Devise, :timeout_in => 8.minutes do
46
41
  user = sign_in_as_user
47
42
 
48
- # Setup last_request_at to timeout
49
- get edit_user_path(user)
43
+ get users_path
50
44
  assert_not_nil last_request_at
51
45
  assert_response :success
52
46
  assert warden.authenticated?(:user)
53
47
 
48
+ get expire_user_path(user)
54
49
  get users_path
55
50
  assert_redirected_to new_user_session_path(:timeout => true)
56
51
  assert_not warden.authenticated?(:user)
@@ -61,9 +56,9 @@ class SessionTimeoutTest < ActionController::IntegrationTest
61
56
  store_translations :en, :devise => {
62
57
  :sessions => { :user => { :timeout => 'Session expired!' } }
63
58
  } do
64
- sign_in_as_user
65
- # Setup last_request_at to timeout
66
- get new_user_path
59
+ user = sign_in_as_user
60
+
61
+ get expire_user_path(user)
67
62
  get users_path
68
63
  follow_redirect!
69
64
  assert_contain 'Session expired!'