clearance 2.0.0 → 2.3.1

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.

Potentially problematic release.


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

Files changed (60) 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 +142 -87
  7. data/NEWS.md +94 -0
  8. data/README.md +4 -24
  9. data/RELEASING.md +25 -0
  10. data/Rakefile +6 -1
  11. data/app/controllers/clearance/base_controller.rb +8 -1
  12. data/app/controllers/clearance/passwords_controller.rb +16 -3
  13. data/app/views/clearance_mailer/change_password.html.erb +2 -2
  14. data/app/views/clearance_mailer/change_password.text.erb +2 -2
  15. data/app/views/passwords/edit.html.erb +1 -1
  16. data/clearance.gemspec +9 -2
  17. data/config/locales/clearance.en.yml +1 -0
  18. data/config/routes.rb +1 -1
  19. data/gemfiles/rails_5.0.gemfile +10 -9
  20. data/gemfiles/rails_5.1.gemfile +11 -10
  21. data/gemfiles/rails_5.2.gemfile +11 -10
  22. data/gemfiles/rails_6.0.gemfile +11 -10
  23. data/gemfiles/rails_6.1.gemfile +21 -0
  24. data/lib/clearance/authentication.rb +1 -1
  25. data/lib/clearance/back_door.rb +2 -1
  26. data/lib/clearance/configuration.rb +37 -18
  27. data/lib/clearance/password_strategies.rb +2 -5
  28. data/lib/clearance/password_strategies/argon2.rb +23 -0
  29. data/lib/clearance/rack_session.rb +5 -1
  30. data/lib/clearance/session.rb +40 -12
  31. data/lib/clearance/user.rb +12 -3
  32. data/lib/clearance/version.rb +1 -1
  33. data/lib/generators/clearance/install/install_generator.rb +13 -0
  34. data/lib/generators/clearance/install/templates/README +10 -4
  35. data/lib/generators/clearance/install/templates/db/migrate/add_clearance_to_users.rb.erb +1 -1
  36. data/lib/generators/clearance/install/templates/db/migrate/create_users.rb.erb +1 -1
  37. data/lib/generators/clearance/routes/templates/routes.rb +1 -1
  38. data/spec/acceptance/clearance_installation_spec.rb +0 -4
  39. data/spec/app_templates/app/models/user.rb +1 -1
  40. data/spec/app_templates/testapp/app/views/layouts/application.html.erb +24 -0
  41. data/spec/clearance/back_door_spec.rb +20 -4
  42. data/spec/clearance/rack_session_spec.rb +3 -2
  43. data/spec/clearance/session_spec.rb +154 -51
  44. data/spec/configuration_spec.rb +60 -14
  45. data/spec/controllers/passwords_controller_spec.rb +19 -5
  46. data/spec/dummy/app/controllers/application_controller.rb +1 -1
  47. data/spec/generators/clearance/install/install_generator_spec.rb +36 -1
  48. data/spec/generators/clearance/views/views_generator_spec.rb +0 -1
  49. data/spec/mailers/clearance_mailer_spec.rb +33 -0
  50. data/spec/models/user_spec.rb +34 -5
  51. data/spec/password_strategies/argon2_spec.rb +79 -0
  52. data/spec/requests/authentication_cookie_spec.rb +55 -0
  53. data/spec/spec_helper.rb +0 -1
  54. data/spec/support/clearance.rb +11 -0
  55. data/spec/support/generator_spec_helpers.rb +1 -5
  56. data/spec/support/request_with_remember_token.rb +8 -6
  57. metadata +42 -12
  58. data/.travis.yml +0 -32
  59. data/app/views/layouts/application.html.erb +0 -23
  60. data/spec/app_templates/app/models/rails5/user.rb +0 -5
@@ -13,10 +13,7 @@ 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'
17
- autoload :BCryptMigrationFromSHA1,
18
- 'clearance/password_strategies/bcrypt_migration_from_sha1'
19
- autoload :Blowfish, 'clearance/password_strategies/blowfish'
20
- autoload :SHA1, 'clearance/password_strategies/sha1'
16
+ autoload :BCrypt, "clearance/password_strategies/bcrypt"
17
+ autoload :Argon2, "clearance/password_strategies/argon2"
21
18
  end
