devise-multi-factor 3.1.8 → 3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aa5e1378455028fe0381e90e77d682e835adc7ca7dafb3bc87f7e17fbe0ed596
4
- data.tar.gz: 6972266b9fac6577838115f5f9d45eb6014e3d5c94a5ccce7d962ae942224d0f
3
+ metadata.gz: b69e7dd9a398b8cfebfd6c3b1e17e4c94603f2097e56a54f78a5f4ce07ce9751
4
+ data.tar.gz: 76f657bc840dc7b9b3d48fb3fe66876194ba4481e6849aac1b8f28b3a22eae52
5
5
  SHA512:
6
- metadata.gz: a5e3fdb1eebefe645b3f15a731dc8305959c56ec07efb2c819bbe4d8c48c54b67cf03a38669d99edae8e32e080b31218f8dffd63047988bb32e16f18844f0d35
7
- data.tar.gz: d11d1f93a080665f9ff48e1aa14a7319ab4ac05f1a6389e955fe0431776d7bb698d53b274e49b94c75eea8517b8eeba1224146ea44254338529db64b9c1da8ed
6
+ metadata.gz: f1dc861603dd878a0caddf74a316f7c385e3d103a0049de7cf5d1f2baadb29c7b7c1a1c25fa2c42a865c01eac178d3bd92621a53b278f6ec4f6c1f3fa0ec11d2
7
+ data.tar.gz: 513a0cb006edb6613a7c3e0af9cf1ab8594ed2841429b09d7b34e538c2a8a2d614f001aa86c1b7a87ef8946afddfdbcae16beda276cd437d8c0ca61fa84b0741
@@ -16,11 +16,11 @@ class Devise::TotpController < DeviseController
16
16
  if resource.enroll_totp!(@otp_secret, params[:otp_attempt])
17
17
  after_two_factor_enroll_success_for(resource)
18
18
  else
19
- flash.now[:error] = 'The authenticator code provided was invalid!'
20
- render_enroll_form
19
+ flash.now[:error] = I18n.t('devise.totp_setup.invalid_code')
20
+ render_enroll_form(status: :unprocessable_entity)
21
21
  end
22
22
  rescue ActiveSupport::MessageVerifier::InvalidSignature
23
- redirect_to send("new_#{resource_name}_two_factor_authentication_path"), flash: { error: 'There has been a problem in the configuration process, please try again.' }
23
+ redirect_to send("new_#{resource_name}_two_factor_authentication_path"), flash: { error: I18n.t('devise.totp_setup.invalid_signature') }
24
24
  end
25
25
 
26
26
  def show
@@ -40,9 +40,9 @@ class Devise::TotpController < DeviseController
40
40
  .to_data_url
41
41
  end
42
42
 
43
- def render_enroll_form
43
+ def render_enroll_form(status: :ok)
44
44
  @qr_code = generate_qr_code(@otp_secret)
45
- render :new
45
+ render :new, status: status
46
46
  end
47
47
 
48
48
  def verifier
@@ -45,9 +45,11 @@ class Devise::TwoFactorAuthenticationController < DeviseController
45
45
  expires_seconds = resource.class.remember_otp_session_for_seconds
46
46
 
47
47
  if expires_seconds && expires_seconds > 0
48
+ expires_at = expires_seconds.seconds.from_now
48
49
  cookies.signed[DeviseMultiFactor::REMEMBER_TFA_COOKIE_NAME] = {
49
- value: "#{resource.class}-#{resource.public_send(Devise.second_factor_resource_id)}",
50
- expires: expires_seconds.seconds.from_now
50
+ value: DeviseMultiFactor::RememberTFACookie.new
51
+ .generate_cookie_data(resource, expires_at: expires_at),
52
+ expires: expires_at,
51
53
  }
52
54
  end
53
55
  end
@@ -6,3 +6,6 @@ en:
6
6
  max_login_attempts_reached: "Access completely denied as you have reached your attempts limit"
7
7
  contact_administrator: "Please contact your system administrator."
