clearance 2.0.0.beta1 → 2.2.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of clearance might be problematic. Click here for more details.

Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +8 -14
  3. data/Appraisals +11 -3
  4. data/Gemfile +1 -3
  5. data/Gemfile.lock +91 -86
  6. data/NEWS.md +86 -4
  7. data/README.md +54 -28
  8. data/app/controllers/clearance/base_controller.rb +8 -1
  9. data/app/controllers/clearance/passwords_controller.rb +23 -5
  10. data/clearance.gemspec +15 -9
  11. data/config/locales/clearance.en.yml +1 -0
  12. data/config/routes.rb +1 -1
  13. data/gemfiles/rails_5.0.gemfile +3 -3
  14. data/gemfiles/rails_5.1.gemfile +3 -3
  15. data/gemfiles/rails_5.2.gemfile +3 -3
  16. data/gemfiles/{rails_4.2.gemfile → rails_6.0.gemfile} +5 -4
  17. data/lib/clearance/authentication.rb +1 -1
  18. data/lib/clearance/back_door.rb +1 -1
  19. data/lib/clearance/configuration.rb +30 -19
  20. data/lib/clearance/password_strategies.rb +5 -4
  21. data/lib/clearance/password_strategies/argon2.rb +23 -0
  22. data/lib/clearance/password_strategies/bcrypt.rb +17 -11
  23. data/lib/clearance/rack_session.rb +5 -1
  24. data/lib/clearance/session.rb +39 -3
  25. data/lib/clearance/testing/deny_access_matcher.rb +1 -5
  26. data/lib/clearance/user.rb +12 -3
  27. data/lib/clearance/version.rb +1 -1
  28. data/lib/generators/clearance/install/install_generator.rb +11 -7
  29. data/lib/generators/clearance/install/templates/README +10 -4
  30. data/lib/generators/clearance/install/templates/db/migrate/add_clearance_to_users.rb.erb +1 -1
  31. data/lib/generators/clearance/install/templates/db/migrate/create_users.rb.erb +1 -1
  32. data/lib/generators/clearance/routes/templates/routes.rb +1 -1
  33. data/spec/acceptance/clearance_installation_spec.rb +0 -4
  34. data/spec/app_templates/app/models/user.rb +1 -1
  35. data/spec/app_templates/testapp/app/controllers/home_controller.rb +1 -5
  36. data/spec/app_templates/testapp/app/views/layouts/application.html.erb +24 -0
  37. data/spec/clearance/back_door_spec.rb +12 -6
  38. data/spec/clearance/rack_session_spec.rb +2 -0
  39. data/spec/clearance/session_spec.rb +91 -16
  40. data/spec/clearance/testing/deny_access_matcher_spec.rb +32 -0
  41. data/spec/configuration_spec.rb +46 -15
  42. data/spec/controllers/passwords_controller_spec.rb +36 -0
  43. data/spec/controllers/permissions_controller_spec.rb +1 -1
  44. data/spec/dummy/app/controllers/application_controller.rb +1 -5
  45. data/spec/dummy/application.rb +7 -1
  46. data/spec/generators/clearance/install/install_generator_spec.rb +31 -6
  47. data/spec/generators/clearance/views/views_generator_spec.rb +0 -2
  48. data/spec/models/user_spec.rb +34 -5
  49. data/spec/password_strategies/argon2_spec.rb +79 -0
  50. data/spec/password_strategies/bcrypt_spec.rb +18 -1
  51. data/spec/requests/authentication_cookie_spec.rb +55 -0
  52. data/spec/requests/token_expiration_spec.rb +5 -0
  53. data/spec/spec_helper.rb +4 -7
  54. data/spec/support/generator_spec_helpers.rb +1 -9
  55. metadata +52 -26
  56. data/app/views/layouts/application.html.erb +0 -23
  57. data/spec/app_templates/app/models/rails5/user.rb +0 -5
  58. data/spec/support/environment.rb +0 -12
  59. data/spec/support/http_method_shim.rb +0 -25
