devise-multi-factor 3.1.5 → 3.2.0

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: ed4e43d631f791265fbe4849b093753d4905e605a959fce1af1cfe1ddbbe01ca
4
- data.tar.gz: 3e1c5ff5e1b8612cb73c675641303f297596fa2d90c1aa028c99e6f99f9383b4
3
+ metadata.gz: b69e7dd9a398b8cfebfd6c3b1e17e4c94603f2097e56a54f78a5f4ce07ce9751
4
+ data.tar.gz: 76f657bc840dc7b9b3d48fb3fe66876194ba4481e6849aac1b8f28b3a22eae52
5
5
  SHA512:
6
- metadata.gz: cc45d1846a2191ae1ccc52faf8b0bf124b828eed2b5df3f25ceb2dddfd9a49dc1a65329bbf8bfef5245aca9f15be5647e6f15398aa3286901237f344505847dc
7
- data.tar.gz: 0b29c86fbc815d956bf0d6c2f40c9d11b194942850ab933a12c479af6dff9aa01fd4579025470c3f57d73a4129f53b8d24aaaf398302405d62ee22b51711c9ec
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
@@ -8,7 +8,7 @@ class Devise::TwoFactorAuthenticationController < DeviseController
8
8
  end
9
9
 
10
10
  def update
11
- render :show and return if params[:code].nil?
11
+ render(:show, status: :unprocessable_entity) and return if params[:code].nil?
12
12
 
13
13
  if resource.authenticate_otp(params[:code])
14
14
  after_two_factor_success_for(resource)
@@ -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."
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
7
7
  s.version = DeviseMultiFactor::VERSION.dup
8
8
  s.authors = ["Dmitrii Golub", "Alex Santos"]
9
9
  s.email = ["hello@alexcsantos.com"]
10
- s.homepage = "https://github.com/Colex/devise_multi_factor"
10
+ s.homepage = "https://github.com/Colex/devise-multi-factor"
11
11
  s.summary = %q{Two factor authentication plugin for devise}
12
12
  s.description = <<-EOF
13
13
  ### Features ###
@@ -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.5".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.5
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-22 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
@@ -290,7 +292,7 @@ files:
290
292
  - spec/support/features_spec_helper.rb
291
293
  - spec/support/sms_provider.rb
292
294
  - spec/support/totp_helper.rb
293
- homepage: https://github.com/Colex/devise_multi_factor
295
+ homepage: https://github.com/Colex/devise-multi-factor
294
296
  licenses: []
295
297
  metadata: {}
296
298
  post_install_message:
@@ -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