8
8
  code_has_been_sent: "Your authentication code has been sent."
9
+ totp_setup:
10
+ invalid_code: "The authenticator code provided was invalid!"
11
+ invalid_signature: "There has been a problem in the configuration process, please try again."
@@ -1,8 +1,7 @@
1
1
  Warden::Manager.after_authentication do |resource, auth, options|
2
2
  if auth.env["action_dispatch.cookies"]
3
- expected_cookie_value = "#{resource.class}-#{resource.public_send(Devise.second_factor_resource_id)}"
4
- actual_cookie_value = auth.env["action_dispatch.cookies"].signed[DeviseMultiFactor::REMEMBER_TFA_COOKIE_NAME]
5
- bypass_by_cookie = actual_cookie_value == expected_cookie_value
3
+ cookie_value = auth.env["action_dispatch.cookies"].signed[DeviseMultiFactor::REMEMBER_TFA_COOKIE_NAME]
4
+ bypass_by_cookie = DeviseMultiFactor::RememberTFACookie.new.valid_cookie_data?(resource, cookie_value)
6
5
  end
7
6
 
8
7
  if resource.respond_to?(:need_two_factor_authentication?) && !bypass_by_cookie
@@ -0,0 +1,39 @@
1
+ module DeviseMultiFactor
2
+ class RememberTFACookie
3
+
4
+ def generate_cookie_data(resource, expires_at:)
5
+ { 'data' => generate_resource_data(resource) }
6
+ .merge('expires_at' => expires_at)
7
+ .to_json
8
+ end
9
+
10
+ def valid_cookie_data?(resource, cookie_data)
11
+ return false if cookie_data.nil?
12
+
13
+ parsed_data = JSON.parse(cookie_data)
14
+ expires_at = parse_time(parsed_data['expires_at'])
15
+ return false if expires_at.nil? || expires_at < Time.current
16
+
17
+ expected_data = generate_resource_data(resource)
18
+ parsed_data['data'] == expected_data
19
+ rescue JSON::ParserError
20
+ false
21
+ end
22
+
23
+ private
24
+
25
+ def generate_resource_data(resource)
26
+ {
27
+ 'resource_name' => resource.class.to_s,
28
+ 'resource_id' => resource.public_send(Devise.second_factor_resource_id),
29
+ 'remember_tfa_token' => resource.try(:remember_tfa_token) || '',
30
+ }
31
+ end
32
+
33
+ def parse_time(time_str)
34
+ Time.parse(time_str)
35
+ rescue StandardError
36
+ nil
37
+ end
38
+ end
39
+ end
@@ -1,3 +1,3 @@
1
1
  module DeviseMultiFactor
2
- VERSION = "3.1.8".freeze
2
+ VERSION = "3.2.0".freeze
3
3
  end
@@ -52,5 +52,6 @@ Devise.add_module :totp_enrollable, model: 'devise_multi_factor/models/totp_enro
52
52
 
53
53
  require 'devise_multi_factor/orm/active_record' if defined?(ActiveRecord::Base)
54
54
  require 'devise_multi_factor/routes'
55
+ require 'devise_multi_factor/remember_tfa_cookie'
55
56
  require 'devise_multi_factor/models/two_factor_authenticatable'
56
57
  require 'devise_multi_factor/rails'
@@ -1,7 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Devise::TwoFactorAuthenticationController, type: :controller do
4
- describe 'is_fully_authenticated? helper' do
4
+ describe '#is_fully_authenticated?' do
5
5
  def post_code(code)
6
6
  if Rails::VERSION::MAJOR >= 5
7
7
  post :update, params: { code: code }
@@ -1,7 +1,7 @@
1
1
  require 'spec_helper'
2
2
  include AuthenticatedModelHelper
3
3
 
4
- feature "User of two factor authentication" do
4
+ feature 'User of two factor authentication' do
5
5
  context 'sending two factor authentication code via SMS' do
6
6
  shared_examples 'sends and authenticates code' do |user, type|
7
7
  before do