@@ -1,2 +1,9 @@
1
- class Clearance::BaseController < ApplicationController
1
+ module Clearance
2
+ # Top-level base class that all Clearance controllers inherit from.
3
+ # Inherits from `ApplicationController` by default and can be overridden by
4
+ # setting a new value with {Configuration#parent_controller=}.
5
+ # @!parse
6
+ # class BaseController < ApplicationController; end
7
+ class BaseController < Clearance.configuration.parent_controller
8
+ end
2
9
  end
@@ -2,6 +2,7 @@ require 'active_support/deprecation'
2
2
 
3
3
  class Clearance::PasswordsController < Clearance::BaseController
4
4
  before_action :ensure_existing_user, only: [:edit, :update]
5
+ before_action :ensure_email_present, only: [:create]
5
6
  skip_before_action :require_login, only: [:create, :edit, :new, :update], raise: false
6
7
 
7
8
  def new
@@ -31,7 +32,7 @@ class Clearance::PasswordsController < Clearance::BaseController
31
32
  def update
32
33
  @user = find_user_for_update
33
34
 
34
- if @user.update_password password_reset_params
35
+ if @user.update_password(password_from_password_reset_params)
35
36
  sign_in @user
36
37
  redirect_to url_after_update
37
38
  session[:password_reset_token] = nil
@@ -48,8 +49,8 @@ class Clearance::PasswordsController < Clearance::BaseController
48
49
  mail.deliver_later
49
50
  end
50
51
 
51
- def password_reset_params
52
- params[:password_reset][:password]
52
+ def password_from_password_reset_params
53
+ params.dig(:password_reset, :password)
53
54
  end
54
55
 
55
56
  def find_user_by_id_and_confirmation_token
@@ -57,12 +58,16 @@ class Clearance::PasswordsController < Clearance::BaseController
57
58
  token = params[:token] || session[:password_reset_token]
58
59
 
59
60
  Clearance.configuration.user_model.
60
- find_by_id_and_confirmation_token params[user_param], token.to_s
61
+ find_by(id: params[user_param], confirmation_token: token.to_s)
62
+ end
63
+
64
+ def email_from_password_params
65
+ params.dig(:password, :email)
61
66
  end
62
67
 
63
68
  def find_user_for_create
64
69
  Clearance.configuration.user_model.
65
- find_by_normalized_email params[:password][:email]
70
+ find_by_normalized_email(email_from_password_params)
66
71
  end
67
72
 
68
73
  def find_user_for_edit
@@ -73,6 +78,13 @@ class Clearance::PasswordsController < Clearance::BaseController
73
78
  find_user_by_id_and_confirmation_token
74
79
  end
75
80
 
81
+ def ensure_email_present
82
+ if email_from_password_params.blank?
83
+ flash_failure_when_missing_email
84
+ render template: "passwords/new"
85
+ end
86
+ end
87
+
76
88
  def ensure_existing_user
77
89
  unless find_user_by_id_and_confirmation_token
78
90
  flash_failure_when_forbidden
@@ -92,6 +104,12 @@ class Clearance::PasswordsController < Clearance::BaseController
92
104
  default: t("flashes.failure_after_update"))
93
105
  end
94
106
 
107
+ def flash_failure_when_missing_email
108
+ flash.now[:alert] = translate(:missing_email,
109
+ scope: [:clearance, :controllers, :passwords],
110
+ default: t("flashes.failure_when_missing_email"))
111
+ end
112
+
95
113
  def url_after_update
96
114
  Clearance.configuration.redirect_url
97
115
  end
@@ -1,14 +1,14 @@
1
1
  $LOAD_PATH.push File.expand_path('../lib', __FILE__)
2
2
  require 'clearance/version'
3
- require 'date'
4
3
 
5
4
  Gem::Specification.new do |s|
6
- s.add_dependency 'bcrypt'
7
- s.add_dependency 'email_validator', '~> 1.4'
8
- s.add_dependency 'railties', '>= 4.2'
9
- s.add_dependency 'activemodel', '>= 4.2'
10
- s.add_dependency 'activerecord', '>= 4.2'
11
- s.add_dependency 'actionmailer', '>= 4.2'
5
+ s.add_dependency 'bcrypt', '>= 3.1.1'
6
+ s.add_dependency 'argon2', '~> 2.0', '>= 2.0.2'
7
+ s.add_dependency 'email_validator', '~> 2.0'
8
+ s.add_dependency 'railties', '>= 5.0'
9
+ s.add_dependency 'activemodel', '>= 5.0'
10
+ s.add_dependency 'activerecord', '>= 5.0'
11
+ s.add_dependency 'actionmailer', '>= 5.0'
12
12
  s.authors = [
13
13
  'Dan Croak',
14
14
  'Eugene Bolshakov',
@@ -29,7 +29,13 @@ Gem::Specification.new do |s|
29
29
  'Galen Frechette',
30
30
  'Josh Steiner'
31
31
  ]
32
- s.description = 'Rails authentication & authorization with email & password.'
32
+ s.description = <<-DESCRIPTION
33
+ Clearance is built to support authentication and authorization via an
34
+ email/password sign-in mechanism in applications.
35
+
36
+ It provides some core classes commonly used for these features, along with
37
+ some opinionated defaults - but is intended to be easy to override.
38
+ DESCRIPTION
33
39
  s.email = 'support@thoughtbot.com'
34
40
  s.extra_rdoc_files = %w(LICENSE README.md)
35
41
  s.files = `git ls-files`.split("\n")
@@ -38,7 +44,7 @@ Gem::Specification.new do |s|
38
44
  s.name = %q{clearance}
39
45
  s.rdoc_options = ['--charset=UTF-8']
40
46
  s.require_paths = ['lib']
41
- s.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
47
+ s.required_ruby_version = Gem::Requirement.new('>= 2.4.0')
42
48
  s.summary = 'Rails authentication & authorization with email & password.'
43
49
  s.test_files = `git ls-files -- {spec}/*`.split("\n")
44
50
  s.version = Clearance::VERSION
@@ -17,6 +17,7 @@ en:
17
17
  failure_when_forbidden: Please double check the URL or try submitting
18
18
  the form again.
19
19
  failure_when_not_signed_in: Please sign in to continue.
20
+ failure_when_missing_email: Email can't be blank.
20
21
  helpers:
21
22
  label:
22
23
  password:
@@ -13,7 +13,7 @@ if Clearance.configuration.routes_enabled?
13
13
  only: Clearance.configuration.user_actions do
14
14
  resource :password,
15
15
  controller: 'clearance/passwords',
16
- only: [:create, :edit, :update]
16
+ only: [:edit, :update]
17
17
  end
18
18
 
19
19
  get '/sign_in' => 'clearance/sessions#new', as: 'sign_in'
@@ -10,11 +10,11 @@ gem "database_cleaner", "~> 1.0"
10
10
  gem "factory_bot_rails", "~> 5.0"
11
11
  gem "nokogiri", "~> 1.10.0"
12
12
  gem "pry", require: false
13
- gem "rspec-rails", "~> 3.5"
14
- gem "shoulda-matchers", "~> 4.0"
15
- gem "sqlite3", "~> 1.3.13"
13
+ gem "shoulda-matchers", "~> 4.1"
16
14
  gem "timecop", "~> 0.6"
17
15
  gem "railties", "~> 5.0.0"
18
16
  gem "rails-controller-testing"
17
+ gem "sqlite3", "~> 1.3.13"
18
+ gem "rspec-rails", "~> 3.1"
19
19
 
20
20
  gemspec path: "../"
@@ -10,11 +10,11 @@ gem "database_cleaner", "~> 1.0"
10
10
  gem "factory_bot_rails", "~> 5.0"
11
11
  gem "nokogiri", "~> 1.10.0"
12
12
  gem "pry", require: false
13
- gem "rspec-rails", "~> 3.5"
14
- gem "shoulda-matchers", "~> 4.0"
15
- gem "sqlite3", "~> 1.3.13"
13
+ gem "shoulda-matchers", "~> 4.1"
16
14
  gem "timecop", "~> 0.6"
17
15
  gem "railties", "~> 5.1.0"
18
16
  gem "rails-controller-testing"
17
+ gem "sqlite3", "~> 1.3.13"
18
+ gem "rspec-rails", "~> 3.1"
19
19
 
20
20
  gemspec path: "../"
@@ -10,11 +10,11 @@ gem "database_cleaner", "~> 1.0"
10
10
  gem "factory_bot_rails", "~> 5.0"
11
11
  gem "nokogiri", "~> 1.10.0"
12
12
  gem "pry", require: false
13
- gem "rspec-rails", "~> 3.5"
14
- gem "shoulda-matchers", "~> 4.0"
15
- gem "sqlite3", "~> 1.3.13"
13
+ gem "shoulda-matchers", "~> 4.1"
16
14
  gem "timecop", "~> 0.6"
17
15
  gem "railties", "~> 5.2.0"
18
16
  gem "rails-controller-testing"
17
+ gem "sqlite3", "~> 1.3.13"
18
+ gem "rspec-rails", "~> 3.1"
19
19
 
20
20
  gemspec path: "../"
@@ -10,10 +10,11 @@ gem "database_cleaner", "~> 1.0"
10
10
  gem "factory_bot_rails", "~> 5.0"
11
11
  gem "nokogiri", "~> 1.10.0"
12
12
  gem "pry", require: false
13
- gem "rspec-rails", "~> 3.5"
14
- gem "shoulda-matchers", "~> 4.0"
15
- gem "sqlite3", "~> 1.3.13"
13
+ gem "shoulda-matchers", "~> 4.1"
16
14
  gem "timecop", "~> 0.6"
17
- gem "railties", "~> 4.2.0"
15
+ gem "railties", "~> 6.0.0"
16
+ gem "rails-controller-testing"
17
+ gem "rspec-rails", "~> 4.0.0.beta3"
18
+ gem "sqlite3", "~> 1.4.0"
18
19
 
19
20
  gemspec path: "../"
@@ -59,7 +59,7 @@ module Clearance
59
59
  # {SessionsController#create}.
60
60
  #
61
61
  # Signing in will also regenerate the CSRF token for the current session,
62
- # provided {Configuration#rotate_csrf_token_on_sign_in} is set.
62
+ # provided {Configuration#rotate_csrf_on_sign_in?} is set.
63
63
  def sign_in(user, &block)
64
64
  clearance_session.sign_in(user, &block)
65
65
 
@@ -68,7 +68,7 @@ module Clearance
68
68
 
69
69
  # @api private
70
70
  def environment_is_allowed?
71
- allowed_environments.include? ENV["RAILS_ENV"]
71
+ allowed_environments.include? Rails.env
72
72
  end
73
73
 
74
74
  # @api private
@@ -42,6 +42,16 @@ module Clearance
42
42
  # @return [Boolean]
43
43
  attr_accessor :httponly
44
44
 
45
+ # Same-site cookies ("First-Party-Only" or "First-Party") allow servers to
46
+ # mitigate the risk of CSRF and information leakage attacks by asserting
47
+ # that a particular cookie should only be sent with requests initiated from
48
+ # the same registrable domain.
49
+ # Defaults to `nil`. For more, see
50
+ # [RFC6265](https://tools.ietf.org/html/draft-west-first-party-cookies-06#section-4.1.1).
51
+ # and https://github.com/rack/rack/blob/6eda04886e3a57918ca2d6a482fda02a678fef0a/lib/rack/utils.rb#L232-L244
52
+ # @return [String]
53
+ attr_accessor :same_site
54
+
45
55
  # Controls the address the password reset email is sent from.
46
56
  # Defaults to reply@example.com.
47
57
  # @return [String]
@@ -88,7 +98,12 @@ module Clearance
88
98
  # The ActiveRecord class that represents users in your application.
89
99
  # Defaults to `::User`.
90
100
  # @return [ActiveRecord::Base]
91
- attr_accessor :user_model
101
+ attr_writer :user_model
102
+
103
+ # The controller class that all Clearance controllers will inherit from.
104
+ # Defaults to `::ApplicationController`.
105
+ # @return [ActionController::Base]
106
+ attr_writer :parent_controller
92
107
 
93
108
  # The array of allowed environments where `Clearance::BackDoor` is enabled.
94
109
  # Defaults to ["test", "ci", "development"]
@@ -103,16 +118,27 @@ module Clearance
103
118
  @cookie_name = "remember_token"
104
119
  @cookie_path = '/'
105
120
  @httponly = true
121
+ @same_site = nil
106
122
  @mailer_sender = 'reply@example.com'
107
123
  @redirect_url = '/'
108
- @rotate_csrf_on_sign_in = nil
124
+ @rotate_csrf_on_sign_in = true
109
125
  @routes = true
110
126
  @secure_cookie = false
111
127
  @sign_in_guards = []
112
128
  end
113
129
 
130
+ # The class representing the configured user model.
131
+ # In the default configuration, this is the `User` class.
132
+ # @return [Class]
114
133
  def user_model
115
- @user_model ||= ::User
134
+ (@user_model || "User").to_s.constantize
135
+ end
136
+
137
+ # The class representing the configured base controller.
138
+ # In the default configuration, this is the `ApplicationController` class.
139
+ # @return [Class]
140
+ def parent_controller
141
+ (@parent_controller || "ApplicationController").to_s.constantize
116
142
  end
117
143
 
118
144
  # Is the user sign up route enabled?
@@ -167,22 +193,7 @@ module Clearance
167
193
  end
168
194
 
169
195
  def rotate_csrf_on_sign_in?
170
- if rotate_csrf_on_sign_in.nil?
171
- warn <<-EOM.squish
172
- Clearance's `rotate_csrf_on_sign_in` configuration setting is unset and
173
- will be treated as `false`. Setting this value to `true` is
174
- recommended to avoid session fixation attacks and will be the default
175
- in Clearance 2.0. It is recommended that you opt-in to this setting
176
- now and test your application. To silence this warning, set
177
- `rotate_csrf_on_sign_in` to `true` or `false` in Clearance's
178
- initializer.
179
-
180
- For more information on session fixation, see:
181
- https://www.owasp.org/index.php/Session_fixation
182
- EOM
183
- end
184
-
185
- rotate_csrf_on_sign_in
196
+ !!rotate_csrf_on_sign_in
186
197
  end
187
198
  end
188
199
 
@@ -13,10 +13,11 @@ module Clearance
13
13
  # `password=(new_password)`. For an example of how to implement these methods,
14
14
  # see {Clearance::PasswordStrategies::BCrypt}.
15
15
  module PasswordStrategies
16
- autoload :BCrypt, 'clearance/password_strategies/bcrypt'
16
+ autoload :BCrypt, "clearance/password_strategies/bcrypt"
17
+ autoload :Argon2, "clearance/password_strategies/argon2"
17
18
  autoload :BCryptMigrationFromSHA1,
18
- 'clearance/password_strategies/bcrypt_migration_from_sha1'
19
- autoload :Blowfish, 'clearance/password_strategies/blowfish'
20
- autoload :SHA1, 'clearance/password_strategies/sha1'
19
+ "clearance/password_strategies/bcrypt_migration_from_sha1"
20
+ autoload :Blowfish, "clearance/password_strategies/blowfish"
21
+ autoload :SHA1, "clearance/password_strategies/sha1"
21
22
  end
22
23
  end
@@ -0,0 +1,23 @@
1
+ module Clearance
2
+ module PasswordStrategies
3
+ # Uses Argon2 to authenticate users and store encrypted passwords.
4
+
5
+ module Argon2
6
+ require "argon2"
7
+
8
+ def authenticated?(password)
9
+ if encrypted_password.present?
10
+ ::Argon2::Password.verify_password(password, encrypted_password)
11
+ end
12
+ end
13
+
14
+ def password=(new_password)
15
+ @password = new_password
16
+
17
+ if new_password.present?
18
+ self.encrypted_password = ::Argon2::Password.new.create(new_password)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -2,10 +2,14 @@ module Clearance
2
2
  module PasswordStrategies
3
3
  # Uses BCrypt to authenticate users and store encrypted passwords.
4
4
  #
5
- # The BCrypt cost (the measure of how many key expansion iterations BCrypt
6
- # will perform) is automatically set to the minimum allowed value when
7
- # Rails is operating in the test environment and the default cost in all
8
- # other envionments. This provides a speed boost in tests.
5
+ # BCrypt has a `cost` argument which determines how computationally
6
+ # expensive the hash is to calculate. The higher the cost, the harder it is
7
+ # for attackers to crack passwords even if they posess a database dump of
8
+ # the encrypted passwords. Clearance uses the `bcrypt-ruby` default cost
9
+ # except in the test environment, where it uses the minimum cost value for
10
+ # speed. If you wish to increase the cost over the default, you can do so
11
+ # by setting a higher cost in an initializer:
12
+ # `BCrypt::Engine.cost = 12`
9
13
  module BCrypt
10
14
  require 'bcrypt'
11
15
 
@@ -19,18 +23,20 @@ module Clearance
19
23
  @password = new_password
20
24
 
21
25
  if new_password.present?
22
- cost = if defined?(::Rails) && ::Rails.env.test?
23
- ::BCrypt::Engine::MIN_COST
24
- else
25
- ::BCrypt::Engine::DEFAULT_COST
26
- end
27
-
28
26
  self.encrypted_password = ::BCrypt::Password.create(
29
27
  new_password,
30
- cost: cost,
28
+ cost: configured_bcrypt_cost,
31
29
  )
32
30
  end
33
31
  end
32
+
33
+ def configured_bcrypt_cost
34
+ if defined?(::Rails) && ::Rails.env.test?
35
+ ::BCrypt::Engine::MIN_COST
36
+ else
37
+ ::BCrypt::Engine.cost
38
+ end
39
+ end
34
40
  end
35
41
  end
36
42
  end
@@ -21,7 +21,11 @@ module Clearance
21
21
  session = Clearance::Session.new(env)
22
22
  env[:clearance] = session
23
23
  response = @app.call(env)
24
- session.add_cookie_to_headers response[1]
24
+
25
+ if session.authentication_successful?
26
+ session.add_cookie_to_headers response[1]
27
+ end
28
+
25
29
  response
26
30
  end
27
31
  end
@@ -81,7 +81,7 @@ module Clearance
81
81
  end
82
82
 
83
83
  @current_user = nil
84
- cookies.delete remember_token_cookie
84
+ cookies.delete remember_token_cookie, delete_cookie_options
85
85
  end
86
86
 
87
87
  # True if {#current_user} is set.
@@ -98,6 +98,13 @@ module Clearance
98
98
  ! signed_in?
99
99
  end
100
100
 
101
+ # True if a successful authentication has been performed
102
+ #
103
+ # @return [Boolean]
104
+ def authentication_successful?
105
+ !!@current_user
106
+ end
107
+
101
108
  private
102
109
 
103
110
  # @api private
@@ -147,20 +154,49 @@ module Clearance
147
154
  guards = Clearance.configuration.sign_in_guards
148
155
 
149
156
  guards.inject(default_guard) do |stack, guard_class|
150
- guard_class.new(self, stack)
157
+ guard_class.to_s.constantize.new(self, stack)
151
158
  end
152
159
  end
153
160
 
154
161
  # @api private
155
162
  def cookie_options
156
163
  {
157
- domain: Clearance.configuration.cookie_domain,
164
+ domain: domain,
158
165
  expires: remember_token_expires,
159
166
  httponly: Clearance.configuration.httponly,
167
+ same_site: Clearance.configuration.same_site,
160
168
  path: Clearance.configuration.cookie_path,
161
169
  secure: Clearance.configuration.secure_cookie,
162
170
  value: remember_token,
163
171
  }
164
172
  end
173
+
174
+ # @api private
175
+ def delete_cookie_options
176
+ Hash.new.tap do |options|
177
+ if configured_cookie_domain
178
+ options[:domain] = configured_cookie_domain
179
+ end
180
+ end
181
+ end
182
+
183
+ # @api private
184
+ def domain
185
+ if configured_cookie_domain.respond_to?(:call)
186
+ configured_cookie_domain.call(request_with_env)
187
+ else
188
+ configured_cookie_domain
189
+ end
190
+ end
191
+
192
+ # @api private
193
+ def configured_cookie_domain
194
+ Clearance.configuration.cookie_domain
195
+ end
196
+
197
+ # @api private
198
+ def request_with_env
199
+ ActionDispatch::Request.new(@env)
200
+ end
165
201
  end
166
202
  end