devise-multi-factor 3.1.7 → 3.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55efbeee85d8da71dcb8389830e9ff26db36a4bcf112c442e2c7d858ef335f33
4
- data.tar.gz: 07ad7344eaa79e42a4cc1679e28841bc60d0c4a39aecc9d86516098a204260b3
3
+ metadata.gz: b790306376953e31fbc613105e7e581d75c2d001b9b7596fa1512284fac3817a
4
+ data.tar.gz: 117bf20fb42c0d70e9f6f5ee926b2bb8a8d64e91803c14d9d47b1565b99829d6
5
5
  SHA512:
6
- metadata.gz: 7bb5bab0057cefa4f6392e517a4daa529d4eb0a77973f662c7500b904cf481d0258a3b39f6946932a2e15a37f2b199210378f3c62867667f8aa3c5ca689f03e7
7
- data.tar.gz: 6f2c0e405d18503be62192f06ce15efa6689c2b161b5604bfc779ae7b50a83a0c828035a918f250abb73804ff177c266c393b36f9c054ce6068992eff849eb61
6
+ metadata.gz: edc39ac3f19009b58e2c6ec607421eca50bf7346d06c04c5b37982c88cdd41e39b195f5af7443fed7d5df34608b0481f0eb1d9adc58db1b17568394f7adb6986
7
+ data.tar.gz: 63c0022150927b92e23240bf9d1b0fb941e9871f593074a3431c713a94ed78c07a33f34aef4769716cb81d3e0306557cae15d74aa46a40341044ae2aab1b4665
@@ -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
@@ -15,7 +15,7 @@ module Devise
15
15
  encrypted_attribute: 'encrypted_otp_secret_key',
16
16
  }.compact
17
17
  encrypt_options = encrypt_options.merge(options[:encrypt]) if options[:encrypt].is_a?(Hash)
18
- encrypts(:otp_secret_key, encrypt_options || {})
18
+ has_encrypted(:otp_secret_key, **(encrypt_options || {}))
19
19
  end
20
20
 
21
21
  def generate_totp_secret
@@ -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.7".freeze
2
+ VERSION = "3.2.1".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.7
4
+ version: 3.2.1
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-09-08 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