authenticate 0.1.0 → 0.2.0
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/CHANGELOG.md +11 -0
- data/Gemfile +0 -4
- data/Gemfile.lock +0 -5
- data/README.md +149 -78
- data/app/controllers/authenticate/passwords_controller.rb +130 -0
- data/app/controllers/authenticate/sessions_controller.rb +46 -0
- data/app/controllers/authenticate/users_controller.rb +46 -0
- data/app/mailers/authenticate_mailer.rb +13 -0
- data/app/views/authenticate_mailer/change_password.html.erb +8 -0
- data/app/views/authenticate_mailer/change_password.text.erb +5 -0
- data/app/views/layouts/application.html.erb +25 -0
- data/app/views/passwords/edit.html.erb +20 -0
- data/app/views/passwords/new.html.erb +19 -0
- data/app/views/sessions/new.html.erb +28 -0
- data/app/views/users/new.html.erb +24 -0
- data/authenticate.gemspec +1 -2
- data/config/locales/authenticate.en.yml +57 -0
- data/config/routes.rb +14 -1
- data/lib/authenticate/callbacks/brute_force.rb +5 -9
- data/lib/authenticate/callbacks/lifetimed.rb +1 -0
- data/lib/authenticate/callbacks/timeoutable.rb +2 -1
- data/lib/authenticate/callbacks/trackable.rb +1 -3
- data/lib/authenticate/configuration.rb +94 -5
- data/lib/authenticate/controller.rb +69 -9
- data/lib/authenticate/debug.rb +1 -0
- data/lib/authenticate/engine.rb +4 -11
- data/lib/authenticate/model/brute_force.rb +22 -3
- data/lib/authenticate/model/db_password.rb +12 -7
- data/lib/authenticate/model/email.rb +8 -10
- data/lib/authenticate/model/password_reset.rb +76 -0
- data/lib/authenticate/model/timeoutable.rb +9 -3
- data/lib/authenticate/model/trackable.rb +1 -1
- data/lib/authenticate/model/username.rb +21 -8
- data/lib/authenticate/modules.rb +19 -1
- data/lib/authenticate/session.rb +3 -1
- data/lib/authenticate/user.rb +6 -1
- data/lib/authenticate/version.rb +1 -1
- data/lib/generators/authenticate/controllers/USAGE +12 -0
- data/lib/generators/authenticate/controllers/controllers_generator.rb +21 -0
- data/lib/generators/authenticate/install/USAGE +7 -0
- data/lib/generators/authenticate/install/install_generator.rb +140 -0
- data/lib/generators/authenticate/install/templates/authenticate.rb +22 -0
- data/lib/generators/authenticate/install/templates/db/migrate/add_authenticate_brute_force_to_users.rb +6 -0
- data/lib/generators/authenticate/install/templates/db/migrate/add_authenticate_password_reset_to_users.rb +7 -0
- data/lib/generators/authenticate/install/templates/db/migrate/add_authenticate_timeoutable_to_users.rb +5 -0
- data/lib/generators/authenticate/install/templates/db/migrate/add_authenticate_to_users.rb +21 -0
- data/lib/generators/authenticate/install/templates/db/migrate/create_users.rb +14 -0
- data/lib/generators/authenticate/install/templates/user.rb +3 -0
- data/lib/generators/authenticate/routes/USAGE +8 -0
- data/lib/generators/authenticate/routes/routes_generator.rb +32 -0
- data/lib/generators/authenticate/routes/templates/routes.rb +10 -0
- data/lib/generators/authenticate/views/USAGE +13 -0
- data/lib/generators/authenticate/views/views_generator.rb +21 -0
- data/spec/dummy/app/controllers/application_controller.rb +1 -0
- data/spec/dummy/config/initializers/authenticate.rb +12 -5
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/migrate/20160130192728_create_users.rb +18 -0
- data/spec/dummy/db/migrate/20160130192729_add_authenticate_brute_force_to_users.rb +6 -0
- data/spec/dummy/db/migrate/20160130192730_add_authenticate_timeoutable_to_users.rb +5 -0
- data/spec/dummy/db/migrate/20160130192731_add_authenticate_password_reset_to_users.rb +7 -0
- data/spec/dummy/db/schema.rb +14 -10
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/factories/users.rb +5 -8
- data/spec/model/brute_force_spec.rb +63 -0
- data/spec/model/session_spec.rb +4 -0
- data/spec/model/user_spec.rb +15 -5
- data/spec/spec_helper.rb +2 -1
- metadata +41 -9
- data/app/controllers/.keep +0 -0
- data/app/mailers/.keep +0 -0
- data/app/views/.keep +0 -0
- data/spec/dummy/db/migrate/20160120003910_create_users.rb +0 -18
@@ -40,7 +40,7 @@ module Authenticate
|
|
40
40
|
end
|
41
41
|
|
42
42
|
|
43
|
-
# Use this as a before_action to restrict controller actions to authenticated users.
|
43
|
+
# Use this filter as a before_action to restrict controller actions to authenticated users.
|
44
44
|
# Consider using in application_controller to restrict access to all controllers.
|
45
45
|
#
|
46
46
|
# Example:
|
@@ -89,22 +89,82 @@ module Authenticate
|
|
89
89
|
authenticate_session.current_user
|
90
90
|
end
|
91
91
|
|
92
|
-
|
93
|
-
|
94
|
-
def authenticate_session
|
95
|
-
@authenticate_session ||= Authenticate::Session.new(request, cookies)
|
96
|
-
end
|
92
|
+
protected
|
97
93
|
|
98
|
-
|
94
|
+
# User is not authorized, bounce 'em to sign in
|
95
|
+
def unauthorized(msg = 'You must sign in') # get default message from locale
|
99
96
|
respond_to do |format|
|
100
97
|
format.any(:js, :json, :xml) { head :unauthorized }
|
101
98
|
format.any {
|
102
|
-
|
103
|
-
|
99
|
+
redirect_unauthorized(msg)
|
100
|
+
}
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def redirect_unauthorized(flash_message)
|
105
|
+
store_location
|
106
|
+
|
107
|
+
if flash_message
|
108
|
+
flash[:notice] = flash_message # TODO use locales
|
109
|
+
end
|
110
|
+
|
111
|
+
if authenticated?
|
112
|
+
redirect_to url_after_denied_access_when_signed_in
|
113
|
+
else
|
114
|
+
redirect_to url_after_denied_access_when_signed_out
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
def redirect_back_or(default)
|
120
|
+
redirect_to(stored_location || default)
|
121
|
+
clear_stored_location
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
# Used as the redirect location when {#unauthorized} is called and there is a
|
126
|
+
# currently signed in user.
|
127
|
+
#
|
128
|
+
# @return [String]
|
129
|
+
def url_after_denied_access_when_signed_in
|
130
|
+
Authenticate.configuration.redirect_url
|
131
|
+
end
|
132
|
+
|
133
|
+
# Used as the redirect location when {#unauthorized} is called and there is
|
134
|
+
# no currently signed in user.
|
135
|
+
#
|
136
|
+
# @return [String]
|
137
|
+
def url_after_denied_access_when_signed_out
|
138
|
+
sign_in_url
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
# Write location to return to in a cookie. This is 12-factor compliant, cloud-safe.
|
144
|
+
def store_location
|
145
|
+
if request.get?
|
146
|
+
value = {
|
147
|
+
expires: nil,
|
148
|
+
httponly: true,
|
149
|
+
path: nil,
|
150
|
+
secure: Authenticate.configuration.secure_cookie,
|
151
|
+
value: request.original_fullpath
|
104
152
|
}
|
153
|
+
cookies[:authenticate_return_to] = value
|
105
154
|
end
|
106
155
|
end
|
107
156
|
|
157
|
+
def stored_location
|
158
|
+
cookies[:authenticate_return_to]
|
159
|
+
end
|
160
|
+
|
161
|
+
def clear_stored_location
|
162
|
+
cookies.delete :authenticate_return_to
|
163
|
+
end
|
164
|
+
|
165
|
+
def authenticate_session
|
166
|
+
@authenticate_session ||= Authenticate::Session.new(request, cookies)
|
167
|
+
end
|
108
168
|
|
109
169
|
end
|
110
170
|
end
|
data/lib/authenticate/debug.rb
CHANGED
data/lib/authenticate/engine.rb
CHANGED
@@ -1,20 +1,13 @@
|
|
1
1
|
module Authenticate
|
2
2
|
class Engine < ::Rails::Engine
|
3
|
-
# config.generators do |g|
|
4
|
-
# g.test_framework :rspec
|
5
|
-
# g.fixture_replacement :factory_girl, dir: 'spec/factories'
|
6
|
-
# end
|
7
3
|
|
8
|
-
|
4
|
+
initializer 'authenticate.filter' do |app|
|
5
|
+
app.config.filter_parameters += [:password, :token]
|
6
|
+
end
|
7
|
+
|
9
8
|
config.generators do |g|
|
10
9
|
g.test_framework :rspec
|
11
10
|
g.fixture_replacement :factory_girl, dir: 'spec/factories'
|
12
|
-
|
13
|
-
|
14
|
-
# g.test_framework :rspec, fixture: false
|
15
|
-
# g.fixture_replacement :factory_girl, dir: 'spec/factories'
|
16
|
-
# g.assets false
|
17
|
-
# g.helper false
|
18
11
|
end
|
19
12
|
|
20
13
|
end
|
@@ -4,9 +4,28 @@ module Authenticate
|
|
4
4
|
module Model
|
5
5
|
|
6
6
|
|
7
|
-
# Protect from brute force attacks.
|
8
|
-
#
|
9
|
-
#
|
7
|
+
# Protect from brute force attacks. Lock accounts that have too many failed consecutive logins.
|
8
|
+
# Todo: email user to allow unlocking via a token.
|
9
|
+
#
|
10
|
+
# = Columns
|
11
|
+
#
|
12
|
+
# * failed_logins_count - each consecutive failed login increments this counter. Set back to 0 on successful login.
|
13
|
+
# * lock_expires_at - datetime a locked account will again become available.
|
14
|
+
#
|
15
|
+
# = Configuration
|
16
|
+
#
|
17
|
+
# * max_consecutive_bad_logins_allowed - how many failed logins are allowed?
|
18
|
+
# * bad_login_lockout_period - how long is the user locked out? nil indicates forever.
|
19
|
+
#
|
20
|
+
# = Methods
|
21
|
+
#
|
22
|
+
# The following methods are added to your user model:
|
23
|
+
# * register_failed_login! - increment failed_logins_count, lock account if in violation
|
24
|
+
# * lock! - lock the account, setting the lock_expires_at attribute
|
25
|
+
# * unlock! - reset failed_logins_count to 0, lock_expires_at to nil
|
26
|
+
# * locked? - is the account locked? @return[Boolean]
|
27
|
+
# * unlocked? - is the account unlocked? @return[Boolean]
|
28
|
+
#
|
10
29
|
module BruteForce
|
11
30
|
extend ActiveSupport::Concern
|
12
31
|
|
@@ -4,19 +4,26 @@ require 'authenticate/crypto/bcrypt'
|
|
4
4
|
module Authenticate
|
5
5
|
module Model
|
6
6
|
|
7
|
-
# Encrypts and stores a password in the database to validate the authenticity of a user while
|
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
|
+
# A crypto provider must provide:
|
11
|
+
# * encrypt(secret) - encrypt the secret, @return [String]
|
12
|
+
# * match?(secret, encrypted) - does the secret match the encrypted? @return [Boolean]
|
13
|
+
#
|
14
|
+
# = Columns
|
15
|
+
#
|
16
|
+
# * encrypted_password - the user's password, encrypted
|
10
17
|
#
|
11
18
|
# = Methods
|
12
19
|
#
|
13
20
|
# The following methods are added to your user model:
|
14
|
-
#
|
15
|
-
#
|
21
|
+
# * password=(new_password) - encrypt and set the user password
|
22
|
+
# * password_match?(password) - checks to see if the user's password matches the given password
|
16
23
|
#
|
17
24
|
# = Validations
|
18
25
|
#
|
19
|
-
#
|
26
|
+
# * :password validation, requiring the password is set unless we're skipping due to a password change
|
20
27
|
#
|
21
28
|
module DbPassword
|
22
29
|
extend ActiveSupport::Concern
|
@@ -36,8 +43,7 @@ module Authenticate
|
|
36
43
|
|
37
44
|
module ClassMethods
|
38
45
|
|
39
|
-
# We only have one crypto provider at the moment, but
|
40
|
-
# to install different crypto.
|
46
|
+
# We only have one crypto provider at the moment, but look up the provider in the config.
|
41
47
|
def crypto_provider
|
42
48
|
Authenticate.configuration.crypto_provider || Authenticate::Crypto::BCrypt
|
43
49
|
end
|
@@ -45,7 +51,6 @@ module Authenticate
|
|
45
51
|
end
|
46
52
|
|
47
53
|
|
48
|
-
|
49
54
|
def password_match?(password)
|
50
55
|
match?(password, self.encrypted_password)
|
51
56
|
end
|
@@ -3,24 +3,24 @@ require 'email_validator'
|
|
3
3
|
module Authenticate
|
4
4
|
module Model
|
5
5
|
|
6
|
-
# Use :email as the identifier for the user.
|
6
|
+
# Use :email as the identifier for the user. Email must be unique.
|
7
7
|
#
|
8
8
|
# = Columns
|
9
|
-
#
|
9
|
+
# * email - the email address of the user
|
10
10
|
#
|
11
11
|
# = Validations
|
12
|
-
#
|
12
|
+
# * :email - require email is set, is a valid format, and is unique
|
13
13
|
#
|
14
14
|
# = Callbacks
|
15
|
-
# - :normalize_email - normalize the email, removing spaces etc, before saving
|
16
15
|
#
|
17
16
|
# = Methods
|
18
|
-
#
|
17
|
+
# * normalize_email - normalize the email, removing spaces etc, before saving
|
19
18
|
#
|
20
19
|
# = class methods
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
20
|
+
# * credentials(params) - return the credentials required for authorization by email
|
21
|
+
# * authenticate(credentials) - find user with given email, validate their password, return the user if authenticated
|
22
|
+
# * normalize_email(email) - clean up the given email and return it.
|
23
|
+
# * find_by_credentials(credentials) - find and return the user with the email address in the credentials
|
24
24
|
#
|
25
25
|
module Email
|
26
26
|
extend ActiveSupport::Concern
|
@@ -41,7 +41,6 @@ module Authenticate
|
|
41
41
|
module ClassMethods
|
42
42
|
|
43
43
|
def credentials(params)
|
44
|
-
# todo closure from configuration
|
45
44
|
[params[:session][:email], params[:session][:password]]
|
46
45
|
end
|
47
46
|
|
@@ -52,7 +51,6 @@ module Authenticate
|
|
52
51
|
|
53
52
|
def find_by_credentials(credentials)
|
54
53
|
email = credentials[0]
|
55
|
-
puts "find_by_credentials email: #{email}"
|
56
54
|
find_by_email normalize_email(email)
|
57
55
|
end
|
58
56
|
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Authenticate
|
2
|
+
module Model
|
3
|
+
|
4
|
+
# Support 'forgot my password' functionality.
|
5
|
+
# Methods:
|
6
|
+
# * update_password(new_password) - call password setter below, generate a new session token if user.valid?, & save
|
7
|
+
# *
|
8
|
+
|
9
|
+
module PasswordReset
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
def self.required_fields(klass)
|
13
|
+
[:password_reset_token, :password_reset_sent_at, :email]
|
14
|
+
end
|
15
|
+
|
16
|
+
# Sets the user's password to the new value. The new password will be encrypted with
|
17
|
+
# the selected encryption scheme (defaults to Bcrypt).
|
18
|
+
#
|
19
|
+
# Updating the user password also generates a new session token.
|
20
|
+
#
|
21
|
+
# Validations will be run as part of this update. If the user instance is
|
22
|
+
# not valid, the password change will not be persisted, and this method will
|
23
|
+
# return `false`.
|
24
|
+
#
|
25
|
+
# @return [Boolean] Was the save successful?
|
26
|
+
def update_password(new_password)
|
27
|
+
return false unless reset_password_period_valid?
|
28
|
+
|
29
|
+
self.password_changing = true
|
30
|
+
self.password = new_password
|
31
|
+
|
32
|
+
if valid?
|
33
|
+
clear_reset_password_token
|
34
|
+
generate_session_token
|
35
|
+
end
|
36
|
+
|
37
|
+
save
|
38
|
+
end
|
39
|
+
|
40
|
+
# Generates a {#password_reset_token} for the user, which allows them to reset
|
41
|
+
# their password via an email link.
|
42
|
+
#
|
43
|
+
# The user model is saved without validations. Any other changes you made to
|
44
|
+
# this user instance will also be persisted, without validation.
|
45
|
+
# It is intended to be called on an instance with no changes (`dirty? == false`).
|
46
|
+
#
|
47
|
+
# @return [Boolean] Was the save successful?
|
48
|
+
def forgot_password!
|
49
|
+
self.password_reset_token = Authenticate::Token.new
|
50
|
+
self.password_reset_sent_at = Time.now.utc
|
51
|
+
save validate: false
|
52
|
+
end
|
53
|
+
|
54
|
+
# Checks if the reset password token is within the time limit.
|
55
|
+
# If the application's reset_password_within is nil, then always return true.
|
56
|
+
#
|
57
|
+
# Example:
|
58
|
+
# # reset_password_within = 1.day and reset_password_sent_at = today
|
59
|
+
# reset_password_period_valid? # returns true
|
60
|
+
#
|
61
|
+
def reset_password_period_valid?
|
62
|
+
reset_within = Authenticate.configuration.reset_password_within.ago.utc
|
63
|
+
return true if reset_within.nil?
|
64
|
+
self.password_reset_sent_at && self.password_reset_sent_at.utc >= reset_within
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def clear_reset_password_token
|
70
|
+
self.password_reset_token = nil
|
71
|
+
self.password_reset_sent_at = nil
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -6,12 +6,14 @@ 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
|
+
# = Columns
|
10
10
|
#
|
11
11
|
# This module expects and tracks this column on your user model:
|
12
|
-
#
|
12
|
+
# * last_access_at - datetime of the last access by the user
|
13
13
|
#
|
14
|
-
#
|
14
|
+
# = Configuration
|
15
|
+
#
|
16
|
+
# * timeout_in - maximum idle time allowed before session is invalidated. nil shuts off this feature.
|
15
17
|
#
|
16
18
|
# Timeoutable is enabled and configured with the `timeout_in` configuration parameter.
|
17
19
|
# `timeout_in` expects a timestamp. Example:
|
@@ -22,6 +24,10 @@ module Authenticate
|
|
22
24
|
#
|
23
25
|
# You must specify a non-nil timeout_in in your initializer to enable Timeoutable.
|
24
26
|
#
|
27
|
+
# = Methods
|
28
|
+
# * timedout? - has this user timed out? @return[Boolean]
|
29
|
+
# * timeout_in - look up timeout period in config, @return [ActiveSupport::CoreExtensions::Numeric::Time]
|
30
|
+
#
|
25
31
|
module Timeoutable
|
26
32
|
extend ActiveSupport::Concern
|
27
33
|
|
@@ -7,7 +7,7 @@ module Authenticate
|
|
7
7
|
#
|
8
8
|
# == Columns
|
9
9
|
# This module expects and tracks the following columns on your user model:
|
10
|
-
# - sign_in_count - increase every time a sign in is
|
10
|
+
# - sign_in_count - increase every time a sign in is successful
|
11
11
|
# - current_sign_in_at - a timestamp updated at each sign in
|
12
12
|
# - last_sign_in_at - a timestamp of the previous sign in
|
13
13
|
# - current_sign_in_ip - the remote ip address of the user at sign in
|
@@ -1,6 +1,19 @@
|
|
1
1
|
module Authenticate
|
2
2
|
module Model
|
3
3
|
|
4
|
+
# Use :username as the identifier for the user. Username must be unique.
|
5
|
+
#
|
6
|
+
# = Columns
|
7
|
+
# * username - the username of your user
|
8
|
+
#
|
9
|
+
# = Validations
|
10
|
+
# * :username requires username is set, ensure it is unique
|
11
|
+
#
|
12
|
+
# = class methods
|
13
|
+
# * credentials(params) - return the credentials required for authorization by username
|
14
|
+
# * authenticate(credentials) - find user with given username, validate their password, return the user if authenticated
|
15
|
+
# * find_by_credentials(credentials) - find and return the user with the username in the credentials
|
16
|
+
#
|
4
17
|
module Username
|
5
18
|
extend ActiveSupport::Concern
|
6
19
|
|
@@ -9,7 +22,7 @@ module Authenticate
|
|
9
22
|
end
|
10
23
|
|
11
24
|
included do
|
12
|
-
before_validation :normalize_username
|
25
|
+
# before_validation :normalize_username
|
13
26
|
validates :username,
|
14
27
|
presence: true,
|
15
28
|
uniqueness: { allow_blank: true }
|
@@ -27,17 +40,17 @@ module Authenticate
|
|
27
40
|
|
28
41
|
def find_by_credentials(credentials)
|
29
42
|
username = credentials[0]
|
30
|
-
find_by_username
|
43
|
+
find_by_username username
|
31
44
|
end
|
32
45
|
|
33
|
-
def normalize_username(username)
|
34
|
-
|
35
|
-
end
|
46
|
+
# def normalize_username(username)
|
47
|
+
# username.to_s.downcase.gsub(/\s+/, '')
|
48
|
+
# end
|
36
49
|
end
|
37
50
|
|
38
|
-
def normalize_username
|
39
|
-
|
40
|
-
end
|
51
|
+
# def normalize_username
|
52
|
+
# self.username = self.class.normalize_username(username)
|
53
|
+
# end
|
41
54
|
|
42
55
|
end
|
43
56
|
|
data/lib/authenticate/modules.rb
CHANGED
@@ -2,9 +2,27 @@ module Authenticate
|
|
2
2
|
module Modules
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
|
-
#
|
5
|
+
# Module to help Authenticate's user model load Authenticate modules.
|
6
|
+
#
|
7
|
+
# All Authenticate modules implement ActiveSupport::Concern.
|
8
|
+
#
|
9
|
+
# Modules can optionally define a class method to define what attributes they require present
|
10
|
+
# in the user model. For example, :username declares:
|
11
|
+
#
|
12
|
+
# module Username
|
13
|
+
# extend ActiveSupport::Concern
|
14
|
+
#
|
15
|
+
# def self.required_fields(klass)
|
16
|
+
# [:username]
|
17
|
+
# end
|
18
|
+
# ...
|
19
|
+
#
|
20
|
+
# If the model class is missing a required field, Authenticate will fail with a MissingAttribute error.
|
21
|
+
# The error will declare what required fields are missing.
|
6
22
|
module ClassMethods
|
7
23
|
|
24
|
+
# Load all modules declared in Authenticate.configuration.modules.
|
25
|
+
# Requires them, then loads as a constant, then checks fields, and finally includes.
|
8
26
|
def load_modules
|
9
27
|
constants = []
|
10
28
|
Authenticate.configuration.modules.each do |mod|
|