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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +17 -0
- data/CHANGELOG.md +14 -2
- data/Gemfile +2 -2
- data/Gemfile.lock +2 -3
- data/README.md +103 -30
- data/app/controllers/authenticate/authenticate_controller.rb +2 -0
- data/app/controllers/authenticate/passwords_controller.rb +1 -1
- data/app/controllers/authenticate/sessions_controller.rb +1 -1
- data/app/controllers/authenticate/users_controller.rb +1 -1
- data/app/views/passwords/new.html.erb +4 -0
- data/authenticate.gemspec +8 -10
- data/config/locales/authenticate.en.yml +9 -1
- data/gemfiles/rails42.gemfile +12 -0
- data/lib/authenticate/callbacks/authenticatable.rb +4 -1
- data/lib/authenticate/callbacks/brute_force.rb +2 -1
- data/lib/authenticate/callbacks/lifetimed.rb +2 -2
- data/lib/authenticate/callbacks/timeoutable.rb +1 -1
- data/lib/authenticate/callbacks/trackable.rb +1 -1
- data/lib/authenticate/controller.rb +12 -3
- data/lib/authenticate/debug.rb +8 -3
- data/lib/authenticate/engine.rb +3 -0
- data/lib/authenticate/lifecycle.rb +25 -16
- data/lib/authenticate/model/brute_force.rb +7 -3
- data/lib/authenticate/model/db_password.rb +12 -14
- data/lib/authenticate/model/email.rb +1 -1
- data/lib/authenticate/model/lifetimed.rb +7 -8
- data/lib/authenticate/model/password_reset.rb +12 -5
- data/lib/authenticate/model/timeoutable.rb +9 -12
- data/lib/authenticate/model/trackable.rb +5 -2
- data/lib/authenticate/model/username.rb +0 -8
- data/lib/authenticate/modules.rb +3 -2
- data/lib/authenticate/session.rb +7 -7
- data/lib/authenticate/version.rb +1 -1
- data/spec/dummy/config/initializers/authenticate.rb +3 -2
- data/spec/factories/users.rb +11 -1
- data/spec/model/db_password_spec.rb +33 -0
- data/spec/model/email_spec.rb +25 -0
- data/spec/model/lifetimed_spec.rb +35 -0
- data/spec/model/password_reset_spec.rb +81 -0
- data/spec/model/session_spec.rb +0 -6
- data/spec/model/timeoutable_spec.rb +20 -0
- data/spec/model/trackable_spec.rb +56 -0
- data/spec/spec_helper.rb +6 -0
- 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, '
|
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?(:
|
4
|
-
throw(:failure,
|
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, '
|
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
|
-
|
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
|
-
|
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 = '
|
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 {
|
data/lib/authenticate/debug.rb
CHANGED
@@ -2,10 +2,15 @@ module Authenticate
|
|
2
2
|
module Debug
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
Rails
|
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
|
data/lib/authenticate/engine.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
71
|
-
conditions.delete_if {|key,
|
72
|
-
#
|
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
|
-
#
|
75
|
-
#
|
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
|
-
|
80
|
+
debug "Lifecycle.callback invalid? #{invalid.inspect}"
|
79
81
|
callback.call(*args) unless invalid
|
80
82
|
end
|
81
|
-
|
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
|
-
#
|
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
|
-
|
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
|
-
# =
|
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
|
-
#
|
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
|
-
# =
|
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
|
-
# =
|
20
|
-
#
|
18
|
+
# = Columns
|
19
|
+
# * current_sign_in_at - requires `current_sign_in_at` column. This column is managed by the :trackable plugin.
|
21
20
|
#
|
22
|
-
# =
|
23
|
-
#
|
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
|
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
|
-
#
|
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
|
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
|
-
#
|
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
|
-
#
|
9
|
-
#
|
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
|