clearance 2.2.1 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.erb-lint.yml +5 -0
  3. data/.github/workflows/tests.yml +52 -0
  4. data/Appraisals +14 -19
  5. data/Gemfile +11 -7
  6. data/Gemfile.lock +112 -65
  7. data/NEWS.md +48 -0
  8. data/README.md +25 -14
  9. data/RELEASING.md +25 -0
  10. data/Rakefile +6 -1
  11. data/app/controllers/clearance/passwords_controller.rb +1 -2
  12. data/app/views/clearance_mailer/change_password.html.erb +2 -2
  13. data/app/views/clearance_mailer/change_password.text.erb +2 -2
  14. data/app/views/passwords/edit.html.erb +1 -1
  15. data/gemfiles/rails_5.0.gemfile +10 -9
  16. data/gemfiles/rails_5.1.gemfile +11 -10
  17. data/gemfiles/rails_5.2.gemfile +11 -10
  18. data/gemfiles/rails_6.0.gemfile +11 -10
  19. data/gemfiles/rails_6.1.gemfile +21 -0
  20. data/lib/clearance/authorization.rb +7 -1
  21. data/lib/clearance/back_door.rb +2 -1
  22. data/lib/clearance/configuration.rb +19 -0
  23. data/lib/clearance/password_strategies.rb +0 -4
  24. data/lib/clearance/rack_session.rb +1 -1
  25. data/lib/clearance/session.rb +24 -12
  26. data/lib/clearance/user.rb +1 -1
  27. data/lib/clearance/version.rb +1 -1
  28. data/lib/generators/clearance/install/install_generator.rb +4 -1
  29. data/lib/generators/clearance/install/templates/db/migrate/add_clearance_to_users.rb.erb +5 -1
  30. data/spec/clearance/back_door_spec.rb +20 -4
  31. data/spec/clearance/rack_session_spec.rb +1 -2
  32. data/spec/clearance/session_spec.rb +116 -43
  33. data/spec/configuration_spec.rb +28 -0
  34. data/spec/controllers/sessions_controller_spec.rb +13 -0
  35. data/spec/generators/clearance/install/install_generator_spec.rb +8 -2
  36. data/spec/mailers/clearance_mailer_spec.rb +33 -0
  37. data/spec/models/user_spec.rb +2 -2
  38. data/spec/support/clearance.rb +11 -0
  39. data/spec/support/request_with_remember_token.rb +8 -6
  40. metadata +7 -4
  41. data/.travis.yml +0 -28
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Clearance
2
2
 
