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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/Gemfile +0 -4
  4. data/Gemfile.lock +0 -5
  5. data/README.md +149 -78
  6. data/app/controllers/authenticate/passwords_controller.rb +130 -0
  7. data/app/controllers/authenticate/sessions_controller.rb +46 -0
  8. data/app/controllers/authenticate/users_controller.rb +46 -0
  9. data/app/mailers/authenticate_mailer.rb +13 -0
  10. data/app/views/authenticate_mailer/change_password.html.erb +8 -0
  11. data/app/views/authenticate_mailer/change_password.text.erb +5 -0
  12. data/app/views/layouts/application.html.erb +25 -0
  13. data/app/views/passwords/edit.html.erb +20 -0
  14. data/app/views/passwords/new.html.erb +19 -0
  15. data/app/views/sessions/new.html.erb +28 -0
  16. data/app/views/users/new.html.erb +24 -0
  17. data/authenticate.gemspec +1 -2
  18. data/config/locales/authenticate.en.yml +57 -0
  19. data/config/routes.rb +14 -1
  20. data/lib/authenticate/callbacks/brute_force.rb +5 -9
  21. data/lib/authenticate/callbacks/lifetimed.rb +1 -0
  22. data/lib/authenticate/callbacks/timeoutable.rb +2 -1
  23. data/lib/authenticate/callbacks/trackable.rb +1 -3
  24. data/lib/authenticate/configuration.rb +94 -5
  25. data/lib/authenticate/controller.rb +69 -9
  26. data/lib/authenticate/debug.rb +1 -0
  27. data/lib/authenticate/engine.rb +4 -11
  28. data/lib/authenticate/model/brute_force.rb +22 -3
  29. data/lib/authenticate/model/db_password.rb +12 -7
  30. data/lib/authenticate/model/email.rb +8 -10
  31. data/lib/authenticate/model/password_reset.rb +76 -0
  32. data/lib/authenticate/model/timeoutable.rb +9 -3
  33. data/lib/authenticate/model/trackable.rb +1 -1
  34. data/lib/authenticate/model/username.rb +21 -8
  35. data/lib/authenticate/modules.rb +19 -1
  36. data/lib/authenticate/session.rb +3 -1
  37. data/lib/authenticate/user.rb +6 -1
  38. data/lib/authenticate/version.rb +1 -1
  39. data/lib/generators/authenticate/controllers/USAGE +12 -0
  40. data/lib/generators/authenticate/controllers/controllers_generator.rb +21 -0
  41. data/lib/generators/authenticate/install/USAGE +7 -0
  42. data/lib/generators/authenticate/install/install_generator.rb +140 -0
  43. data/lib/generators/authenticate/install/templates/authenticate.rb +22 -0
  44. data/lib/generators/authenticate/install/templates/db/migrate/add_authenticate_brute_force_to_users.rb +6 -0
  45. data/lib/generators/authenticate/install/templates/db/migrate/add_authenticate_password_reset_to_users.rb +7 -0
  46. data/lib/generators/authenticate/install/templates/db/migrate/add_authenticate_timeoutable_to_users.rb +5 -0
  47. data/lib/generators/authenticate/install/templates/db/migrate/add_authenticate_to_users.rb +21 -0
  48. data/lib/generators/authenticate/install/templates/db/migrate/create_users.rb +14 -0
  49. data/lib/generators/authenticate/install/templates/user.rb +3 -0
  50. data/lib/generators/authenticate/routes/USAGE +8 -0
  51. data/lib/generators/authenticate/routes/routes_generator.rb +32 -0
  52. data/lib/generators/authenticate/routes/templates/routes.rb +10 -0
  53. data/lib/generators/authenticate/views/USAGE +13 -0
  54. data/lib/generators/authenticate/views/views_generator.rb +21 -0
  55. data/spec/dummy/app/controllers/application_controller.rb +1 -0
  56. data/spec/dummy/config/initializers/authenticate.rb +12 -5
  57. data/spec/dummy/db/development.sqlite3 +0 -0
  58. data/spec/dummy/db/migrate/20160130192728_create_users.rb +18 -0
  59. data/spec/dummy/db/migrate/20160130192729_add_authenticate_brute_force_to_users.rb +6 -0
  60. data/spec/dummy/db/migrate/20160130192730_add_authenticate_timeoutable_to_users.rb +5 -0
  61. data/spec/dummy/db/migrate/20160130192731_add_authenticate_password_reset_to_users.rb +7 -0
  62. data/spec/dummy/db/schema.rb +14 -10
  63. data/spec/dummy/db/test.sqlite3 +0 -0
  64. data/spec/factories/users.rb +5 -8
  65. data/spec/model/brute_force_spec.rb +63 -0
  66. data/spec/model/session_spec.rb +4 -0
  67. data/spec/model/user_spec.rb +15 -5
  68. data/spec/spec_helper.rb +2 -1
  69. metadata +41 -9
  70. data/app/controllers/.keep +0 -0
  71. data/app/mailers/.keep +0 -0
  72. data/app/views/.keep +0 -0
  73. 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
- private
93
-
94
- def authenticate_session
95
- @authenticate_session ||= Authenticate::Session.new(request, cookies)
96
- end
92
+ protected
97
93
 
98
- def unauthorized(msg = 'You must sign in')
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
- flash[:notice] = msg # TODO use locales
103
- redirect_to '/authenticate' #TODO something better baby
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
@@ -3,6 +3,7 @@ module Authenticate
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  def d(msg)
6
+ # todo check: Rails constant loaded? Authenticate config read? do a puts otherwise
6
7
  Rails.logger.info msg.to_s if Authenticate.configuration.debug
7
8
  end
8
9
 
@@ -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
- # viget
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
- # Lock accounts that have too many failed consecutive logins.
9
- # Todo: email user to allow faster unlocking via token.
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 signing in.
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
- # - password_match?(password) - checks to see if the user's password matches the given password
15
- # - password=(new_password) - encrypt and set the user password
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
- # - :password validation, requiring the password is set
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 this is a pluggable point
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. Must be unique to the system.
6
+ # Use :email as the identifier for the user. Email must be unique.
7
7
  #
8
8
  # = Columns
9
- # - :email containing the email address of the user
9
+ # * email - the email address of the user
10
10
  #
11
11
  # = Validations
12
- # - :email requires email is set, validations the format, and ensure it is unique
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
- # - :email - require the email address is set and is a valid format
17
+ # * normalize_email - normalize the email, removing spaces etc, before saving
19
18
  #
20
19
  # = class methods
21
- # - authenticate(email, password) - find user with given email, validate their password, return the user.
22
- # - normalize_email(email) - clean up the given email and return it.
23
- # - find_by_normalized_email(email) - normalize the given email, then look for the user with that email.
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
- # == Columns
9
+ # = Columns
10
10
  #
11
11
  # This module expects and tracks this column on your user model:
12
- # - last_access_at - timestamp of the last access by the user
12
+ # * last_access_at - datetime of the last access by the user
13
13
  #
14
- # == Configuration
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 made
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 normalize_username(username)
43
+ find_by_username username
31
44
  end
32
45
 
33
- def normalize_username(username)
34
- username.to_s.downcase.gsub(/\s+/, '')
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
- self.username = self.class.normalize_username(username)
40
- end
51
+ # def normalize_username
52
+ # self.username = self.class.normalize_username(username)
53
+ # end
41
54
 
42
55
  end
43
56
 
@@ -2,9 +2,27 @@ module Authenticate
2
2
  module Modules
3
3
  extend ActiveSupport::Concern
4
4
 
5
- # Methods to help your user model load Authenticate modules
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|