22
19
  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
@@ -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
27
+ end
28
+
25
29
  response
26
30
  end
27
31
  end
@@ -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
 
@@ -81,7 +75,7 @@ module Clearance
81
75
  end
82
76
 
83
77
  @current_user = nil
84
- cookies.delete remember_token_cookie
78
+ cookies.delete remember_token_cookie, delete_cookie_options
85
79
  end
86
80
 
87
81
  # True if {#current_user} is set.
@@ -98,6 +92,13 @@ module Clearance
98
92
  ! signed_in?
99
93
  end
100
94
 
95
+ # True if a successful authentication has been performed
96
+ #
97
+ # @return [Boolean]
98
+ def authentication_successful?
99
+ !!@current_user
100
+ end
101
+
101
102
  private
102
103
 
103
104
  # @api private
@@ -105,9 +106,27 @@ module Clearance
105
106
  @cookies ||= ActionDispatch::Request.new(@env).cookie_jar
106
107
  end
107
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
+
108
120
  # @api private
109
121
  def remember_token
110
- 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
111
130
  end
112
131
 
113
132
  # @api private
@@ -152,7 +171,7 @@ module Clearance
152
171
  end
153
172
 
154
173
  # @api private
155
- def cookie_options
174
+ def cookie_options(value)
156
175
  {
157
176
  domain: domain,
158
177
  expires: remember_token_expires,
@@ -160,10 +179,19 @@ module Clearance
160
179
  same_site: Clearance.configuration.same_site,
161
180
  path: Clearance.configuration.cookie_path,
162
181
  secure: Clearance.configuration.secure_cookie,
163
- value: remember_token,
182
+ value: value,
164
183
  }
165
184
  end
166
185
 
186
+ # @api private
187
+ def delete_cookie_options
188
+ Hash.new.tap do |options|
189
+ if configured_cookie_domain
190
+ options[:domain] = domain
191
+ end
192
+ end
193
+ end
194
+
167
195
  # @api private
168
196
  def domain
169
197
  if configured_cookie_domain.respond_to?(:call)
@@ -60,7 +60,7 @@ module Clearance
60
60
  # @see PasswordStrategies
61
61
  # @return [void]
62
62
  #
63
- # @!method authenticated?
63
+ # @!method authenticated?(password)
64
64
  # Check's the provided password against the user's encrypted password using
65
65
  # the configured password strategy. By default, this will be
66
66
  # {PasswordStrategies::BCrypt#authenticated?}, but can be changed with
@@ -117,11 +117,13 @@ module Clearance
117
117
  if password.present? && user.authenticated?(password)
118
118
  user
119
119
  end
120
+ else
121
+ prevent_timing_attack
120
122
  end
121
123
  end
122
124
 
123
125
  def find_by_normalized_email(email)
124
- find_by_email normalize_email(email)
126
+ find_by(email: normalize_email(email))
125
127
  end
126
128
 
127
129
  def normalize_email(email)
@@ -130,6 +132,13 @@ module Clearance
130
132
 
131
133
  private
132
134
 
135
+ DUMMY_PASSWORD = "*"
136
+
137
+ def prevent_timing_attack
138
+ new(password: DUMMY_PASSWORD)
139
+ nil
140
+ end
141
+
133
142
  def password_strategy
134
143
  Clearance.configuration.password_strategy || PasswordStrategies::BCrypt
135
144
  end
@@ -143,7 +152,7 @@ module Clearance
143
152
  validates :email,
144
153
  email: { strict_mode: true },
145
154
  presence: true,
146
- uniqueness: { allow_blank: true },
155
+ uniqueness: { allow_blank: true, case_sensitive: true },
147
156
  unless: :email_optional?
148
157
 
149
158
  validates :password, presence: true, unless: :skip_password_validation?
@@ -1,3 +1,3 @@
1
1
  module Clearance
2
- VERSION = "2.0.0".freeze
2
+ VERSION = "2.3.1".freeze
3
3
  end
@@ -122,6 +122,19 @@ module Clearance
122
122
  "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
123
123
  end
124
124
 
