devise-two-factor 5.0.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 +4 -4
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +10 -3
- data/.github/workflows/push.yml +28 -0
- data/.markdownlint.json +6 -0
- data/Appraisals +15 -30
- data/CHANGELOG.md +33 -0
- data/README.md +79 -37
- data/Rakefile +2 -0
- data/SECURITY.md +5 -0
- data/UPGRADING.md +38 -15
- data/devise-two-factor.gemspec +8 -14
- data/gemfiles/rails_7.0.gemfile +2 -2
- data/gemfiles/{rails_4.2.gemfile → rails_7.1.gemfile} +2 -2
- data/gemfiles/{rails_5.0.gemfile → rails_7.2.gemfile} +2 -2
- data/gemfiles/{rails_5.1.gemfile → rails_8.0.gemfile} +2 -2
- data/gemfiles/{rails_4.1.gemfile → rails_8.1.gemfile} +2 -2
- data/lib/devise-two-factor.rb +7 -4
- data/lib/devise_two_factor/models/two_factor_authenticatable.rb +13 -14
- data/lib/devise_two_factor/models/two_factor_backupable.rb +6 -2
- data/lib/devise_two_factor/spec_helpers/two_factor_authenticatable_shared_examples.rb +5 -5
- data/lib/devise_two_factor/spec_helpers/two_factor_backupable_shared_examples.rb +52 -24
- data/lib/devise_two_factor/strategies/two_factor_authenticatable.rb +8 -2
- data/lib/devise_two_factor/strategies/two_factor_backupable.rb +6 -4
- data/lib/devise_two_factor/version.rb +1 -1
- data/spec/devise/models/two_factor_authenticatable_spec.rb +5 -1
- data/spec/devise/models/two_factor_backupable_spec.rb +4 -0
- data/spec/spec_helper.rb +0 -1
- metadata +45 -109
- checksums.yaml.gz.sig +0 -0
- data/certs/tinfoil-cacert.pem +0 -41
- data/certs/tinfoilsecurity-gems-cert.pem +0 -35
- data/gemfiles/rails_5.2.gemfile +0 -8
- data/gemfiles/rails_6.0.gemfile +0 -8
- data/gemfiles/rails_6.1.gemfile +0 -8
- data.tar.gz.sig +0 -0
- metadata.gz.sig +0 -0
data/lib/devise-two-factor.rb
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
require 'logger'
|
|
1
2
|
require 'devise'
|
|
2
3
|
require 'devise_two_factor/models'
|
|
3
4
|
require 'devise_two_factor/strategies'
|
|
4
5
|
|
|
5
6
|
module Devise
|
|
6
|
-
# The length of generated OTP
|
|
7
|
+
# The length of randomly generated OTP shared secret (in bytes).
|
|
8
|
+
# The secrets will be base32-encoded and have a length 1.6 times the configured value.
|
|
7
9
|
mattr_accessor :otp_secret_length
|
|
8
|
-
@@otp_secret_length =
|
|
10
|
+
@@otp_secret_length = 20
|
|
9
11
|
|
|
10
12
|
# The number of seconds before and after the current
|
|
11
13
|
# time for which codes will be accepted
|
|
@@ -20,7 +22,8 @@ module Devise
|
|
|
20
22
|
mattr_accessor :otp_encrypted_attribute_options
|
|
21
23
|
@@otp_encrypted_attribute_options = {}
|
|
22
24
|
|
|
23
|
-
# The length of
|
|
25
|
+
# The length of randomly generated OTP backup codes (in bytes).
|
|
26
|
+
# The codes will be hex-encoded and have a length twice the configured value.
|
|
24
27
|
mattr_accessor :otp_backup_code_length
|
|
25
28
|
@@otp_backup_code_length = 16
|
|
26
29
|
|
|
@@ -31,7 +34,7 @@ module Devise
|
|
|
31
34
|
end
|
|
32
35
|
|
|
33
36
|
Devise.add_module(:two_factor_authenticatable, :route => :session, :strategy => true,
|
|
34
|
-
:controller => :sessions, :model => true)
|
|
37
|
+
:controller => :sessions, :model => true, :insert_at => 0)
|
|
35
38
|
|
|
36
39
|
Devise.add_module(:two_factor_backupable, :route => :session, :strategy => true,
|
|
37
40
|
:controller => :sessions, :model => true)
|
|
@@ -41,12 +41,11 @@ module Devise
|
|
|
41
41
|
|
|
42
42
|
if self.consumed_timestep
|
|
43
43
|
# reconstruct the timestamp of the last consumed timestep
|
|
44
|
-
after_timestamp = self.consumed_timestep *
|
|
44
|
+
after_timestamp = self.consumed_timestep * totp.interval
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
end
|
|
47
|
+
timestamp = totp.verify(code.gsub(/\s+/, ""), drift_behind: self.class.otp_allowed_drift, drift_ahead: self.class.otp_allowed_drift, after: after_timestamp)
|
|
48
|
+
return consume_otp!(totp, timestamp) if timestamp
|
|
50
49
|
|
|
51
50
|
false
|
|
52
51
|
end
|
|
@@ -59,11 +58,6 @@ module Devise
|
|
|
59
58
|
otp.at(Time.now)
|
|
60
59
|
end
|
|
61
60
|
|
|
62
|
-
# ROTP's TOTP#timecode is private, so we duplicate it here
|
|
63
|
-
def current_otp_timestep
|
|
64
|
-
Time.now.utc.to_i / otp.interval
|
|
65
|
-
end
|
|
66
|
-
|
|
67
61
|
def otp_provisioning_uri(account, options = {})
|
|
68
62
|
otp_secret = options[:otp_secret] || self.otp_secret
|
|
69
63
|
ROTP::TOTP.new(otp_secret, options).provisioning_uri(account)
|
|
@@ -78,10 +72,14 @@ module Devise
|
|
|
78
72
|
|
|
79
73
|
# An OTP cannot be used more than once in a given timestep
|
|
80
74
|
# Storing timestep of last valid OTP is sufficient to satisfy this requirement
|
|
81
|
-
def consume_otp!
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
75
|
+
def consume_otp!(otp, timestamp)
|
|
76
|
+
timestep = timestamp / otp.interval
|
|
77
|
+
|
|
78
|
+
if self.consumed_timestep != timestep
|
|
79
|
+
self.consumed_timestep = timestep
|
|
80
|
+
save!(validate: false)
|
|
81
|
+
|
|
82
|
+
return true
|
|
85
83
|
end
|
|
86
84
|
|
|
87
85
|
false
|
|
@@ -93,8 +91,9 @@ module Devise
|
|
|
93
91
|
:otp_encrypted_attribute_options,
|
|
94
92
|
:otp_secret_encryption_key)
|
|
95
93
|
|
|
94
|
+
# Geneartes an OTP secret of the specified length, returning it after Base32 encoding.
|
|
96
95
|
def generate_otp_secret(otp_secret_length = self.otp_secret_length)
|
|
97
|
-
ROTP::Base32.
|
|
96
|
+
ROTP::Base32.random(otp_secret_length)
|
|
98
97
|
end
|
|
99
98
|
|
|
100
99
|
# Return value will be splatted with ** so return a version of the
|
|
@@ -20,7 +20,7 @@ module Devise
|
|
|
20
20
|
code_length = self.class.otp_backup_code_length
|
|
21
21
|
|
|
22
22
|
number_of_codes.times do
|
|
23
|
-
codes << SecureRandom.hex(code_length
|
|
23
|
+
codes << SecureRandom.hex(code_length)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
hashed_codes = codes.map { |code| Devise::Encryptor.digest(self.class, code) }
|
|
@@ -30,15 +30,19 @@ module Devise
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
# Returns true and invalidates the given code
|
|
33
|
-
#
|
|
33
|
+
# if that code is a valid backup code.
|
|
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
|
|
|
40
43
|
codes.delete(backup_code)
|
|
41
44
|
self.otp_backup_codes = codes
|
|
45
|
+
save!(validate: false)
|
|
42
46
|
return true
|
|
43
47
|
end
|
|
44
48
|
|
|
@@ -13,8 +13,8 @@ RSpec.shared_examples 'two_factor_authenticatable' do
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
describe '#otp_secret' do
|
|
16
|
-
it 'should be of the
|
|
17
|
-
expect(subject.otp_secret.length).to eq(subject.class.otp_secret_length)
|
|
16
|
+
it 'should be of the expected length' do
|
|
17
|
+
expect(subject.otp_secret.length).to eq(subject.class.otp_secret_length*8/5)
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
|
|
@@ -125,15 +125,15 @@ RSpec.shared_examples 'two_factor_authenticatable' do
|
|
|
125
125
|
|
|
126
126
|
describe '#otp_provisioning_uri' do
|
|
127
127
|
let(:otp_secret_length) { subject.class.otp_secret_length }
|
|
128
|
-
let(:account) {
|
|
128
|
+
let(:account) { 'user@host.example' }
|
|
129
129
|
let(:issuer) { 'Tinfoil' }
|
|
130
130
|
|
|
131
131
|
it 'should return uri with specified account' do
|
|
132
|
-
expect(subject.otp_provisioning_uri(account)).to match(%r{otpauth://totp/#{CGI.escape(account)}\?secret=\w{#{otp_secret_length}}})
|
|
132
|
+
expect(subject.otp_provisioning_uri(account)).to match(%r{otpauth://totp/#{CGI.escape(account)}\?secret=\w{#{otp_secret_length*8/5}}})
|
|
133
133
|
end
|
|
134
134
|
|
|
135
135
|
it 'should return uri with issuer option' do
|
|
136
|
-
expect(subject.otp_provisioning_uri(account, issuer: issuer)).to match(%r{otpauth://totp/#{issuer}:#{CGI.escape(account)}\?.*secret=\w{#{otp_secret_length}}(&|$)})
|
|
136
|
+
expect(subject.otp_provisioning_uri(account, issuer: issuer)).to match(%r{otpauth://totp/#{issuer}:#{CGI.escape(account)}\?.*secret=\w{#{otp_secret_length*8/5}}(&|$)})
|
|
137
137
|
expect(subject.otp_provisioning_uri(account, issuer: issuer)).to match(%r{otpauth://totp/#{issuer}:#{CGI.escape(account)}\?.*issuer=#{issuer}(&|$)})
|
|
138
138
|
end
|
|
139
139
|
end
|
|
@@ -17,7 +17,7 @@ RSpec.shared_examples 'two_factor_backupable' do
|
|
|
17
17
|
|
|
18
18
|
it 'generates recovery codes of the correct length' do
|
|
19
19
|
@plaintext_codes.each do |code|
|
|
20
|
-
expect(code.length).to eq(subject.class.otp_backup_code_length)
|
|
20
|
+
expect(code.length).to eq(subject.class.otp_backup_code_length*2)
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
@@ -34,7 +34,7 @@ RSpec.shared_examples 'two_factor_backupable' do
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
context 'with existing recovery codes' do
|
|
37
|
-
let(:old_codes) {
|
|
37
|
+
let(:old_codes) { ['adam', 'betty', 'charles'] }
|
|
38
38
|
let(:old_codes_hashed) { old_codes.map { |x| Devise::Encryptor.digest(subject.class, x) } }
|
|
39
39
|
|
|
40
40
|
before do
|
|
@@ -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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
expect(subject.invalidate_otp_backup_code!(
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
99
|
+
context "with backup codes as a string" do
|
|
100
|
+
before do
|
|
101
|
+
@plaintext_codes = subject.generate_otp_backup_codes!
|
|
79
102
|
|
|
80
|
-
|
|
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
|
-
|
|
83
|
-
expect
|
|
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
|
|
@@ -21,7 +27,7 @@ module Devise
|
|
|
21
27
|
|
|
22
28
|
def validate_otp(resource)
|
|
23
29
|
return true unless resource.otp_required_for_login
|
|
24
|
-
return if params[scope]['otp_attempt'].nil?
|
|
30
|
+
return if params[scope].nil? || params[scope]['otp_attempt'].nil?
|
|
25
31
|
resource.validate_and_consume_otp!(params[scope]['otp_attempt'])
|
|
26
32
|
end
|
|
27
33
|
end
|
|
@@ -5,10 +5,7 @@ module Devise
|
|
|
5
5
|
def authenticate!
|
|
6
6
|
resource = mapping.to.find_for_database_authentication(authentication_hash)
|
|
7
7
|
|
|
8
|
-
if validate(resource) { resource
|
|
9
|
-
# Devise fails to authenticate invalidated resources, but if we've
|
|
10
|
-
# gotten here, the object changed (Since we deleted a recovery code)
|
|
11
|
-
resource.save!
|
|
8
|
+
if validate(resource) { validate_backup_code(resource) }
|
|
12
9
|
super
|
|
13
10
|
end
|
|
14
11
|
|
|
@@ -18,6 +15,11 @@ module Devise
|
|
|
18
15
|
# but database authenticatable automatically halts on a bad password
|
|
19
16
|
@halted = false if @result == :failure
|
|
20
17
|
end
|
|
18
|
+
|
|
19
|
+
def validate_backup_code(resource)
|
|
20
|
+
return if params[scope].nil? || params[scope]['otp_attempt'].nil?
|
|
21
|
+
resource.invalidate_otp_backup_code!(params[scope]['otp_attempt'])
|
|
22
|
+
end
|
|
21
23
|
end
|
|
22
24
|
end
|
|
23
25
|
end
|
|
@@ -18,13 +18,17 @@ class TwoFactorAuthenticatableDouble
|
|
|
18
18
|
|
|
19
19
|
attr_accessor :consumed_timestep
|
|
20
20
|
|
|
21
|
-
def save(
|
|
21
|
+
def save!(_)
|
|
22
22
|
# noop for testing
|
|
23
23
|
true
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
describe ::Devise::Models::TwoFactorAuthenticatable do
|
|
28
|
+
it 'should be inserted prior to other devise modules' do
|
|
29
|
+
expect(Devise::ALL.first).to eq(:two_factor_authenticatable)
|
|
30
|
+
end
|
|
31
|
+
|
|
28
32
|
context 'When included in a class' do
|
|
29
33
|
subject { TwoFactorAuthenticatableDouble.new }
|
|
30
34
|
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
|
@@ -1,135 +1,74 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: devise-two-factor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 6.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
8
|
-
autorequire:
|
|
7
|
+
- Quinn Wilton
|
|
9
8
|
bindir: bin
|
|
10
|
-
cert_chain:
|
|
11
|
-
-
|
|
12
|
-
-----BEGIN CERTIFICATE-----
|
|
13
|
-
MIIHSjCCBTKgAwIBAgIJAK2u0LojMCNgMA0GCSqGSIb3DQEBBQUAMIGcMQswCQYD
|
|
14
|
-
VQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVBhbG8gQWx0bzEfMB0GA1UE
|
|
15
|
-
ChMWVGluZm9pbCBTZWN1cml0eSwgSW5jLjEfMB0GA1UEAxMWVGluZm9pbCBTZWN1
|
|
16
|
-
cml0eSwgSW5jLjEqMCgGCSqGSIb3DQEJARYbc3VwcG9ydEB0aW5mb2lsc2VjdXJp
|
|
17
|
-
dHkuY29tMB4XDTIxMDkwOTE4MjIwMFoXDTI1MDkwOTE4MjIwMFowgZwxCzAJBgNV
|
|
18
|
-
BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJUGFsbyBBbHRvMR8wHQYDVQQK
|
|
19
|
-
ExZUaW5mb2lsIFNlY3VyaXR5LCBJbmMuMR8wHQYDVQQDExZUaW5mb2lsIFNlY3Vy
|
|
20
|
-
aXR5LCBJbmMuMSowKAYJKoZIhvcNAQkBFhtzdXBwb3J0QHRpbmZvaWxzZWN1cml0
|
|
21
|
-
eS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCqbHvsSj0H0FB1
|
|
22
|
-
0gLYoDK1BKugkSB2DZeZZHP6B1UdWRahJXJP9oT1lhfQxx8iX4cgEi7JU3NqA6NR
|
|
23
|
-
cIRFQ50eH/qlmgs7909gaf8pDaeC0vR3wd0GeRg6qr1eDEnkzIyr/D1AMiX6H1eP
|
|
24
|
-
Y7J3SfrdaL3gft2iPRKGkgqsXR7oBNLA3n/ShiNgPXqRDl1CCj6aMY0cn5ROFScz
|
|
25
|
-
vT2FUB4DEwPD2l18m1p99OnXqsOLL2J65qA2+cI8FtgFmlwIi5oSf+URvIdNx+cH
|
|
26
|
-
lInlAtVHCvAKYLY0dlQ7czMQBcRpYjp2rwPt9f2ksq9b/voMTBABYHFV+IVn8svv
|
|
27
|
-
GZ5e1+icjtr/R7dCGmCdEdFLXVxafmZhukymG9USv9DKuv1qh7r4q8KaPIE8n7nQ
|
|
28
|
-
m97jENFfsgnwv+nUmIJ3tzuW5ZxO7A0tIIYdwzt0UjrO3ya4R5bTFXr4bnzZ/g/s
|
|
29
|
-
CLknWqg1BCRlPd6LnpVGPT0gNDV1pEO25wE3A3Yy0Ujxudcgay/CgUhnlU11qOAc
|
|
30
|
-
xmar2fhNZsviUhndd/220Ad5QMV2XzcAiopJIeu0juIVGRQM7x2h19Hsp0m6sOEF
|
|
31
|
-
jfhvbdUa4nvmIFeYFY+hr/YkTmG9ZjyBa8YaZXhwjhSmKCQ374J7mn5e0Cryuvi5
|
|
32
|
-
tYhwJn8rdwYZF/h2qqfEu8vaLoD09QIDAQABo4IBizCCAYcwHQYDVR0OBBYEFMmT
|
|
33
|
-
/x412UH+5OHqgleeTjLOv6iHMIHRBgNVHSMEgckwgcaAFMmT/x412UH+5OHqglee
|
|
34
|
-
TjLOv6iHoYGipIGfMIGcMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNV
|
|
35
|
-
BAcTCVBhbG8gQWx0bzEfMB0GA1UEChMWVGluZm9pbCBTZWN1cml0eSwgSW5jLjEf
|
|
36
|
-
MB0GA1UEAxMWVGluZm9pbCBTZWN1cml0eSwgSW5jLjEqMCgGCSqGSIb3DQEJARYb
|
|
37
|
-
c3VwcG9ydEB0aW5mb2lsc2VjdXJpdHkuY29tggkAra7QuiMwI2AwDwYDVR0TAQH/
|
|
38
|
-
BAUwAwEB/zARBglghkgBhvhCAQEEBAMCAQYwCQYDVR0SBAIwADArBglghkgBhvhC
|
|
39
|
-
AQ0EHhYcVGlueUNBIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAmBgNVHREEHzAdgRtz
|
|
40
|
-
dXBwb3J0QHRpbmZvaWxzZWN1cml0eS5jb20wDgYDVR0PAQH/BAQDAgEGMA0GCSqG
|
|
41
|
-
SIb3DQEBBQUAA4ICAQBZy4JJSmwLuO0nZbdr4tJeVS2P8bcGi6PzAcdzVfwzjp6n
|
|
42
|
-
5qf8m4O8my4lnJieom0GrWSHQoPY1Yur4hEoZbugKO9DWZL3dTiGcrgw0TbQ6Gtq
|
|
43
|
-
TTPatW3LA21qFJwvohSvLqPdmZuM+H9g49sdl2kNTDVI6iUyMYuNpL14aPKPGBFo
|
|
44
|
-
o7UjciT1h7JtJl9b/fXrbPeRHBwpZXWeipiPGv/OZW5KnOsNlUkTquS7Zj4ETkIC
|
|
45
|
-
6mVtmsLvq+YwFthFyMU37pXwYxcmqRmH6lX+XC6AVW5oO4GBmG+Zr/Z+h5Cih5ca
|
|
46
|
-
/mX88RkO+dGTjw1IdxKmxOqKL62OBATKrTDJ/scsmRptynA4TunYW+7ikOpDbPfL
|
|
47
|
-
l18aleLISlcgWJg/Czf2nmBqAClPLnhV8qxWsvt58MQQ/Jpoggvpl8EG1PylWiBS
|
|
48
|
-
Kc/4Ad/FKQFpTzXUgDg2kV07npVjYbBzA5p4ZSWSlflFu93jb9gg2+qtnRSImVCZ
|
|
49
|
-
nQjZdsv8hebElPAIbtJjSnoH1Kz2ucYLakdF1UMKnpp1PVREtuKPz/foU9KUHs0z
|
|
50
|
-
dWRALx8cWG4uKK9AIEUlVdGKfX0Wj0qFK0KGxl3f3jObud5Agwue2EPKWwUzEGUh
|
|
51
|
-
Iqp60gNw3vBdKHw4dh1bfcbXWnRDL+OQPuOFZeMWgu1QmeHeuggYtYtRg7V5Kg==
|
|
52
|
-
-----END CERTIFICATE-----
|
|
53
|
-
- |
|
|
54
|
-
-----BEGIN CERTIFICATE-----
|
|
55
|
-
MIIGADCCA+igAwIBAgIIHIF9ta6cW3YwDQYJKoZIhvcNAQENBQAwgZwxCzAJBgNV
|
|
56
|
-
BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJUGFsbyBBbHRvMR8wHQYDVQQK
|
|
57
|
-
ExZUaW5mb2lsIFNlY3VyaXR5LCBJbmMuMR8wHQYDVQQDExZUaW5mb2lsIFNlY3Vy
|
|
58
|
-
aXR5LCBJbmMuMSowKAYJKoZIhvcNAQkBFhtzdXBwb3J0QHRpbmZvaWxzZWN1cml0
|
|
59
|
-
eS5jb20wHhcNMjIwMzIyMjI1MzAwWhcNMjUwOTA5MTgyMjAwWjCBiDELMAkGA1UE
|
|
60
|
-
BhMCVVMxCzAJBgNVBAgTAkNBMR8wHQYDVQQKExZUaW5mb2lsIFNlY3VyaXR5LCBJ
|
|
61
|
-
bmMuMR0wGwYDVQQDExR0aW5mb2lsc2VjdXJpdHktZ2VtczEsMCoGCSqGSIb3DQEJ
|
|
62
|
-
ARYdZW5naW5lZXJzQHRpbmZvaWxzZWN1cml0eS5jb20wggIiMA0GCSqGSIb3DQEB
|
|
63
|
-
AQUAA4ICDwAwggIKAoICAQDNJYNH8D+8lACLt3KzjEIPs3XVBCPaMm2eD/Xk9OOT
|
|
64
|
-
uDV/NqgMK0icD9MRxMUtS3SCrC9QcPocXT76f2LQ3yVJuK+rBUasymEES47PIx2c
|
|
65
|
-
zC4n4Hga0xPPuBpioO26oaRFsobyzh9RPOIbnYfpjyqtdrbm+YyM3sPR4XzFirv9
|
|
66
|
-
xomT4E9T4RCLgOQHTcLKL9K9m+EN7PeVdVUXV0Pa7cVs2vJUKedsd7vnr6Lzbn8T
|
|
67
|
-
oPk/7J/4W931PbaeI5yg9ZuaRa9K2IaY1TkPI67NW4qKitBVepRlXw6Sb7TYcUnc
|
|
68
|
-
WEQ/eC5CpnOmqUrG5tfGD8cc5aGZOkitW/VXZgVj81xgCv1hk4HjErrqq4FBNAaC
|
|
69
|
-
SNyBfwR0TUYqg1lN1nbNjOKwfb6YRn06R2ovcFJG0tmGhsQULCr6fW8u2TfSM+U9
|
|
70
|
-
WFSIJx2griureY7EZPwg/MgsUiWUWMFemz3GVYXWJR3dN2pW9Uqr3rkjKZbA0bst
|
|
71
|
-
GWahJO9HuFdDakQxoaTPYPtTQDC+kskkO6lKG1KLIoZ1iLZzB1Ks1vEeyE7lp1im
|
|
72
|
-
WgpUq+q23PFkt1gIBi/4tGvzsLZye25QU2Y+XLzldCNm+DyRFXZ+Q+bK33IveUeU
|
|
73
|
-
WEOv4T1qTXHAOypyzmgodVRG/PrlsSMOBfE515kG1mDMGjRcCpEtlskgxUbf7qM7
|
|
74
|
-
hQIDAQABo1gwVjAJBgNVHRMEAjAAMEkGA1UdHwRCMEAwPqA8oDqGOGh0dHBzOi8v
|
|
75
|
-
d3d3LnRpbmZvaWxzZWN1cml0eS5jb20vc2VjdXJpdHkvcmV2b2NhdGlvbl9saXN0
|
|
76
|
-
MA0GCSqGSIb3DQEBDQUAA4ICAQAiYF/m2ny/mxFvBVxHfdYuzybhCvsEUd+TSnoe
|
|
77
|
-
mqOWntY3sxCOaY0aGOMB4vyg9G+oP/kT4m63sD4uQxeuU7WOjaG2smCSS5q+PSWS
|
|
78
|
-
v63gILqPamjSyP/Om864EA6YlvVQ7nPXhVDEaiBt3iliefJGmb0wWSbbDCmq3aMb
|
|
79
|
-
WTLuax/IeY6MjJi20LutIcuz+VX8OxlA1hSpgAToMz3xrhA8fPt5UkKhkDkPFYBF
|
|
80
|
-
5htKVipyijChWsXyt33YM2qGaavTEXzxza1I99PGNRKxUMvbSMas4YaLqkBpQSc+
|
|
81
|
-
mcrLWYPiXWsePGu+j08AypE2Ubp4AOSZJN9rBBGotC3gofipo+K/sBiOM9xXI76Q
|
|
82
|
-
0HYOxXPa7D7UQQG1R9i0rcxmf9qepIVYCldmqVkKKDizcDo5UI9lRiLFjDyQhn6l
|
|
83
|
-
YFY9bPQ4lKTK5Jr3M6+dV7fHxLhqXyMGs1905IUb7qvB7Bq/f0qJfC0JZuY/qdn2
|
|
84
|
-
lL0SeFKOVsjErtobh3u8p8j2USkc8uJgIANHpXEMEExdp899CV/eVjh3TpAR7E6T
|
|
85
|
-
mg7Q9Hi6Hh8z+Le9iR4I49vPEWDQEvj35IT6VfwU79UfIOlX+DkW8fFkPbaut3Se
|
|
86
|
-
vqIDv6JBG9I16h/HhchntKfM58MI1bNZFBSdZqYOJiL8JIjP8HNIk76Y366ppG29
|
|
87
|
-
EhBYYg==
|
|
88
|
-
-----END CERTIFICATE-----
|
|
89
|
-
date: 2022-07-11 00:00:00.000000000 Z
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
90
11
|
dependencies:
|
|
91
12
|
- !ruby/object:Gem::Dependency
|
|
92
13
|
name: railties
|
|
93
14
|
requirement: !ruby/object:Gem::Requirement
|
|
94
15
|
requirements:
|
|
95
|
-
- - "
|
|
16
|
+
- - ">="
|
|
96
17
|
- !ruby/object:Gem::Version
|
|
97
18
|
version: '7.0'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '8.2'
|
|
98
22
|
type: :runtime
|
|
99
23
|
prerelease: false
|
|
100
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
101
25
|
requirements:
|
|
102
|
-
- - "
|
|
26
|
+
- - ">="
|
|
103
27
|
- !ruby/object:Gem::Version
|
|
104
28
|
version: '7.0'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '8.2'
|
|
105
32
|
- !ruby/object:Gem::Dependency
|
|
106
33
|
name: activesupport
|
|
107
34
|
requirement: !ruby/object:Gem::Requirement
|
|
108
35
|
requirements:
|
|
109
|
-
- - "
|
|
36
|
+
- - ">="
|
|
110
37
|
- !ruby/object:Gem::Version
|
|
111
38
|
version: '7.0'
|
|
39
|
+
- - "<"
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '8.2'
|
|
112
42
|
type: :runtime
|
|
113
43
|
prerelease: false
|
|
114
44
|
version_requirements: !ruby/object:Gem::Requirement
|
|
115
45
|
requirements:
|
|
116
|
-
- - "
|
|
46
|
+
- - ">="
|
|
117
47
|
- !ruby/object:Gem::Version
|
|
118
48
|
version: '7.0'
|
|
49
|
+
- - "<"
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '8.2'
|
|
119
52
|
- !ruby/object:Gem::Dependency
|
|
120
53
|
name: devise
|
|
121
54
|
requirement: !ruby/object:Gem::Requirement
|
|
122
55
|
requirements:
|
|
123
|
-
- - "
|
|
56
|
+
- - ">="
|
|
124
57
|
- !ruby/object:Gem::Version
|
|
125
58
|
version: '4.0'
|
|
59
|
+
- - "<"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '5.0'
|
|
126
62
|
type: :runtime
|
|
127
63
|
prerelease: false
|
|
128
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
129
65
|
requirements:
|
|
130
|
-
- - "
|
|
66
|
+
- - ">="
|
|
131
67
|
- !ruby/object:Gem::Version
|
|
132
68
|
version: '4.0'
|
|
69
|
+
- - "<"
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '5.0'
|
|
133
72
|
- !ruby/object:Gem::Dependency
|
|
134
73
|
name: rotp
|
|
135
74
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -215,27 +154,30 @@ dependencies:
|
|
|
215
154
|
- !ruby/object:Gem::Version
|
|
216
155
|
version: '0'
|
|
217
156
|
- !ruby/object:Gem::Dependency
|
|
218
|
-
name:
|
|
157
|
+
name: rake
|
|
219
158
|
requirement: !ruby/object:Gem::Requirement
|
|
220
159
|
requirements:
|
|
221
|
-
- - "
|
|
160
|
+
- - "~>"
|
|
222
161
|
- !ruby/object:Gem::Version
|
|
223
|
-
version: '
|
|
162
|
+
version: '13'
|
|
224
163
|
type: :development
|
|
225
164
|
prerelease: false
|
|
226
165
|
version_requirements: !ruby/object:Gem::Requirement
|
|
227
166
|
requirements:
|
|
228
|
-
- - "
|
|
167
|
+
- - "~>"
|
|
229
168
|
- !ruby/object:Gem::Version
|
|
230
|
-
version: '
|
|
231
|
-
description:
|
|
232
|
-
|
|
169
|
+
version: '13'
|
|
170
|
+
description: Devise-Two-Factor is a minimalist extension to Devise which offers support
|
|
171
|
+
for two-factor authentication through the TOTP scheme.
|
|
233
172
|
executables: []
|
|
234
173
|
extensions: []
|
|
235
174
|
extra_rdoc_files: []
|
|
236
175
|
files:
|
|
176
|
+
- ".github/dependabot.yml"
|
|
237
177
|
- ".github/workflows/ci.yml"
|
|
178
|
+
- ".github/workflows/push.yml"
|
|
238
179
|
- ".gitignore"
|
|
180
|
+
- ".markdownlint.json"
|
|
239
181
|
- ".rspec"
|
|
240
182
|
- Appraisals
|
|
241
183
|
- CHANGELOG.md
|
|
@@ -244,18 +186,14 @@ files:
|
|
|
244
186
|
- LICENSE
|
|
245
187
|
- README.md
|
|
246
188
|
- Rakefile
|
|
189
|
+
- SECURITY.md
|
|
247
190
|
- UPGRADING.md
|
|
248
|
-
- certs/tinfoil-cacert.pem
|
|
249
|
-
- certs/tinfoilsecurity-gems-cert.pem
|
|
250
191
|
- devise-two-factor.gemspec
|
|
251
|
-
- gemfiles/rails_4.1.gemfile
|
|
252
|
-
- gemfiles/rails_4.2.gemfile
|
|
253
|
-
- gemfiles/rails_5.0.gemfile
|
|
254
|
-
- gemfiles/rails_5.1.gemfile
|
|
255
|
-
- gemfiles/rails_5.2.gemfile
|
|
256
|
-
- gemfiles/rails_6.0.gemfile
|
|
257
|
-
- gemfiles/rails_6.1.gemfile
|
|
258
192
|
- gemfiles/rails_7.0.gemfile
|
|
193
|
+
- gemfiles/rails_7.1.gemfile
|
|
194
|
+
- gemfiles/rails_7.2.gemfile
|
|
195
|
+
- gemfiles/rails_8.0.gemfile
|
|
196
|
+
- gemfiles/rails_8.1.gemfile
|
|
259
197
|
- lib/devise-two-factor.rb
|
|
260
198
|
- lib/devise_two_factor/models.rb
|
|
261
199
|
- lib/devise_two_factor/models/two_factor_authenticatable.rb
|
|
@@ -271,11 +209,10 @@ files:
|
|
|
271
209
|
- spec/devise/models/two_factor_authenticatable_spec.rb
|
|
272
210
|
- spec/devise/models/two_factor_backupable_spec.rb
|
|
273
211
|
- spec/spec_helper.rb
|
|
274
|
-
homepage: https://github.com/
|
|
212
|
+
homepage: https://github.com/devise-two-factor/devise-two-factor
|
|
275
213
|
licenses:
|
|
276
214
|
- MIT
|
|
277
215
|
metadata: {}
|
|
278
|
-
post_install_message:
|
|
279
216
|
rdoc_options: []
|
|
280
217
|
require_paths:
|
|
281
218
|
- lib
|
|
@@ -290,8 +227,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
290
227
|
- !ruby/object:Gem::Version
|
|
291
228
|
version: '0'
|
|
292
229
|
requirements: []
|
|
293
|
-
rubygems_version:
|
|
294
|
-
signing_key:
|
|
230
|
+
rubygems_version: 4.0.3
|
|
295
231
|
specification_version: 4
|
|
296
232
|
summary: Barebones two-factor authentication with Devise
|
|
297
233
|
test_files:
|
checksums.yaml.gz.sig
DELETED
|
Binary file
|
data/certs/tinfoil-cacert.pem
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
-----BEGIN CERTIFICATE-----
|
|
2
|
-
MIIHSjCCBTKgAwIBAgIJAK2u0LojMCNgMA0GCSqGSIb3DQEBBQUAMIGcMQswCQYD
|
|
3
|
-
VQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVBhbG8gQWx0bzEfMB0GA1UE
|
|
4
|
-
ChMWVGluZm9pbCBTZWN1cml0eSwgSW5jLjEfMB0GA1UEAxMWVGluZm9pbCBTZWN1
|
|
5
|
-
cml0eSwgSW5jLjEqMCgGCSqGSIb3DQEJARYbc3VwcG9ydEB0aW5mb2lsc2VjdXJp
|
|
6
|
-
dHkuY29tMB4XDTIxMDkwOTE4MjIwMFoXDTI1MDkwOTE4MjIwMFowgZwxCzAJBgNV
|
|
7
|
-
BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJUGFsbyBBbHRvMR8wHQYDVQQK
|
|
8
|
-
ExZUaW5mb2lsIFNlY3VyaXR5LCBJbmMuMR8wHQYDVQQDExZUaW5mb2lsIFNlY3Vy
|
|
9
|
-
aXR5LCBJbmMuMSowKAYJKoZIhvcNAQkBFhtzdXBwb3J0QHRpbmZvaWxzZWN1cml0
|
|
10
|
-
eS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCqbHvsSj0H0FB1
|
|
11
|
-
0gLYoDK1BKugkSB2DZeZZHP6B1UdWRahJXJP9oT1lhfQxx8iX4cgEi7JU3NqA6NR
|
|
12
|
-
cIRFQ50eH/qlmgs7909gaf8pDaeC0vR3wd0GeRg6qr1eDEnkzIyr/D1AMiX6H1eP
|
|
13
|
-
Y7J3SfrdaL3gft2iPRKGkgqsXR7oBNLA3n/ShiNgPXqRDl1CCj6aMY0cn5ROFScz
|
|
14
|
-
vT2FUB4DEwPD2l18m1p99OnXqsOLL2J65qA2+cI8FtgFmlwIi5oSf+URvIdNx+cH
|
|
15
|
-
lInlAtVHCvAKYLY0dlQ7czMQBcRpYjp2rwPt9f2ksq9b/voMTBABYHFV+IVn8svv
|
|
16
|
-
GZ5e1+icjtr/R7dCGmCdEdFLXVxafmZhukymG9USv9DKuv1qh7r4q8KaPIE8n7nQ
|
|
17
|
-
m97jENFfsgnwv+nUmIJ3tzuW5ZxO7A0tIIYdwzt0UjrO3ya4R5bTFXr4bnzZ/g/s
|
|
18
|
-
CLknWqg1BCRlPd6LnpVGPT0gNDV1pEO25wE3A3Yy0Ujxudcgay/CgUhnlU11qOAc
|
|
19
|
-
xmar2fhNZsviUhndd/220Ad5QMV2XzcAiopJIeu0juIVGRQM7x2h19Hsp0m6sOEF
|
|
20
|
-
jfhvbdUa4nvmIFeYFY+hr/YkTmG9ZjyBa8YaZXhwjhSmKCQ374J7mn5e0Cryuvi5
|
|
21
|
-
tYhwJn8rdwYZF/h2qqfEu8vaLoD09QIDAQABo4IBizCCAYcwHQYDVR0OBBYEFMmT
|
|
22
|
-
/x412UH+5OHqgleeTjLOv6iHMIHRBgNVHSMEgckwgcaAFMmT/x412UH+5OHqglee
|
|
23
|
-
TjLOv6iHoYGipIGfMIGcMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNV
|
|
24
|
-
BAcTCVBhbG8gQWx0bzEfMB0GA1UEChMWVGluZm9pbCBTZWN1cml0eSwgSW5jLjEf
|
|
25
|
-
MB0GA1UEAxMWVGluZm9pbCBTZWN1cml0eSwgSW5jLjEqMCgGCSqGSIb3DQEJARYb
|
|
26
|
-
c3VwcG9ydEB0aW5mb2lsc2VjdXJpdHkuY29tggkAra7QuiMwI2AwDwYDVR0TAQH/
|
|
27
|
-
BAUwAwEB/zARBglghkgBhvhCAQEEBAMCAQYwCQYDVR0SBAIwADArBglghkgBhvhC
|
|
28
|
-
AQ0EHhYcVGlueUNBIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAmBgNVHREEHzAdgRtz
|
|
29
|
-
dXBwb3J0QHRpbmZvaWxzZWN1cml0eS5jb20wDgYDVR0PAQH/BAQDAgEGMA0GCSqG
|
|
30
|
-
SIb3DQEBBQUAA4ICAQBZy4JJSmwLuO0nZbdr4tJeVS2P8bcGi6PzAcdzVfwzjp6n
|
|
31
|
-
5qf8m4O8my4lnJieom0GrWSHQoPY1Yur4hEoZbugKO9DWZL3dTiGcrgw0TbQ6Gtq
|
|
32
|
-
TTPatW3LA21qFJwvohSvLqPdmZuM+H9g49sdl2kNTDVI6iUyMYuNpL14aPKPGBFo
|
|
33
|
-
o7UjciT1h7JtJl9b/fXrbPeRHBwpZXWeipiPGv/OZW5KnOsNlUkTquS7Zj4ETkIC
|
|
34
|
-
6mVtmsLvq+YwFthFyMU37pXwYxcmqRmH6lX+XC6AVW5oO4GBmG+Zr/Z+h5Cih5ca
|
|
35
|
-
/mX88RkO+dGTjw1IdxKmxOqKL62OBATKrTDJ/scsmRptynA4TunYW+7ikOpDbPfL
|
|
36
|
-
l18aleLISlcgWJg/Czf2nmBqAClPLnhV8qxWsvt58MQQ/Jpoggvpl8EG1PylWiBS
|
|
37
|
-
Kc/4Ad/FKQFpTzXUgDg2kV07npVjYbBzA5p4ZSWSlflFu93jb9gg2+qtnRSImVCZ
|
|
38
|
-
nQjZdsv8hebElPAIbtJjSnoH1Kz2ucYLakdF1UMKnpp1PVREtuKPz/foU9KUHs0z
|
|
39
|
-
dWRALx8cWG4uKK9AIEUlVdGKfX0Wj0qFK0KGxl3f3jObud5Agwue2EPKWwUzEGUh
|
|
40
|
-
Iqp60gNw3vBdKHw4dh1bfcbXWnRDL+OQPuOFZeMWgu1QmeHeuggYtYtRg7V5Kg==
|
|
41
|
-
-----END CERTIFICATE-----
|