@@ -48,62 +48,62 @@ feature "User of two factor authentication" do
48
48
  it_behaves_like 'sends and authenticates code', create_user, 'encrypted'
49
49
  end
50
50
 
51
- scenario "must be logged in" do
51
+ scenario 'must be logged in' do
52
52
  visit user_two_factor_authentication_path
53
53
 
54
- expect(page).to have_content("Welcome Home")
55
- expect(page).to have_content("You are signed out")
54
+ expect(page).to have_content('Welcome Home')
55
+ expect(page).to have_content('You are signed out')
56
56
  end
57
57
 
58
- context "when logged in" do
58
+ context 'when logged in' do
59
59
  let(:user) { create_user }
60
60
 
61
61
  background do
62
62
  login_as user
63
63
  end
64
64
 
65
- scenario "is redirected to TFA when path requires authentication" do
66
- visit dashboard_path + "?A=param%20a&B=param%20b"
65
+ scenario 'is redirected to TFA when path requires authentication' do
66
+ visit dashboard_path + '?A=param%20a&B=param%20b'
67
67
 
68
- expect(page).to_not have_content("Your Personal Dashboard")
68
+ expect(page).to_not have_content('Your Personal Dashboard')
69
69
 
70
- fill_in "code", with: SMSProvider.last_message.body
71
- click_button "Submit"
70
+ fill_in 'code', with: SMSProvider.last_message.body
71
+ click_button 'Submit'
72
72
 
73
- expect(page).to have_content("Your Personal Dashboard")
74
- expect(page).to have_content("You are signed in as Marissa")
75
- expect(page).to have_content("Param A is param a")
76
- expect(page).to have_content("Param B is param b")
73
+ expect(page).to have_content('Your Personal Dashboard')
74
+ expect(page).to have_content('You are signed in as Marissa')
75
+ expect(page).to have_content('Param A is param a')
76
+ expect(page).to have_content('Param B is param b')
77
77
  end
78
78
 
79
- scenario "is locked out after max failed attempts" do
79
+ scenario 'is locked out after max failed attempts' do
80
80
  visit user_two_factor_authentication_path
81
81
 
82
82
  max_attempts = User.max_login_attempts
83
83
 
84
84
  max_attempts.times do
85
- fill_in "code", with: "incorrect#{rand(100)}"
86
- click_button "Submit"
85
+ fill_in 'code', with: "incorrect#{rand(100)}"
86
+ click_button 'Submit'
87
87
 
88
- within(".flash.alert") do
89
- expect(page).to have_content("Attempt failed")
88
+ within('.flash.alert') do
89
+ expect(page).to have_content('Attempt failed')
90
90
  end
91
91
  end
92
92
 
93
- expect(page).to have_content("Access completely denied")
94
- expect(page).to have_content("You are signed out")
93
+ expect(page).to have_content('Access completely denied')
94
+ expect(page).to have_content('You are signed out')
95
95
  end
96
96
 
97
- scenario "cannot retry authentication after max attempts" do
97
+ scenario 'cannot retry authentication after max attempts' do
98
98
  user.update_attribute(:second_factor_attempts_count, User.max_login_attempts)
99
99
 
100
100
  visit user_two_factor_authentication_path
101
101
 
102
- expect(page).to have_content("Access completely denied")
103
- expect(page).to have_content("You are signed out")
102
+ expect(page).to have_content('Access completely denied')
103
+ expect(page).to have_content('You are signed out')
104
104
  end
105
105
 
106
- describe "rememberable TFA" do
106
+ describe 'rememberable TFA' do
107
107
  before do
108
108
  @original_remember_otp_session_for_seconds = User.remember_otp_session_for_seconds
109
109
  User.remember_otp_session_for_seconds = 30.days
@@ -120,11 +120,11 @@ feature "User of two factor authentication" do
120
120
 
121
121
  login_as user
122
122
  visit dashboard_path
123
- expect(page).to have_content("Your Personal Dashboard")
124
- expect(page).to have_content("You are signed in as Marissa")
123
+ expect(page).to have_content('Your Personal Dashboard')
124
+ expect(page).to have_content('You are signed in as Marissa')
125
125
  end