125
+ def migration_primary_key_type_string
126
+ if configured_key_type
127
+ ", id: :#{configured_key_type}"
128
+ end
129
+ end
130
+
131
+ def configured_key_type
132
+ active_record = Rails.configuration.generators.active_record
133
+ active_record ||= Rails.configuration.generators.options[:active_record]
134
+
135
+ active_record[:primary_key_type]
136
+ end
137
+
125
138
  def models_inherit_from
126
139
  "ApplicationRecord"
127
140
  end
@@ -8,9 +8,11 @@ Next steps:
8
8
  # config/environments/{development,test}.rb
9
9
  config.action_mailer.default_url_options = { host: 'localhost:3000' }
10
10
 
11
- In production it should be your app's domain name.
11
+ In the production environment it should be your application's full hostname.
12
12
 
13
- 2. Display user session and flashes. For example, in your application layout:
13
+ 2. Display user session status.
14
+
15
+ From somewhere in your layout, render sign in and sign out buttons:
14
16
 
15
17
  <% if signed_in? %>
16
18
  Signed in as: <%= current_user.email %>
@@ -19,14 +21,18 @@ Next steps:
19
21
  <%= link_to 'Sign in', sign_in_path %>
20
22
  <% end %>
21
23
 
24
+ 3. Render the flash contents.
25
+
26
+ Make sure the flash is being rendered in your views using something like:
27
+
22
28
  <div id="flash">
23
29
  <% flash.each do |key, value| %>
24
30
  <div class="flash <%= key %>"><%= value %></div>
25
31
  <% end %>
26
32
  </div>
27
33
 
28
- 3. Migrate:
34
+ 4. Migrate:
29
35
 
30
- rails db:migrate
36
+ Run `rails db:migrate` to add the clearance database changes.
31
37
 
32
38
  *******************************************************************************
@@ -13,7 +13,7 @@ class AddClearanceToUsers < ActiveRecord::Migration<%= migration_version %>
13
13
  users = select_all("SELECT id FROM users WHERE remember_token IS NULL")
14
14
 
15
15
  users.each do |user|
16
- update <<-SQL
16
+ update <<-SQL.squish
17
17
  UPDATE users
18
18
  SET remember_token = '#{Clearance::Token.new}'
19
19
  WHERE id = '#{user['id']}'
@@ -1,6 +1,6 @@
1
1
  class CreateUsers < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
- create_table :users do |t|
3
+ create_table :users<%= migration_primary_key_type_string %> do |t|
4
4
  t.timestamps null: false
5
5
  t.string :email, null: false
6
6
  t.string :encrypted_password, limit: 128, null: false
@@ -4,7 +4,7 @@
4
4
  resources :users, controller: "clearance/users", only: [:create] do
5
5
  resource :password,
6
6
  controller: "clearance/passwords",
7
- only: [:create, :edit, :update]
7
+ only: [:edit, :update]
8
8
  end
9
9
 
10
10
  get "/sign_in" => "clearance/sessions#new", as: "sign_in"
@@ -36,9 +36,6 @@ describe "Clearance Installation" do
36
36
  --skip-keeps
37
37
  --skip-sprockets
38
38
  CMD
39
-
40
- FileUtils.rm_f("public/index.html")
41
- FileUtils.rm_f("app/views/layouts/application.html.erb")
42
39
  end
43
40
 
44
41
  def testapp_templates
@@ -47,7 +44,6 @@ describe "Clearance Installation" do
47
44
 
48
45
  def configure_test_app
49
46
  FileUtils.rm_f("public/index.html")
50
- FileUtils.rm_f("app/views/layouts/application.html.erb")
51
47
  FileUtils.cp_r(testapp_templates, "..")
52
48
  end
53
49
 
@@ -1,4 +1,4 @@
1
- class User < ActiveRecord::Base
1
+ class User < ApplicationRecord
2
2
  def previously_existed?
3
3
  true
4
4
  end
