devise-two-factor 6.2.0 → 6.3.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: b6893a24ebfb4eae935305d8b0657383c1630eb7619e23dfcd9ad0190bf1cd64
4
- data.tar.gz: 24bab113bff7ae25afd03f9230caf372f8bdb0c319eeb18ab469353ca5136de1
3
+ metadata.gz: 4513e96354e689136149f21dbd447785cc74113a66725bed6153e44758a1709b
4
+ data.tar.gz: 9e83133ebdc4166577129276188688780aeec065a5fead7861073496210215e9
5
5
  SHA512:
6
- metadata.gz: 0b78eedcbafc38b967fb8a6717b9f70f87751b234ce905f5b4d99a176224e4927bc7039a69bb454daba16c8b07b368dc1d9a89e19016910733fd5f744f857a7c
7
- data.tar.gz: '0199a55f34cc5b67375620c1acabbca8b7256ead8b5401a6917dc2589b64158080851c9d5a061bc7fba1b95d36034f4e24aa13f1efb802a9cf44d1e202f407a0'
6
+ metadata.gz: a119f4f3f825409f6cea77f934fe7e35fa741dfb1b242c33a59dd523eb43ef2270b3dda80d1403f4baf7ade03271bbd4b145d21c6c390eeb223e00f0aadb6d29
7
+ data.tar.gz: 0f31866cb9c1523ff94095d2b43cf819d2a06a658a2dad94383d93e4c6c47d9e791092090cf8063f351c7727df6f2146e4bf22bf5a204c0b1f80aaf8a4fec3fb
@@ -26,7 +26,7 @@ jobs:
26
26
  env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
27
27
  BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/rails_${{ matrix.rails }}.gemfile
28
28
  steps:
29
- - uses: actions/checkout@v5
29
+ - uses: actions/checkout@v6
30
30
  - name: Set up Ruby
31
31
  uses: ruby/setup-ruby@v1
32
32
  with:
@@ -17,7 +17,7 @@ jobs:
17
17
 
18
18
  steps:
19
19
  # Set up
20
- - uses: actions/checkout@v5
20
+ - uses: actions/checkout@v6
21
21
  - name: Set up Ruby
22
22
  uses: ruby/setup-ruby@v1
23
23
  with:
data/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 6.3.0
6
+
7
+ - Fixed timing to be consistent when Devise paranoid mode is active.
8
+
5
9
  ## 6.2.0
6
10
 
7
11
  - Rails 8.1 support
data/README.md CHANGED
@@ -233,7 +233,7 @@ class AddDeviseTwoFactorBackupableToUsers < ActiveRecord::Migration
233
233
  end
234
234
  ```
235
235
 
236
- #### MySQL
236
+ #### MySQL, SQL Server, other databases without an array string type
237
237
 
238
238
  ```ruby
239
239
  # migration
@@ -17,7 +17,7 @@ Gem::Specification.new do |s|
17
17
 
18
18
  s.add_runtime_dependency 'railties', '>= 7.0', '< 8.2'
19
19
  s.add_runtime_dependency 'activesupport', '>= 7.0', '< 8.2'
20
- s.add_runtime_dependency 'devise', '~> 4.0'
20
+ s.add_runtime_dependency 'devise', '>= 4.0', '< 5.0'
21
21
  s.add_runtime_dependency 'rotp', '~> 6.0'
22
22
 
23
23
  s.add_development_dependency 'activemodel'
@@ -34,6 +34,9 @@ module Devise
34
34
  def invalidate_otp_backup_code!(code)
35
35
  codes = self.otp_backup_codes || []
36
36
 
37
+ # Should we still have some other kind of non iterable result, terminate.
38
+ raise TypeError.new("`otp_backup_codes` is expected to be an Array, got #{codes.class.name}. Hint: If your database does not support arrays, does your model correctly `serialize :otp_backup_codes, Array`?") unless codes.is_a?(Array)
39
+
37
40
  codes.each do |backup_code|
38
41
  next unless Devise::Encryptor.compare(self.class, backup_code, code)
39
42
 
@@ -49,38 +49,66 @@ RSpec.shared_examples 'two_factor_backupable' do
49
49
  end
50
50
 
51
51
  describe '#invalidate_otp_backup_code!' do
52
- before do
53
- @plaintext_codes = subject.generate_otp_backup_codes!
54
- end
55
52
 
56
- context 'given an invalid recovery code' do
57
- it 'returns false' do
58
- expect(subject.invalidate_otp_backup_code!('password')).to be false
59
- end
60
- end
61
53
 
62
- context 'given a valid recovery code' do
63
- it 'returns true' do
64
- @plaintext_codes.each do |code|
65
- expect(subject.invalidate_otp_backup_code!(code)).to be true
54
+ describe "#invalidate_otp_backup_code!" do
55
+ context "with no backup codes" do
56
+ it "does nothing" do
57
+ expect(subject.invalidate_otp_backup_code!("foo")).to be false
66
58
  end
67
59
  end
68
60
 