126
126
 
127
- scenario "requires TFA code again after 30 days" do
127
+ scenario 'requires TFA code again after 30 days' do
128
128
  sms_sign_in
129
129
 
130
130
  logout
@@ -132,8 +132,8 @@ feature "User of two factor authentication" do
132
132
  Timecop.travel(30.days.from_now)
133
133
  login_as user
134
134
  visit dashboard_path
135
- expect(page).to have_content("You are signed in as Marissa")
136
- expect(page).to have_content("Enter the code that was sent to you")
135
+ expect(page).to have_content('You are signed in as Marissa')
136
+ expect(page).to have_content('Enter the code that was sent to you')
137
137
  end
138
138
 
139
139
  scenario 'TFA should be different for different users' do
@@ -172,7 +172,7 @@ feature "User of two factor authentication" do
172
172
  set_tfa_cookie(tfa_cookie1)
173
173
  login_as(user2)
174
174
  visit dashboard_path
175
- expect(page).to have_content("Enter the code that was sent to you")
175
+ expect(page).to have_content('Enter the code that was sent to you')
176
176
  end
177
177
 
178
178
  scenario 'Delete cookie when user logs out if enabled' do
@@ -184,7 +184,7 @@ feature "User of two factor authentication" do
184
184
  login_as user
185
185
 
186
186
  visit dashboard_path
187
- expect(page).to have_content("Enter the code that was sent to you")
187
+ expect(page).to have_content('Enter the code that was sent to you')
188
188
  end
189
189
  end
190
190
 