@@ -0,0 +1,24 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <%= javascript_include_tag 'application' %>
5
+ <%= csrf_meta_tag %>
6
+ </head>
7
+ <body>
8
+ <div id="header">
9
+ <% if signed_in? -%>
10
+ <%= button_to t(".sign_out"), sign_out_path, method: :delete %>
11
+ <% else -%>
12
+ <%= link_to t(".sign_in"), sign_in_path %>
13
+ <% end -%>
14
+ </div>
15
+
16
+ <div id="flash">
17
+ <% flash.each do |key, value| -%>
18
+ <div id="flash_<%= key %>"><%=h value %></div>
19
+ <% end %>
20
+ </div>
21
+
22
+ <%= yield %>
23
+ </body>
24
+ </html>
@@ -46,6 +46,18 @@ describe Clearance::BackDoor do
46
46
  end
47
47
  end
48
48
 
49
+ it "strips 'as' from the params" do
50
+ user_id = "123"
51
+ user = double("user")
52
+ allow(User).to receive(:find).with(user_id).and_return(user)
53
+ env = build_env(as: user_id, foo: :bar)
54
+ back_door = Clearance::BackDoor.new(mock_app)
55
+
56
+ back_door.call(env)
57
+
58
+ expect(env["QUERY_STRING"]).to eq("foo=bar")
59
+ end
60
+
49
61
  context "when the environments are disabled" do
50
62
  before do
51
63
  Clearance.configuration.allowed_backdoor_environments = nil
@@ -84,14 +96,18 @@ describe Clearance::BackDoor do
84
96
  env_for_user_id("")
85
97
  end
86
98
 
87
- def env_for_user_id(user_id)
99
+ def build_env(params)
100
+ query = Rack::Utils.build_query(params)
88
101
  clearance = double("clearance", sign_in: true)
89
- Rack::MockRequest.env_for("/?as=#{user_id}").merge(clearance: clearance)
102
+ Rack::MockRequest.env_for("/?#{query}").merge(clearance: clearance)
103
+ end
104
+
105
+ def env_for_user_id(user_id)
106
+ build_env(as: user_id)
90
107
  end
91
108
 
92
109
  def env_for_username(username)
93
- clearance = double("clearance", sign_in: true)
94
- Rack::MockRequest.env_for("/?as=#{username}").merge(clearance: clearance)
110
+ build_env(as: username)
95
111
  end
96
112
 
97
113
  def mock_app
@@ -11,6 +11,8 @@ describe Clearance::RackSession do
11
11
  env = Rack::MockRequest.env_for('/')
12
12
  expected_session = "the session"
13
13
  allow(expected_session).to receive(:add_cookie_to_headers)
14
+ allow(expected_session).to receive(:authentication_successful?).
15
+ and_return(true)
14
16
  allow(Clearance::Session).to receive(:new).
15
17
  with(env).
16
18
  and_return(expected_session)
@@ -18,7 +20,6 @@ describe Clearance::RackSession do
18
20
  response = Rack::MockResponse.new(*app.call(env))
19
21
 
20
22
  expect(response.body).to eq expected_session
21
- expect(expected_session).to have_received(:add_cookie_to_headers).
22
- with(hash_including(headers))
23
+ expect(expected_session).to have_received(:add_cookie_to_headers)
23
24
  end
24
25
  end
@@ -4,7 +4,6 @@ describe Clearance::Session do
4
4
  before { Timecop.freeze }
5
5
  after { Timecop.return }
6
6
 
7
- let(:headers) { {} }
8
7
  let(:session) { Clearance::Session.new(env_without_remember_token) }
9
8
  let(:user) { create(:user) }
10
9
 
@@ -35,9 +34,63 @@ describe Clearance::Session do
35
34
  Clearance.configuration.cookie_name = "custom_cookie_name"
36
35
 
37
36
  session.sign_in user
38
- session.add_cookie_to_headers(headers)
37
+ session.add_cookie_to_headers
39
38
 
