authenticate 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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|
|