@@ -0,0 +1,135 @@
1
+ require 'spec_helper'
2
+
3
+ class MockUser
4
+ def id
5
+ 15
6
+ end
7
+ end
8
+
9
+ class MockUserWithRememberTFAToken
10
+ def id
11
+ 45
12
+ end
13
+
14
+ def remember_tfa_token
15
+ 'generated_token'
16
+ end
17
+ end
18
+
19
+ describe DeviseMultiFactor::RememberTFACookie do
20
+ subject(:remember_tfa_cookie) { described_class.new }
21
+
22
+ describe '#generate_cookie_data' do
23
+ describe 'when resource does not define remember_tfa_token method' do
24
+ let(:resource) { MockUser.new }
25
+
26
+ it 'returns cookie value with expiration date' do
27
+ result = remember_tfa_cookie.generate_cookie_data(
28
+ resource,
29
+ expires_at: Time.utc(2022, 1, 17, 19, 28, 0),
30
+ )
31
+
32
+ expect(JSON.parse(result)).to eql(
33
+ 'data' => {
34
+ 'resource_name' => 'MockUser',
35
+ 'resource_id' => 15,
36
+ 'remember_tfa_token' => '',
37
+ },
38
+ 'expires_at' => '2022-01-17T19:28:00.000Z',
39
+ )
40
+ end
41
+ end
42
+
43
+ describe 'when resource defines remember_tfa_token method' do
44
+ let(:resource) { MockUserWithRememberTFAToken.new }
45
+
46
+ it 'returns cookie value with expiration date and tfa remember token' do
47
+ result = remember_tfa_cookie.generate_cookie_data(
48
+ resource,
49
+ expires_at: Time.utc(2022, 1, 17, 19, 28, 0),
50
+ )
51
+
52
+ expect(JSON.parse(result)).to eql(
53
+ 'data' => {
54
+ 'resource_name' => 'MockUserWithRememberTFAToken',
55
+ 'resource_id' => 45,
56
+ 'remember_tfa_token' => 'generated_token',
57
+ },
58
+ 'expires_at' => '2022-01-17T19:28:00.000Z',
59
+ )
60
+ end
61
+ end
62
+ end
63
+
64
+ describe '#valid_cookie_data?' do
65
+ let(:cookie_data) do
66
+ {
67
+ 'data' => {
68
+ 'resource_name' => resource_name,
69
+ 'resource_id' => resource_id,
70
+ 'remember_tfa_token' => remember_tfa_token,
71
+ },
72
+ 'expires_at' => '2022-01-17T19:28:00.000Z',
73
+ }.to_json
74
+ end
75
+ let(:resource_name) { 'MockUserWithRememberTFAToken' }
76
+ let(:resource_id) { 45 }
77
+ let(:remember_tfa_token) { 'generated_token' }
78
+ let(:resource) { MockUserWithRememberTFAToken.new }
79
+
80
+ describe 'when cookie data has expired' do
81
+ it 'returns false' do
82
+ Timecop.freeze(Time.utc(2022, 1, 17, 19, 29, 0)) do
83
+ result = remember_tfa_cookie.valid_cookie_data?(resource, cookie_data)
84
+ expect(result).to be(false)
85
+ end
86
+ end
87
+ end
88
+
89
+ describe 'when cookie data has not expired' do
90
+ let(:date_before_expiration) { Time.utc(2022, 1, 17, 19, 27, 0) }
91
+
92
+ describe 'when resource class does not match' do
93
+ let(:resource_name) { 'MockUser' }
94
+
95
+ it 'returns false' do
96
+ Timecop.freeze(date_before_expiration) do
97
+ result = remember_tfa_cookie.valid_cookie_data?(resource, cookie_data)
98
+ expect(result).to be(false)
99
+ end
100
+ end
101
+ end
102
+
103
+ describe 'when resource id does not match' do
104
+ let(:resource_id) { 46 }
105
+
106
+ it 'returns false' do
107
+ Timecop.freeze(date_before_expiration) do
108
+ result = remember_tfa_cookie.valid_cookie_data?(resource, cookie_data)
109
+ expect(result).to be(false)
110
+ end
111
+ end
112
+ end
113
+
114
+ describe 'when remember tfa token does not match' do
115
+ let(:remember_tfa_token) { '' }
116
+
117
+ it 'returns false' do
118
+ Timecop.freeze(date_before_expiration) do
119
+ result = remember_tfa_cookie.valid_cookie_data?(resource, cookie_data)
120
+ expect(result).to be(false)
121
+ end
122
+ end
123
+ end
124
+
125
+ describe 'when all cookie data matches' do
126
+ it 'returns true' do
127
+ Timecop.freeze(date_before_expiration) do
128
+ result = remember_tfa_cookie.valid_cookie_data?(resource, cookie_data)
129
+ expect(result).to be(true)
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devise-multi-factor
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.8
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitrii Golub
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-02-25 00:00:00.000000000 Z
12
+ date: 2022-01-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -220,6 +220,7 @@ files:
220
220
  - lib/devise_multi_factor/models/two_factor_authenticatable.rb
221
221
  - lib/devise_multi_factor/orm/active_record.rb
222
222
  - lib/devise_multi_factor/rails.rb
223
+ - lib/devise_multi_factor/remember_tfa_cookie.rb
223
224
  - lib/devise_multi_factor/routes.rb
224
225
  - lib/devise_multi_factor/schema.rb
225
226
  - lib/devise_multi_factor/version.rb
@@ -230,6 +231,7 @@ files:
230
231
  - spec/features/two_factor_authenticatable_spec.rb
231
232
  - spec/generators/active_record/devise_multi_factor_generator_spec.rb
232
233
  - spec/lib/devise_multi_factor/models/two_factor_authenticatable_spec.rb
234
+ - spec/lib/devise_multi_factor/tfa_remember_cookie_spec.rb
233
235
  - spec/rails_app/.gitignore
234
236
  - spec/rails_app/README.md
235
237
  - spec/rails_app/Rakefile
@@ -308,7 +310,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
308
310
  - !ruby/object:Gem::Version
309
311
  version: '0'
310
312
  requirements: []
311
- rubygems_version: 3.0.3
313
+ rubygems_version: 3.0.3.1
312
314
  signing_key:
313
315
  specification_version: 4
314
316
  summary: Two factor authentication plugin for devise