authenticate 0.1.0 → 0.2.0

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