authenticate 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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