69
- it 'invalidates that recovery code' do
70
- code = @plaintext_codes.sample
61
+ context "with an array of backup codes, newly generated" do
62
+ before do
63
+ @plaintext_codes = subject.generate_otp_backup_codes!
64
+ end
65
+
66
+ context 'given an invalid recovery code' do
67
+ it 'returns false' do
68
+ expect(subject.invalidate_otp_backup_code!('password')).to be false
69
+ end
70
+ end
71
+
72
+ context 'given a valid recovery code' do
73
+ it 'returns true' do
74
+ @plaintext_codes.each do |code|
75
+ expect(subject.invalidate_otp_backup_code!(code)).to be true
76
+ end
77
+ end
71
78
 
72
- subject.invalidate_otp_backup_code!(code)
73
- expect(subject.invalidate_otp_backup_code!(code)).to be false
79
+ it 'invalidates that recovery code' do
80
+ code = @plaintext_codes.sample
81
+
82
+ subject.invalidate_otp_backup_code!(code)
83
+ expect(subject.invalidate_otp_backup_code!(code)).to be false
84
+ end
85
+
86
+ it 'does not invalidate the other recovery codes' do
87
+ code = @plaintext_codes.sample
88
+ subject.invalidate_otp_backup_code!(code)
89
+
90
+ @plaintext_codes.delete(code)
91
+
92
+ @plaintext_codes.each do |code|
93
+ expect(subject.invalidate_otp_backup_code!(code)).to be true
94
+ end
95
+ end
96
+ end
74
97
  end
75
98
 
76
- it 'does not invalidate the other recovery codes' do
77
- code = @plaintext_codes.sample
78
- subject.invalidate_otp_backup_code!(code)
99
+ context "with backup codes as a string" do
100
+ before do
101
+ @plaintext_codes = subject.generate_otp_backup_codes!
79
102
 
80
- @plaintext_codes.delete(code)
103
+ # Simulates database adapters that don't understand `t.string :otp_backup_codes, type: array` properly
104
+ # such as SQL Server; and have just returned the serialized string still.
105
+ # and the user not having done:
106
+ # `serialize :otp_backup_codes, Array` in their model
107
+ subject.otp_backup_codes = subject.otp_backup_codes.to_json
108
+ end
81
109
 
82
- @plaintext_codes.each do |code|
83
- expect(subject.invalidate_otp_backup_code!(code)).to be true
110
+ it "raises a meaningful error" do
111
+ expect { subject.invalidate_otp_backup_code!("flork") }.to raise_error(TypeError)
84
112
  end
85
113
  end
86
114
  end
@@ -4,12 +4,18 @@ module Devise
4
4
 
5
5
  def authenticate!
6
6
  resource = mapping.to.find_for_database_authentication(authentication_hash)
7
+
8
+ hashed = false
7
9
  # We authenticate in two cases:
8
10
  # 1. The password and the OTP are correct
9
11
  # 2. The password is correct, and OTP is not required for login
10
12
  # We check the OTP, then defer to DatabaseAuthenticatable
11
- if validate(resource) { validate_otp(resource) }
13
+ if validate(resource) { hashed = true; validate_otp(resource) }
12
14
  super
15
+ else
16
+ # Paranoid mode: do the expensive hash even when resource is nil,
17
+ # to avoid timing-based user enumeration.
18
+ mapping.to.new.password = password if !hashed && Devise.paranoid
13
19
  end
14
20
 
15
21
  fail(Devise.paranoid ? :invalid : :not_found_in_database) unless resource
@@ -1,3 +1,3 @@
1
1
  module DeviseTwoFactor
2
- VERSION = '6.2.0'.freeze
2
+ VERSION = '6.3.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devise-two-factor
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.2.0
4
+ version: 6.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Quinn Wilton
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-10-22 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: railties
@@ -53,16 +53,22 @@ dependencies:
53
53
  name: devise
54
54
  requirement: !ruby/object:Gem::Requirement
55
55
  requirements:
56
- - - "~>"
56
+ - - ">="
57
57
  - !ruby/object:Gem::Version
58
58
  version: '4.0'
59
+ - - "<"
60
+ - !ruby/object:Gem::Version
61
+ version: '5.0'
59
62
  type: :runtime
60
63
  prerelease: false
61
64
  version_requirements: !ruby/object:Gem::Requirement
62
65
  requirements:
63
- - - "~>"
66
+ - - ">="
64
67
  - !ruby/object:Gem::Version
65
68
  version: '4.0'
69
+ - - "<"
70
+ - !ruby/object:Gem::Version
71
+ version: '5.0'
66
72
  - !ruby/object:Gem::Dependency
67
73
  name: rotp
68
74
  requirement: !ruby/object:Gem::Requirement
@@ -221,7 +227,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
221
227
  - !ruby/object:Gem::Version
222
228
  version: '0'
223
229
  requirements: []
224
- rubygems_version: 3.6.2
230
+ rubygems_version: 4.0.3
225
231
  specification_version: 4
226
232
  summary: Barebones two-factor authentication with Devise
227
233
  test_files: