authenticate 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +17 -0
  4. data/CHANGELOG.md +14 -2
  5. data/Gemfile +2 -2
  6. data/Gemfile.lock +2 -3
  7. data/README.md +103 -30
  8. data/app/controllers/authenticate/authenticate_controller.rb +2 -0
  9. data/app/controllers/authenticate/passwords_controller.rb +1 -1
  10. data/app/controllers/authenticate/sessions_controller.rb +1 -1
  11. data/app/controllers/authenticate/users_controller.rb +1 -1
  12. data/app/views/passwords/new.html.erb +4 -0
  13. data/authenticate.gemspec +8 -10
  14. data/config/locales/authenticate.en.yml +9 -1
  15. data/gemfiles/rails42.gemfile +12 -0
  16. data/lib/authenticate/callbacks/authenticatable.rb +4 -1
  17. data/lib/authenticate/callbacks/brute_force.rb +2 -1
  18. data/lib/authenticate/callbacks/lifetimed.rb +2 -2
  19. data/lib/authenticate/callbacks/timeoutable.rb +1 -1
  20. data/lib/authenticate/callbacks/trackable.rb +1 -1
  21. data/lib/authenticate/controller.rb +12 -3
  22. data/lib/authenticate/debug.rb +8 -3
  23. data/lib/authenticate/engine.rb +3 -0
  24. data/lib/authenticate/lifecycle.rb +25 -16
  25. data/lib/authenticate/model/brute_force.rb +7 -3
  26. data/lib/authenticate/model/db_password.rb +12 -14
  27. data/lib/authenticate/model/email.rb +1 -1
  28. data/lib/authenticate/model/lifetimed.rb +7 -8
  29. data/lib/authenticate/model/password_reset.rb +12 -5
  30. data/lib/authenticate/model/timeoutable.rb +9 -12
  31. data/lib/authenticate/model/trackable.rb +5 -2
  32. data/lib/authenticate/model/username.rb +0 -8
  33. data/lib/authenticate/modules.rb +3 -2
  34. data/lib/authenticate/session.rb +7 -7
  35. data/lib/authenticate/version.rb +1 -1
  36. data/spec/dummy/config/initializers/authenticate.rb +3 -2
  37. data/spec/factories/users.rb +11 -1
  38. data/spec/model/db_password_spec.rb +33 -0
  39. data/spec/model/email_spec.rb +25 -0
  40. data/spec/model/lifetimed_spec.rb +35 -0
  41. data/spec/model/password_reset_spec.rb +81 -0
  42. data/spec/model/session_spec.rb +0 -6
  43. data/spec/model/timeoutable_spec.rb +20 -0
  44. data/spec/model/trackable_spec.rb +56 -0
  45. data/spec/spec_helper.rb +6 -0
  46. metadata +18 -13
@@ -0,0 +1,12 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "bundler", "~> 1.3"
6
+ gem "factory_girl_rails", "~> 4.2"
7
+ gem "rspec-rails", "~> 3.1"
8
+ gem "sqlite3", "~> 1.3"
9
+ gem "pry", :require => false
10
+ gem "rails", "~> 4.2.0"
11
+
12
+ gemspec :path => "../"
@@ -1,4 +1,7 @@
1
+ # Callback to check that the session has been authenticated.
2
+ #
3
+
1
4
  # If user failed to authenticate, toss them out.
2
5
  Authenticate.lifecycle.after_authentication name: 'authenticatable' do |user, session, opts|
3
- throw(:failure, 'Wrong email or password.') unless session.authenticated?
6
+ throw(:failure, I18n.t('callbacks.authenticatable.failure')) unless session && session.authenticated?
4
7
  end
@@ -21,7 +21,8 @@ Authenticate.lifecycle.prepend_after_authentication name: 'brute force protectio
21
21
  # if the user is still locked, let them know how long they are locked for.
22
22
  if user && user.locked?
23
23
  remaining = time_ago_in_words(user.lock_expires_at)
24
- throw(:failure, "Your account is locked, will unlock in #{remaining.to_s}")
24
+ # throw(:failure, "Your account is locked, will unlock in #{remaining.to_s}")
25
+ throw(:failure, I18n.t('callbacks.brute_force.failure', time_remaining: remaining.to_s))
25
26
  end
26
27
 
27
28
  end
@@ -1,6 +1,6 @@
1
1
  # Catch sessions that have been live for too long and kill them, forcing the user to reauthenticate.
2
2
  Authenticate.lifecycle.after_set_user name: 'lifetimed after set_user', except: :authentication do |user, session, options|
3
- if user && user.respond_to?(:max_session_timedout?)
4
- throw(:failure, "Your session has reached it's maximum allowed lifetime, you must log in again") if user.max_session_timedout?
3
+ if user && user.respond_to?(:max_session_lifetime_exceeded?)
4
+ throw(:failure, I18n.t('callbacks.lifetimed.failure')) if user.max_session_lifetime_exceeded?
5
5
  end
6
6
  end
@@ -9,7 +9,7 @@ end
9
9
  # Fail users that have timed out. Otherwise update last_access_at.
10
10
  Authenticate.lifecycle.after_set_user name: 'timeoutable after set_user', except: :authentication do |user, session, options|
11
11
  if user && user.respond_to?(:timedout?)
12
- throw(:failure, 'Your session has expired') if user.timedout?
12
+ throw(:failure, I18n.t('callbacks.timeoutable.failure')) if user.timedout?
13
13
  user.last_access_at = Time.now.utc
14
14
  user.save!
15
15
  end
@@ -1,6 +1,6 @@
1
1
  # Update all standard tracked stats at each authentication.
2
2
  Authenticate.lifecycle.after_authentication name: 'trackable' do |user, session, options|
3
- if user.respond_to?(:update_tracked_fields!)
3
+ if user && user.respond_to?(:update_tracked_fields!)
4
4
  user.update_tracked_fields!(session.request)
5
5
  end
6
6
  end
@@ -14,7 +14,7 @@ module Authenticate
14
14
  def authenticate(params)
15
15
  # todo: get params from User model
16
16
  user_credentials = Authenticate.configuration.user_model_class.credentials(params)
17
- puts "Controller::user_credentials: #{user_credentials.inspect}"
17
+ debug "Controller::user_credentials: #{user_credentials.inspect}"
18
18
  Authenticate.configuration.user_model_class.authenticate(user_credentials)
19
19
  end
20
20
 
@@ -54,7 +54,7 @@ module Authenticate
54
54
  # end
55
55
  #
56
56
  def require_authentication
57
- d 'Controller::require_authentication'
57
+ debug 'Controller::require_authentication'
58
58
  unless authenticated?
59
59
  unauthorized
60
60
  end
@@ -89,10 +89,19 @@ module Authenticate
89
89
  authenticate_session.current_user
90
90
  end
91
91
 
92
+ # Return true if it's an Authenticate controller. Useful if you want to apply a before
93
+ # filter to all controllers, except the ones in Authenticate:
94
+ #
95
+ # before_action :my_filter, unless: :authenticate_controller?
96
+ #
97
+ def authenticate_controller?
98
+ is_a?(Authenticate::AuthenticateController)
99
+ end
100
+
92
101
  protected
93
102
 
94
103
  # User is not authorized, bounce 'em to sign in
95
- def unauthorized(msg = 'You must sign in') # get default message from locale
104
+ def unauthorized(msg = t('flashes.failure_when_not_signed_in'))
96
105
  respond_to do |format|
97
106
  format.any(:js, :json, :xml) { head :unauthorized }
98
107
  format.any {
@@ -2,10 +2,15 @@ module Authenticate
2
2
  module Debug
3
3
  extend ActiveSupport::Concern
4
4
 
5
- def d(msg)
6
- # todo check: Rails constant loaded? Authenticate config read? do a puts otherwise
7
- Rails.logger.info msg.to_s if Authenticate.configuration.debug
5
+
6
+ def debug(msg)
7
+ if defined?(Rails) && defined?(Rails.logger)
8
+ Rails.logger.info msg.to_s if Authenticate.configuration.debug
9
+ else
10
+ puts msg.to_s if Authenticate.configuration.debug
11
+ end
8
12
  end
9
13
 
14
+
10
15
  end
11
16
  end
@@ -1,3 +1,6 @@
1
+ require 'authenticate'
2
+ require 'rails'
3
+
1
4
  module Authenticate
2
5
  class Engine < ::Rails::Engine
3
6
 
@@ -41,10 +41,7 @@ module Authenticate
41
41
 
42
42
  # This callback is triggered after the first time a user is set during per-hit authorization, or during login.
43
43
  def after_set_user(options = {}, method = :push, &block)
44
- raise BlockNotGiven unless block_given?
45
- options = process_opts(options)
46
- # puts "register after_set_user #{options.inspect}"
47
- after_set_user_callbacks.send(method, [block, options])
44
+ add_callback(after_set_user_callbacks, options, method, &block)
48
45
  end
49
46
 
50
47
 
@@ -52,33 +49,38 @@ module Authenticate
52
49
  # A callback to run after the user successfully authenticates, during the login process.
53
50
  # Mechanically identical to [#after_set_user].
54
51
  def after_authentication(options = {}, method = :push, &block)
55
- raise BlockNotGiven unless block_given?
56
- options = process_opts(options)
57
- # puts "register after_authentication #{options}"
58
- after_authentication_callbacks.send(method, [block, options])
52
+ add_callback(after_authentication_callbacks, options, method, &block)
59
53
  end
60
54
 
61
55
 
56
+ # Run callbacks of the given kind.
57
+ #
58
+ # * kind - :authenticate or :after_set_user
59
+ # * args - user, session, opts hash. Opts is an optional event, e.g. { event: :authentication }
60
+ #
61
+ # Example:
62
+ # Authenticate.lifecycle.run_callbacks(:after_set_user, @current_user, self, { event: :authentication })
63
+ #
62
64
  def run_callbacks(kind, *args) # args - |user, session, opts|
63
65
  # Last callback arg MUST be a Hash
64
66
  options = args.last
65
- d "@@@@@@@@@@@@ run_callbacks kind:#{kind} options:#{options.inspect}"
67
+ debug "START Lifecycle.run_callbacks kind:#{kind} options:#{options.inspect}"
66
68
 
67
69
  # each callback has 'conditions' stored with it
68
70
  send("#{kind}_callbacks").each do |callback, conditions|
69
71
  conditions = conditions.dup # make a copy, we mutate it
70
- d "running callback -- #{conditions.inspect}"
71
- conditions.delete_if {|key, val| !@@conditions.include? key}
72
- # d "conditions after filter:#{conditions.inspect}"
72
+ debug "Lifecycle.running callback -- #{conditions.inspect}"
73
+ conditions.delete_if {|key, _val| !@@conditions.include? key}
74
+ # debug "conditions after filter:#{conditions.inspect}"
73
75
  invalid = conditions.find do |key, value|
74
- # d "!!!!!!! conditions key:#{key} value:#{value} options[key]:#{options[key].inspect}"
75
- # d("!value.include?(options[key]):#{!value.include?(options[key])}") if value.is_a?(Array)
76
+ # debug "!!!!!!! conditions key:#{key} value:#{value} options[key]:#{options[key].inspect}"
77
+ # debug("!value.include?(options[key]):#{!value.include?(options[key])}") if value.is_a?(Array)
76
78
  value.is_a?(Array) ? !value.include?(options[key]) : (value != options[key])
77
79
  end
78
- d "callback invalid? #{invalid.inspect}"
80
+ debug "Lifecycle.callback invalid? #{invalid.inspect}"
79
81
  callback.call(*args) unless invalid
80
82
  end
81
- d "FINISHED run_callbacks #{kind}"
83
+ debug "FINISHED Lifecycle.run_callbacks #{kind}"
82
84
  nil
83
85
  end
84
86
 
@@ -89,6 +91,13 @@ module Authenticate
89
91
 
90
92
  private
91
93
 
94
+ def add_callback(callbacks, options = {}, method = :push, &block)
95
+ raise BlockNotGiven unless block_given?
96
+ options = process_opts(options)
97
+ callbacks.send(method, [block, options])
98
+ end
99
+
100
+
92
101
  # set event: to run callback on based on options
93
102
  def process_opts(options)
94
103
  if options.key?(:only)
@@ -7,18 +7,22 @@ module Authenticate
7
7
  # Protect from brute force attacks. Lock accounts that have too many failed consecutive logins.
8
8
  # Todo: email user to allow unlocking via a token.
9
9
  #
10
- # = Columns
10
+ # To enable brute force protection, set the config params shown below. Example:
11
+ #
12
+ # Authenticate.configure do |config|
13
+ # config.bad_login_lockout_period = 5.minutes
14
+ # config.max_consecutive_bad_logins_allowed = 3
15
+ # end
11
16
  #
17
+ # = Columns
12
18
  # * failed_logins_count - each consecutive failed login increments this counter. Set back to 0 on successful login.
13
19
  # * lock_expires_at - datetime a locked account will again become available.
14
20
  #
15
21
  # = Configuration
16
- #
17
22
  # * max_consecutive_bad_logins_allowed - how many failed logins are allowed?
18
23
  # * bad_login_lockout_period - how long is the user locked out? nil indicates forever.
19
24
  #
20
25
  # = Methods
21
- #
22
26
  # The following methods are added to your user model:
23
27
  # * register_failed_login! - increment failed_logins_count, lock account if in violation
24
28
  # * lock! - lock the account, setting the lock_expires_at attribute
@@ -7,22 +7,20 @@ module Authenticate
7
7
  # Encrypts and stores a password in the database to validate the authenticity of a user while logging in.
8
8
  #
9
9
  # Authenticate can plug in any crypto provider, but currently only features BCrypt.
10
+ #
10
11
  # A crypto provider must provide:
11
12
  # * encrypt(secret) - encrypt the secret, @return [String]
12
13
  # * match?(secret, encrypted) - does the secret match the encrypted? @return [Boolean]
13
14
  #
14
15
  # = Columns
15
- #
16
16
  # * encrypted_password - the user's password, encrypted
17
17
  #
18
18
  # = Methods
19
- #
20
19
  # The following methods are added to your user model:
21
20
  # * password=(new_password) - encrypt and set the user password
22
21
  # * password_match?(password) - checks to see if the user's password matches the given password
23
22
  #
24
23
  # = Validations
25
- #
26
24
  # * :password validation, requiring the password is set unless we're skipping due to a password change
27
25
  #
28
26
  module DbPassword
@@ -33,6 +31,7 @@ module Authenticate
33
31
  end
34
32
 
35
33
  included do
34
+ private_class_method :crypto_provider
36
35
  include crypto_provider
37
36
  attr_reader :password
38
37
  attr_accessor :password_changing
@@ -41,16 +40,6 @@ module Authenticate
41
40
 
42
41
 
43
42
 
44
- module ClassMethods
45
-
46
- # We only have one crypto provider at the moment, but look up the provider in the config.
47
- def crypto_provider
48
- Authenticate.configuration.crypto_provider || Authenticate::Crypto::BCrypt
49
- end
50
-
51
- end
52
-
53
-
54
43
  def password_match?(password)
55
44
  match?(password, self.encrypted_password)
56
45
  end
@@ -65,7 +54,16 @@ module Authenticate
65
54
 
66
55
  private
67
56
 
68
- # If we already have an encrypted password and it's not changing, skip the validation.
57
+ module ClassMethods
58
+
59
+ # We only have one crypto provider at the moment, but look up the provider in the config.
60
+ def crypto_provider
61
+ Authenticate.configuration.crypto_provider || Authenticate::Crypto::BCrypt
62
+ end
63
+ end
64
+
65
+
66
+ # If we already have an encrypted password and it's not changing, skip the validation.
69
67
  def skip_password_validation?
70
68
  encrypted_password.present? && !password_changing
71
69
  end
@@ -16,7 +16,7 @@ module Authenticate
16
16
  # = Methods
17
17
  # * normalize_email - normalize the email, removing spaces etc, before saving
18
18
  #
19
- # = class methods
19
+ # = Class Methods
20
20
  # * credentials(params) - return the credentials required for authorization by email
21
21
  # * authenticate(credentials) - find user with given email, validate their password, return the user if authenticated
22
22
  # * normalize_email(email) - clean up the given email and return it.
@@ -3,11 +3,10 @@ require 'authenticate/callbacks/lifetimed'
3
3
  module Authenticate
4
4
  module Model
5
5
 
6
- # The user session has a maximum allowed lifespan, after which the session is expired and requires
6
+ # Imposes a maximum allowed lifespan on a user's session, after which the session is expired and requires
7
7
  # re-authentication.
8
8
  #
9
- # = configuration
10
- #
9
+ # = Configuration
11
10
  # Set the maximum session lifetime in the initializer, giving a timestamp.
12
11
  #
13
12
  # Authenticate.configure do |config|
@@ -16,11 +15,11 @@ module Authenticate
16
15
  #
17
16
  # If the max_session_lifetime configuration parameter is nil, the :lifetimed module is not loaded.
18
17
  #
19
- # = columns
20
- # Requires the `current_sign_in_at` column. This column is managed by the :trackable plugin.
18
+ # = Columns
19
+ # * current_sign_in_at - requires `current_sign_in_at` column. This column is managed by the :trackable plugin.
21
20
  #
22
- # = methods
23
- # - max_session_timedout? - true if the user's session is too old and must be reaped
21
+ # = Methods
22
+ # * max_session_lifetime_exceeded? - true if the user's session has exceeded the max lifetime allowed
24
23
  #
25
24
  #
26
25
  module Lifetimed
@@ -31,7 +30,7 @@ module Authenticate
31
30
  end
32
31
 
33
32
  # Has the session reached its maximum allowed lifespan?
34
- def max_session_timedout?
33
+ def max_session_lifetime_exceeded?
35
34
  return false if max_session_lifetime.nil?
36
35
  return false if current_sign_in_at.nil?
37
36
  current_sign_in_at <= max_session_lifetime.ago
@@ -2,10 +2,17 @@ module Authenticate
2
2
  module Model
3
3
 
4
4
  # Support 'forgot my password' functionality.
5
- # Methods:
5
+ #
6
+ # = Columns
7
+ # * password_reset_token - token required to reset a password
8
+ # * password_reset_sent_at - datetime password reset token was emailed to user
9
+ # * email - email address of user
10
+ #
11
+ # = Methods
6
12
  # * update_password(new_password) - call password setter below, generate a new session token if user.valid?, & save
7
- # *
8
-
13
+ # * forgot_password! - generate a new password reset token, timestamp, and save
14
+ # * reset_password_period_valid? - is the password reset token still usable?
15
+ #
9
16
  module PasswordReset
10
17
  extend ActiveSupport::Concern
11
18
 
@@ -59,9 +66,9 @@ module Authenticate
59
66
  # reset_password_period_valid? # returns true
60
67
  #
61
68
  def reset_password_period_valid?
62
- reset_within = Authenticate.configuration.reset_password_within.ago.utc
69
+ reset_within = Authenticate.configuration.reset_password_within
63
70
  return true if reset_within.nil?
64
- self.password_reset_sent_at && self.password_reset_sent_at.utc >= reset_within
71
+ self.password_reset_sent_at && self.password_reset_sent_at.utc >= reset_within.ago.utc
65
72
  end
66
73
 
67
74
  private
@@ -6,22 +6,20 @@ module Authenticate
6
6
  # Expire user sessions that have not been accessed within a certain period of time.
7
7
  # Expired users will be asked for credentials again.
8
8
  #
9
- # = Columns
9
+ # Timeoutable is enabled and configured with the `timeout_in` configuration parameter.
10
+ # Example:
11
+ #
12
+ # Authenticate.configure do |config|
13
+ # config.timeout_in = 15.minutes
14
+ # end
10
15
  #
16
+ # = Columns
11
17
  # This module expects and tracks this column on your user model:
12
18
  # * last_access_at - datetime of the last access by the user
13
19
  #
14
20
  # = Configuration
15
- #
16
21
  # * timeout_in - maximum idle time allowed before session is invalidated. nil shuts off this feature.
17
22
  #
18
- # Timeoutable is enabled and configured with the `timeout_in` configuration parameter.
19
- # `timeout_in` expects a timestamp. Example:
20
- #
21
- # Authenticate.configure do |config|
22
- # config.timeout_in = 15.minutes
23
- # end
24
- #
25
23
  # You must specify a non-nil timeout_in in your initializer to enable Timeoutable.
26
24
  #
27
25
  # = Methods
@@ -37,14 +35,13 @@ module Authenticate
37
35
 
38
36
  # Checks whether the user session has expired based on configured time.
39
37
  def timedout?
40
- Rails.logger.info "User.timedout? timeout_in:#{timeout_in} last_access_at:#{last_access_at}"
41
38
  return false if timeout_in.nil?
42
39
  return false if last_access_at.nil?
43
- # result = Time.now.utc > (last_access_at + timeout_in)
44
- Rails.logger.info "User.timedout? #{last_access_at >= timeout_in.ago} timeout_in.ago:#{timeout_in.ago} last_access_at:#{last_access_at}"
45
40
  last_access_at <= timeout_in.ago
46
41
  end
47
42
 
43
+ private
44
+
48
45
  def timeout_in
49
46
  Authenticate.configuration.timeout_in
50
47
  end
@@ -5,8 +5,11 @@ module Authenticate
5
5
 
6
6
  # Track information about your user sign ins. This module is always enabled.
7
7
  #
8
- # == Columns
9
- # This module expects and tracks the following columns on your user model:
8
+ # = Methods
9
+ # * update_tracked_fields - update the user's tracked fields based on the request.
10
+ # * update_tracked_fields! - update tracked fields and save immediately, bypassing validations
11
+ #
12
+ # = Columns
10
13
  # - sign_in_count - increase every time a sign in is successful
11
14
  # - current_sign_in_at - a timestamp updated at each sign in
12
15
  # - last_sign_in_at - a timestamp of the previous sign in