3
- [![Build Status](https://secure.travis-ci.org/thoughtbot/clearance.svg)](http://travis-ci.org/thoughtbot/clearance?branch=master)
3
+ [![Build Status](https://github.com/thoughtbot/clearance/actions/workflows/tests.yml/badge.svg)]( https://github.com/thoughtbot/clearance/actions/workflows/tests.yml?query=branch%3Amain)
4
4
  [![Code Climate](https://codeclimate.com/github/thoughtbot/clearance.svg)](https://codeclimate.com/github/thoughtbot/clearance)
5
- [![Documentation Quality](https://inch-ci.org/github/thoughtbot/clearance.svg?branch=master)](https://inch-ci.org/github/thoughtbot/clearance)
5
+ [![Documentation Quality](https://inch-ci.org/github/thoughtbot/clearance.svg?branch=main)](https://inch-ci.org/github/thoughtbot/clearance)
6
6
  [![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](https://houndci.com)
7
7
 
8
8
  Rails authentication with email & password.
@@ -55,12 +55,14 @@ Clearance.configure do |config|
55
55
  config.cookie_name = "remember_token"
56
56
  config.cookie_path = "/"
57
57
  config.routes = true
58
- config.httponly = false
58
+ config.httponly = true
59
59
  config.mailer_sender = "reply@example.com"
60
60
  config.password_strategy = Clearance::PasswordStrategies::BCrypt
61
61
  config.redirect_url = "/"
62
62
  config.rotate_csrf_on_sign_in = true
63
+ config.same_site = nil
63
64
  config.secure_cookie = false
65
+ config.signed_cookie = false
64
66
  config.sign_in_guards = []
65
67
  config.user_model = "User"
66
68
  config.parent_controller = "ApplicationController"
@@ -285,24 +287,33 @@ and `password` attributes. Over-riding the `email_optional?` or
285
287
  `skip_password_validation?` methods to return `true` will disable those
286
288
  validations from being added.
287
289
 
288
- ### Deliver Email in Background Job
290
+ ### Signed Cookies
289
291
 
290
- Clearance has a password reset mailer. If you are using Rails 4.2 and Clearance
291
- 1.6 or greater, Clearance will use ActiveJob's `deliver_later` method to
292
- automatically take advantage of your configured queue.
292
+ By default, Clearance uses unsigned cookies. If you would like to use signed
293
+ cookies you can do so by overriding the default in an initializer like so:
293
294
 
294
- If you are using an earlier version of Rails, you can override the
295
- `Clearance::Passwords` controller and define the behavior you need in the
296
- `deliver_email` method.
295
+ ```ruby
296
+ Clearance.configure do |config|
297
+ # ... other overrides
298
+ config.signed_cookie = true
299
+ end
300
+ ```
301
+
302
+ If you are currently not using signed cookies but would like to migrate your
303
+ users over to them without breaking current sessions, you can do so by passing
304
+ in `:migrate` rather than `true` as so:
297
305
 
298
306
  ```ruby
299
- class PasswordsController < Clearance::PasswordsController
300
- def deliver_email(user)
301
- ClearanceMailer.delay.change_password(user)
302
- end
307
+ Clearance.configure do |config|
308
+ # ... other overrides
309
+ config.signed_cookie = :migrate
303
310
  end
304
311
  ```
305
312
 
313
+ You can read more about signed cookies in Clearance and why they are a good idea
314
+ in the [pull request that added them](https://github.com/thoughtbot/clearance/pull/917).
315
+
316
+
306
317
  ## Extending Sign In
307
318
 
308
319
  By default, Clearance will sign in any user with valid credentials. If you need
data/RELEASING.md ADDED
@@ -0,0 +1,25 @@
1
+ # Releasing
2
+
3
+ 1. Update version file accordingly.
4
+ 1. Run `bundle install` to update Gemfile.lock
5
+ 1. Update `NEWS.md` to reflect the changes since last release.
6
+ 1. Commit changes.
7
+ There shouldn't be code changes,
8
+ and thus CI doesn't need to run,
9
+ you can then add "[ci skip]" to the commit message.
10
+ 1. Push the new commit
11
+ 1. Tag the release: `git tag -s vVERSION`
12
+ - We recommend the [_quick guide on how to sign a commit_] from GitHub.
13
+ 1. Push changes: `git push --tags`
14
+ 1. Build and publish:
15
+ ```bash
16
+ gem build clearance.gemspec
17
+ gem push clearance-*.gem
18
+ ```
19
+ 1. Add a new GitHub release using the recent `NEWS.md` as the content. Sample
20
+ URL: https://github.com/thoughtbot/clearance/releases/new?tag=vVERSION
21
+ 1. Announce the new release,
22
+ making sure to say "thank you" to the contributors
23
+ who helped shape this version!
24
+
25
+ [_quick guide on how to sign a commit_]: https://docs.github.com/en/github/authenticating-to-github/signing-commits
data/Rakefile CHANGED
@@ -22,5 +22,10 @@ RSpec::Core::RakeTask.new("spec:acceptance") do |task|
22
22
  task.verbose = false
23
23
  end
24
24
 
25
+ desc "Lint ERB templates"
26
+ task :erb_lint do
27
+ sh("bundle", "exec", "erblint", "app/views/**/*.erb")
28
+ end
29
+
25
30
  desc "Run the specs and acceptance tests"
26
- task default: %w(spec spec:acceptance)
31
+ task default: %w(spec spec:acceptance erb_lint)
@@ -45,8 +45,7 @@ class Clearance::PasswordsController < Clearance::BaseController
45
45
  private
46
46
 
47
47
  def deliver_email(user)
48
- mail = ::ClearanceMailer.change_password(user)
49
- mail.deliver_later
48
+ ::ClearanceMailer.change_password(user).deliver_now
50
49
  end
51
50
 
52
51
  def password_from_password_reset_params
@@ -2,7 +2,7 @@
2
2
 
3
3
  <p>
4
4
  <%= link_to t(".link_text", default: "Change my password"),
5
- edit_user_password_url(@user, token: @user.confirmation_token.html_safe) %>
5
+ url_for([@user, :password, action: :edit, token: @user.confirmation_token]) %>
6
6
  </p>
7
7
 
8
- <p><%= raw t(".closing") %></p>
8
+ <p><%= t(".closing") %></p>
@@ -1,5 +1,5 @@
1
1
  <%= t(".opening") %>
2
2
 
3
- <%= edit_user_password_url(@user, token: @user.confirmation_token.html_safe) %>
3
+ <%= url_for([@user, :password, action: :edit, token: @user.confirmation_token]) %>
4
4
 
5
- <%= raw t(".closing") %>
5
+ <%= t(".closing") %>
@@ -4,7 +4,7 @@
4
4
  <p><%= t(".description") %></p>
5
5
 
6
6
  <%= form_for :password_reset,
7
- url: user_password_path(@user, token: @user.confirmation_token),
7
+ url: [@user, :password, token: @user.confirmation_token],
8
8
  html: { method: :put } do |form| %>
9
9
  <div class="password-field">
10
10
  <%= form.label :password %>
@@ -2,19 +2,20 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "addressable", "~> 2.6.0"
5
+ gem "addressable"
6
6
  gem "ammeter"
7
7
  gem "appraisal"
8
- gem "capybara", ">= 2.6.2"
9
- gem "database_cleaner", "~> 1.0"
10
- gem "factory_bot_rails", "~> 5.0"
11
- gem "nokogiri", "~> 1.10.0"
8
+ gem "capybara", ">= 2.6.2", "< 3.33.0"
9
+ gem "database_cleaner"
10
+ gem "erb_lint", require: false
11
+ gem "factory_bot_rails"
12
+ gem "nokogiri"
12
13
  gem "pry", require: false
13
- gem "shoulda-matchers", "~> 4.1"
14
- gem "timecop", "~> 0.6"
15
- gem "railties", "~> 5.0.0"
16
14
  gem "rails-controller-testing"
17
- gem "sqlite3", "~> 1.3.13"
18
15
  gem "rspec-rails", "~> 3.1"
16
+ gem "shoulda-matchers"
17
+ gem "sqlite3", "~> 1.3.13"
18
+ gem "timecop"
19
+ gem "railties", "~> 5.0"
19
20
 
20
21
  gemspec path: "../"
@@ -2,19 +2,20 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "addressable", "~> 2.6.0"
5
+ gem "addressable"
6
6
  gem "ammeter"
7
7
  gem "appraisal"
8
- gem "capybara", ">= 2.6.2"
9
- gem "database_cleaner", "~> 1.0"
10
- gem "factory_bot_rails", "~> 5.0"
11
- gem "nokogiri", "~> 1.10.0"
8
+ gem "capybara"
9
+ gem "database_cleaner"
10
+ gem "erb_lint", require: false
11
+ gem "factory_bot_rails"
12
+ gem "nokogiri"
12
13
  gem "pry", require: false
13
- gem "shoulda-matchers", "~> 4.1"
14
- gem "timecop", "~> 0.6"
15
- gem "railties", "~> 5.1.0"
16
14
  gem "rails-controller-testing"
17
- gem "sqlite3", "~> 1.3.13"
18
- gem "rspec-rails", "~> 3.1"
15
+ gem "rspec-rails"
16
+ gem "shoulda-matchers"
17
+ gem "sqlite3"
18
+ gem "timecop"
19
+ gem "railties", "~> 5.1"
19
20
 
20
21
  gemspec path: "../"
@@ -2,19 +2,20 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "addressable", "~> 2.6.0"
5
+ gem "addressable"
6
6
  gem "ammeter"
7
7
  gem "appraisal"
8
- gem "capybara", ">= 2.6.2"
9
- gem "database_cleaner", "~> 1.0"
10
- gem "factory_bot_rails", "~> 5.0"
11
- gem "nokogiri", "~> 1.10.0"
8
+ gem "capybara"
9
+ gem "database_cleaner"
10
+ gem "erb_lint", require: false
11
+ gem "factory_bot_rails"
12
+ gem "nokogiri"
12
13
  gem "pry", require: false
13
- gem "shoulda-matchers", "~> 4.1"
14
- gem "timecop", "~> 0.6"
15
- gem "railties", "~> 5.2.0"
16
14
  gem "rails-controller-testing"
17
- gem "sqlite3", "~> 1.3.13"
18
- gem "rspec-rails", "~> 3.1"
15
+ gem "rspec-rails"
16
+ gem "shoulda-matchers"
17
+ gem "sqlite3"
18
+ gem "timecop"
19
+ gem "railties", "~> 5.2"
19
20
 
20
21
  gemspec path: "../"
@@ -2,19 +2,20 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "addressable", "~> 2.6.0"
5
+ gem "addressable"
6
6
  gem "ammeter"
7
7
  gem "appraisal"
8
- gem "capybara", ">= 2.6.2"
9
- gem "database_cleaner", "~> 1.0"
10
- gem "factory_bot_rails", "~> 5.0"
11
- gem "nokogiri", "~> 1.10.0"
8
+ gem "capybara"
9
+ gem "database_cleaner"
10
+ gem "erb_lint", require: false
11
+ gem "factory_bot_rails"
12
+ gem "nokogiri"
12
13
  gem "pry", require: false
13
- gem "shoulda-matchers", "~> 4.1"
14
- gem "timecop", "~> 0.6"
15
- gem "railties", "~> 6.0.0"
16
14
  gem "rails-controller-testing"
17
- gem "rspec-rails", "~> 4.0.0.beta3"
18
- gem "sqlite3", "~> 1.4.0"
15
+ gem "rspec-rails"
16
+ gem "shoulda-matchers"
17
+ gem "sqlite3"
18
+ gem "timecop"
19
+ gem "railties", "~> 6.0"
19
20
 
20
21
  gemspec path: "../"
@@ -0,0 +1,21 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "addressable"
6
+ gem "ammeter"
7
+ gem "appraisal"
8
+ gem "capybara"
9
+ gem "database_cleaner"
10
+ gem "erb_lint", require: false
11
+ gem "factory_bot_rails"
12
+ gem "nokogiri"
13
+ gem "pry", require: false
14
+ gem "rails-controller-testing"
15
+ gem "rspec-rails"
16
+ gem "shoulda-matchers"
17
+ gem "sqlite3"
18
+ gem "timecop"
19
+ gem "railties", "~> 6.1"
20
+
21
+ gemspec path: "../"
@@ -86,10 +86,16 @@ module Clearance
86
86
  def return_to
87
87
  if return_to_url
88
88
  uri = URI.parse(return_to_url)
89
- "#{uri.path}?#{uri.query}".chomp("?") + "##{uri.fragment}".chomp("#")
89
+ path = path_without_leading_slashes(uri)
90
+ "#{path}?#{uri.query}".chomp("?") + "##{uri.fragment}".chomp("#")
90
91
  end
91
92
  end
92
93
 
94
+ # @api private
95
+ def path_without_leading_slashes(uri)
96
+ uri.path.sub(/\A\/+/, "/")
97
+ end
98
+
93
99
  # @api private
94
100
  def return_to_url
95
101
  session[:return_to]
@@ -49,7 +49,8 @@ module Clearance
49
49
  # @api private
50
50
  def sign_in_through_the_back_door(env)
51
51
  params = Rack::Utils.parse_query(env["QUERY_STRING"])
52
- user_param = params["as"]
52
+ user_param = params.delete("as")
53
+ env["QUERY_STRING"] = Rack::Utils.build_query(params)
53
54
 
54
55
  if user_param.present?
55
56
  user = find_user(user_param)
@@ -88,6 +88,14 @@ module Clearance
88
88
  # @return [Boolean]
89
89
  attr_accessor :secure_cookie
90
90
 
91
+ # Controls whether cookies are signed.
92
+ # Defaults to `false` for backwards compatibility.
93
+ # When set, uses Rails' signed cookies
94
+ # (more secure against timing/brute-force attacks)
95
+ # see [ActionDispatch::Cookies](https://api.rubyonrails.org/classes/ActionDispatch/Cookies.html)
96
+ # @return [Boolean|:migrate]
97
+ attr_reader :signed_cookie
98
+
91
99
  # The array of sign in guards to run when signing a user in.
92
100
  # Defaults to an empty array. Sign in guards respond to `call` and are
93
101
  # initialized with a session and the current stack. Each guard can decide
@@ -124,9 +132,20 @@ module Clearance
124
132
  @rotate_csrf_on_sign_in = true
125
133
  @routes = true
126
134
  @secure_cookie = false
135
+ @signed_cookie = false
127
136
  @sign_in_guards = []
128
137
  end
129
138
 
139
+ def signed_cookie=(value)
140
+ if [true, false, :migrate].include? value
141
+ @signed_cookie = value
142
+ else
143
+ raise "Clearance's signed_cookie configuration value is invalid. " \
144
+ "Valid values are true, false, or :migrate. " \
145
+ "Set this option via Clearance.configure in an initializer"
146
+ end
147
+ end
148
+
130
149
  # The class representing the configured user model.
131
150
  # In the default configuration, this is the `User` class.
132
151
  # @return [Class]
@@ -15,9 +15,5 @@ module Clearance
15
15
  module PasswordStrategies
16
16
  autoload :BCrypt, "clearance/password_strategies/bcrypt"
17
17
  autoload :Argon2, "clearance/password_strategies/argon2"
18
- autoload :BCryptMigrationFromSHA1,
19
- "clearance/password_strategies/bcrypt_migration_from_sha1"
20
- autoload :Blowfish, "clearance/password_strategies/blowfish"
21
- autoload :SHA1, "clearance/password_strategies/sha1"
22
18
  end
23
19
  end
@@ -23,7 +23,7 @@ module Clearance
23
23
  response = @app.call(env)
24
24
 
25
25
  if session.authentication_successful?
26
- session.add_cookie_to_headers response[1]
26
+ session.add_cookie_to_headers
27
27
  end
28
28
 
29
29
  response
@@ -14,15 +14,9 @@ module Clearance
14
14
  # Called by {RackSession} to add the Clearance session cookie to a response.
15
15
  #
16
16
  # @return [void]
17
- def add_cookie_to_headers(headers)
17
+ def add_cookie_to_headers
18
18
  if signed_in_with_remember_token?
19
- Rack::Utils.set_cookie_header!(
20
- headers,
21
- remember_token_cookie,
22
- cookie_options.merge(
23
- value: current_user.remember_token,
24
- ),
25
- )
19
+ set_remember_token(current_user.remember_token)
26
20
  end
27
21
  end
28
22
 
@@ -112,9 +106,27 @@ module Clearance
112
106
  @cookies ||= ActionDispatch::Request.new(@env).cookie_jar
113
107
  end
114
108
 
109
+ # @api private
110
+ def set_remember_token(token)
111
+ case Clearance.configuration.signed_cookie
112
+ when true, :migrate
113
+ cookies.signed[remember_token_cookie] = cookie_options(token)
114
+ when false
115
+ cookies[remember_token_cookie] = cookie_options(token)
116
+ end
117
+ remember_token
118
+ end
119
+
115
120
  # @api private
116
121
  def remember_token
117
- cookies[remember_token_cookie]
122
+ case Clearance.configuration.signed_cookie
123
+ when true
124
+ cookies.signed[remember_token_cookie]
125
+ when :migrate
126
+ cookies.signed[remember_token_cookie] || cookies[remember_token_cookie]
127
+ when false
128
+ cookies[remember_token_cookie]
129
+ end
118
130
  end
119
131
 
120
132
  # @api private
@@ -159,7 +171,7 @@ module Clearance
159
171
  end
160
172
 
161
173
  # @api private
162
- def cookie_options
174
+ def cookie_options(value)
163
175
  {
164
176
  domain: domain,
165
177
  expires: remember_token_expires,
@@ -167,7 +179,7 @@ module Clearance
167
179
  same_site: Clearance.configuration.same_site,
168
180
  path: Clearance.configuration.cookie_path,
169
181
  secure: Clearance.configuration.secure_cookie,
170
- value: remember_token,
182
+ value: value,
171
183
  }
172
184
  end
173
185
 
@@ -175,7 +187,7 @@ module Clearance
175
187
  def delete_cookie_options
176
188
  Hash.new.tap do |options|
177
189
  if configured_cookie_domain
178
- options[:domain] = configured_cookie_domain
190
+ options[:domain] = domain
179
191
  end
180
192
  end
181
193
  end