40
- expect(headers["Set-Cookie"]).to match(/custom_cookie_name=.+;/)
39
+ expect(remember_token_cookie(session, "custom_cookie_name")).to be_present
40
+ end
41
+ end
42
+
43
+ context "with signed cookies == false" do
44
+ it "uses cookies.signed" do
45
+ Clearance.configuration.signed_cookie = true
46
+
47
+ cookie_jar = {}
48
+ expect(session).to receive(:cookies).and_return(cookie_jar)
49
+ expect(cookie_jar).to receive(:signed).and_return(cookie_jar)
50
+
51
+ session.sign_in user
52
+ end
53
+ end
54
+
55
+ context "with signed cookies == true" do
56
+ it "uses cookies.signed" do
57
+ Clearance.configuration.signed_cookie = true
58
+
59
+ cookie_jar = {}
60
+ expect(session).to receive(:cookies).and_return(cookie_jar)
61
+ expect(cookie_jar).to receive(:signed).and_return(cookie_jar)
62
+
63
+ session.sign_in user
64
+ end
65
+ end
66
+
67
+ context "with signed cookies == :migrate" do
68
+ before do
69
+ Clearance.configuration.signed_cookie = :migrate
70
+ end
71
+
72
+ context "signed cookie exists" do
73
+ it "uses cookies.signed[remember_token]" do
74
+ cookie_jar = { "remember_token" => "signed cookie" }
75
+ expect(session).to receive(:cookies).and_return(cookie_jar)
76
+ expect(cookie_jar).to receive(:signed).and_return(cookie_jar)
77
+
78
+ session.sign_in user
79
+ end
80
+ end
81
+
82
+ context "signed cookie does not exist yet" do
83
+ it "uses cookies[remember_token] instead" do
84
+ cookie_jar = { "remember_token" => "signed cookie" }
85
+ # first call will try to get the signed cookie
86
+ expect(session).to receive(:cookies).and_return(cookie_jar)
87
+ # ... but signed_cookie doesn't exist
88
+ expect(cookie_jar).to receive(:signed).and_return({})
89
+ # then it attempts to retrieve the unsigned cookie
90
+ expect(session).to receive(:cookies).and_return(cookie_jar)
91
+
92
+ session.sign_in user
93
+ end
41
94
  end
42
95
  end
43
96
 
@@ -157,9 +210,9 @@ describe Clearance::Session do
157
210
  end
158
211
 
159
212
  it 'sets a httponly cookie' do
160
- session.add_cookie_to_headers(headers)
213
+ session.add_cookie_to_headers
161
214
 
162
- expect(headers['Set-Cookie']).to match(/remember_token=.+; HttpOnly/)
215
+ expect(remember_token_cookie(session)[:httponly]).to be_truthy
163
216
  end
164
217
  end
165
218
 
@@ -170,9 +223,9 @@ describe Clearance::Session do
170
223
  end
171
224
 
172
225
  it 'sets a standard cookie' do
173
- session.add_cookie_to_headers(headers)
226
+ session.add_cookie_to_headers
174
227
 
175
- expect(headers['Set-Cookie']).not_to match(/remember_token=.+; HttpOnly/)
228
+ expect(remember_token_cookie(session)[:httponly]).to be_falsey
176
229
  end
177
230
  end
178
231
 
@@ -183,9 +236,9 @@ describe Clearance::Session do
183
236
  end
184
237
 
185
238
  it "sets a same-site cookie" do
186
- session.add_cookie_to_headers(headers)
239
+ session.add_cookie_to_headers
187
240
 
188
- expect(headers["Set-Cookie"]).to match(/remember_token=.+; SameSite/)
241
+ expect(remember_token_cookie(session)[:same_site]).to eq(:lax)
189
242
  end
190
243
  end
191
244
 
@@ -195,9 +248,9 @@ describe Clearance::Session do
195
248
  end
196
249
 
197
250
  it "sets a standard cookie" do
198
- session.add_cookie_to_headers(headers)
251
+ session.add_cookie_to_headers
199
252
 
200
- expect(headers["Set-Cookie"]).to_not match(/remember_token=.+; SameSite/)
253
+ expect(remember_token_cookie(session)[:same_site]).to be_nil
201
254
  end
202
255
  end
203
256
 
@@ -205,15 +258,11 @@ describe Clearance::Session do
205
258
  context 'default configuration' do
206
259
  it 'is set to 1 year from now' do
207
260
  user = double("User", remember_token: "123abc")
208
- headers = {}
209
261
  session = Clearance::Session.new(env_without_remember_token)
210
262
  session.sign_in user
211
- session.add_cookie_to_headers headers
263
+ session.add_cookie_to_headers
212
264
 
213
- expect(headers).to set_cookie(
214
- 'remember_token',
215
- user.remember_token, 1.year.from_now
216
- )
265
+ expect(remember_token_cookie(session)[:expires]).to eq(1.year.from_now)
217
266
  end
218
267
  end
219
268
 
@@ -225,18 +274,14 @@ describe Clearance::Session do
225
274
  end
226
275
  with_custom_expiration expires_at do
227
276
  user = double("User", remember_token: "123abc")
228
- headers = {}
229
277
  environment = env_with_cookies(remember_me: 'true')
230
278
  session = Clearance::Session.new(environment)
231
279
  session.sign_in user
232
- session.add_cookie_to_headers headers
233
-
234
- expect(headers).to set_cookie(
235
- 'remember_token',
236
- user.remember_token,
237
- remembered_expires
238
- )
239
-
280
+ session.add_cookie_to_headers
281
+ expect(remember_token_cookie(session)[:expires]).to \
282
+ eq(remembered_expires)
283
+ expect(remember_token_cookie(session)[:value]).to \
284
+ eq(user.remember_token)
240
285
  end
241
286
  end
242
287
  end
@@ -249,9 +294,9 @@ describe Clearance::Session do
249
294
  end
250
295
 
251
296
  it 'sets a standard cookie' do
252
- session.add_cookie_to_headers(headers)
297
+ session.add_cookie_to_headers
253
298
 
254
- expect(headers['Set-Cookie']).not_to match(/remember_token=.+; secure/)
299
+ expect(remember_token_cookie(session)[:secure]).to be_falsey
255
300
  end
256
301
  end
257
302
 
@@ -262,9 +307,9 @@ describe Clearance::Session do
262
307
  end
263
308
 
264
309
  it 'sets a secure cookie' do
265
- session.add_cookie_to_headers(headers)
310
+ session.add_cookie_to_headers
266
311
 
267
- expect(headers['Set-Cookie']).to match(/remember_token=.+; secure/)
312
+ expect(remember_token_cookie(session)[:secure]).to be_truthy
268
313
  end
269
314
  end
270
315
  end
@@ -280,9 +325,9 @@ describe Clearance::Session do
280
325
  let(:cookie_domain) { ".example.com" }
281
326
 
282
327
  it "sets a standard cookie" do
283
- session.add_cookie_to_headers(headers)
328
+ session.add_cookie_to_headers
284
329
 
285
- expect(headers['Set-Cookie']).to match(/domain=\.example\.com; path/)
330
+ expect(remember_token_cookie(session)[:domain]).to eq(cookie_domain)
286
331
  end
287
332
  end
288
333
 
@@ -290,9 +335,9 @@ describe Clearance::Session do
290
335
  let(:cookie_domain) { lambda { |_r| ".example.com" } }
291
336
 
292
337
  it "sets a standard cookie" do
293
- session.add_cookie_to_headers(headers)
338
+ session.add_cookie_to_headers
294
339
 
295
- expect(headers['Set-Cookie']).to match(/domain=\.example\.com; path/)
340
+ expect(remember_token_cookie(session)[:domain]).to eq(".example.com")
296
341
  end
297
342
  end
298
343
  end
@@ -301,9 +346,9 @@ describe Clearance::Session do
301
346
  before { session.sign_in(user) }
302
347
 
303
348
  it 'sets a standard cookie' do
304
- session.add_cookie_to_headers(headers)
349
+ session.add_cookie_to_headers
305
350
 
306
- expect(headers["Set-Cookie"]).not_to match(/domain=.+; path/)
351
+ expect(remember_token_cookie(session)[:domain]).to be_nil
307
352
  end
308
353
  end
309
354
  end
@@ -313,9 +358,9 @@ describe Clearance::Session do
313
358
  before { session.sign_in(user) }
314
359
 
315
360
  it 'sets a standard cookie' do
316
- session.add_cookie_to_headers(headers)
361
+ session.add_cookie_to_headers
317
362
 
318
- expect(headers["Set-Cookie"]).to_not match(/domain=.+; path/)
363
+ expect(remember_token_cookie(session)[:domain]).to be_nil
319
364
  end
320
365
  end
321
366
 
@@ -326,28 +371,86 @@ describe Clearance::Session do
326
371
  end
327
372
 
328
373
  it 'sets a standard cookie' do
329
- session.add_cookie_to_headers(headers)
374
+ session.add_cookie_to_headers
330
375
 
331
- expect(headers['Set-Cookie']).to match(/path=\/user; expires/)
376
+ expect(remember_token_cookie(session)[:path]).to eq("/user")
332
377
  end
333
378
  end
334
379
  end
335
380
 
336
381
  it 'does not set a remember token when signed out' do
337
- headers = {}
338
382
  session = Clearance::Session.new(env_without_remember_token)
339
- session.add_cookie_to_headers headers
340
- expect(headers["Set-Cookie"]).to be nil
383
+ session.add_cookie_to_headers
384
+ expect(remember_token_cookie(session)).to be_nil
341
385
  end
342
386
 
343
- it 'signs out a user' do
344
- user = create(:user)
345
- old_remember_token = user.remember_token
346
- env = env_with_remember_token(old_remember_token)
347
- session = Clearance::Session.new(env)
348
- session.sign_out
349
- expect(session.current_user).to be_nil
350
- expect(user.reload.remember_token).not_to eq old_remember_token
387
+ describe "#sign_out" do
388
+ it "signs out a user" do
389
+ user = create(:user)
390
+ old_remember_token = user.remember_token
391
+ env = env_with_remember_token(old_remember_token)
392
+ session = Clearance::Session.new(env)
393
+ cookie_jar = ActionDispatch::Request.new(env).cookie_jar
394
+ expect(cookie_jar.deleted?(:remember_token)).to be false
395
+
396
+ session.sign_out
397
+
398
+ expect(cookie_jar.deleted?(:remember_token)).to be true
399
+ expect(session.current_user).to be_nil
400
+ expect(user.reload.remember_token).not_to eq old_remember_token
401
+ end
402
+
403
+ context "with custom cookie domain" do
404
+ let(:domain) { ".example.com" }
405
+
406
+ before do
407
+ Clearance.configuration.cookie_domain = domain
408
+ end
409
+
410
+ it "clears cookie" do
411
+ user = create(:user)
412
+ env = env_with_remember_token(
413
+ value: user.remember_token,
414
+ domain: domain,
415
+ )
416
+ session = Clearance::Session.new(env)
417
+ cookie_jar = ActionDispatch::Request.new(env).cookie_jar
418
+ expect(cookie_jar.deleted?(:remember_token, domain: domain)).to be false
419
+
420
+ session.sign_out
421
+
422
+ expect(cookie_jar.deleted?(:remember_token, domain: domain)).to be true
423
+ end
424
+ end
425
+
426
+ context 'with callable cookie domain' do
427
+ it 'clears cookie' do
428
+ domain = '.example.com'
429
+ Clearance.configuration.cookie_domain = ->(_) { domain }
430
+ user = create(:user)
431
+ env = env_with_remember_token(
432
+ value: user.remember_token,
433
+ domain: domain
434
+ )
435
+ session = Clearance::Session.new(env)
436
+ cookie_jar = ActionDispatch::Request.new(env).cookie_jar
437
+ expect(cookie_jar.deleted?(:remember_token, domain: domain)).to be false
438
+
439
+ session.sign_out
440
+
441
+ expect(cookie_jar.deleted?(:remember_token, domain: domain)).to be true
442
+ end
443
+ end
444
+ end
445
+
446
+ # a bit of a hack to get the cookies that ActionDispatch sets inside session
447
+ def remember_token_cookie(session, cookie_name = "remember_token")
448
+ cookies = session.send(:cookies)
449
+ # see https://stackoverflow.com/a/21315095
450
+ set_cookies = cookies.instance_eval <<-RUBY, __FILE__, __LINE__ + 1
451
+ @set_cookies
452
+ RUBY
453
+ set_cookies[cookie_name]
351
454
  end
352
455
 
353
456
  def env_with_cookies(cookies)