devise-two-factor 1.1.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of devise-two-factor might be problematic. Click here for more details.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/UPGRADING.md +11 -0
- data/devise-two-factor.gemspec +1 -1
- data/lib/devise_two_factor/models/two_factor_authenticatable.rb +22 -4
- data/lib/devise_two_factor/spec_helpers/two_factor_authenticatable_shared_examples.rb +38 -12
- data/lib/devise_two_factor/spec_helpers/two_factor_backupable_shared_examples.rb +10 -11
- data/lib/devise_two_factor/strategies/two_factor_authenticatable.rb +1 -1
- data/lib/devise_two_factor/version.rb +1 -1
- data/lib/generators/devise_two_factor/devise_two_factor_generator.rb +1 -0
- data/spec/devise/models/two_factor_authenticatable_spec.rb +7 -0
- metadata +32 -36
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 139ae00abf04084452d11ccb85fba46e18ba36bb
|
4
|
+
data.tar.gz: f8ebe13d8db9f67663306e854be967bd08d39460
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4114ed8083d8daacbd15355132dc02c1bc33770bc8018a8a858e0888e1200eae1420be1eccc79af96b7ba5374af60c6a81562300b39b629ccdaba9f373c3b968
|
7
|
+
data.tar.gz: 74bc2b4571a54206c9d3cf256eaa27bedf1e57f3c99ae1a7424beef6217afd8bfb4b22b5e6f5be3fcf8a3a7c894d820e46e019c1f14c47cd83e2f80a16089dd9
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data.tar.gz.sig
CHANGED
Binary file
|
data/UPGRADING.md
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# Guide to upgrading from 1.x to 2.x
|
2
|
+
|
3
|
+
Pull request #43 added a new field to protect against "shoulder-surfing" attacks. If upgrading, you'll need to add the `:consumed_timestep` column to your `Users` model.
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
class AddConsumedTimestepToUsers < ActiveRecord::Migration
|
7
|
+
def change
|
8
|
+
add_column :users, :consumed_timestep, :integer
|
9
|
+
end
|
10
|
+
end
|
11
|
+
```
|
data/devise-two-factor.gemspec
CHANGED
@@ -32,7 +32,7 @@ Gem::Specification.new do |s|
|
|
32
32
|
|
33
33
|
s.add_development_dependency 'activemodel'
|
34
34
|
s.add_development_dependency 'bundler', '> 1.0'
|
35
|
-
s.add_development_dependency 'rspec', '>
|
35
|
+
s.add_development_dependency 'rspec', '> 3'
|
36
36
|
s.add_development_dependency 'simplecov'
|
37
37
|
s.add_development_dependency 'faker'
|
38
38
|
s.add_development_dependency 'timecop'
|
@@ -15,17 +15,19 @@ module Devise
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def self.required_fields(klass)
|
18
|
-
[:encrypted_otp_secret, :encrypted_otp_secret_iv, :encrypted_otp_secret_salt]
|
18
|
+
[:encrypted_otp_secret, :encrypted_otp_secret_iv, :encrypted_otp_secret_salt, :consumed_timestep]
|
19
19
|
end
|
20
20
|
|
21
21
|
# This defaults to the model's otp_secret
|
22
|
-
# If this hasn't been generated yet, pass a secret as an
|
23
|
-
def
|
22
|
+
# If this hasn't been generated yet, pass a secret as an option
|
23
|
+
def validate_and_consume_otp!(code, options = {})
|
24
24
|
otp_secret = options[:otp_secret] || self.otp_secret
|
25
25
|
return false unless otp_secret.present?
|
26
26
|
|
27
27
|
totp = self.otp(otp_secret)
|
28
|
-
totp.verify_with_drift(code, self.class.otp_allowed_drift)
|
28
|
+
return consume_otp! if totp.verify_with_drift(code, self.class.otp_allowed_drift)
|
29
|
+
|
30
|
+
false
|
29
31
|
end
|
30
32
|
|
31
33
|
def otp(otp_secret = self.otp_secret)
|
@@ -36,6 +38,11 @@ module Devise
|
|
36
38
|
otp.at(Time.now)
|
37
39
|
end
|
38
40
|
|
41
|
+
# ROTP's TOTP#timecode is private, so we duplicate it here
|
42
|
+
def current_otp_timestep
|
43
|
+
Time.now.utc.to_i / otp.interval
|
44
|
+
end
|
45
|
+
|
39
46
|
def otp_provisioning_uri(account, options = {})
|
40
47
|
otp_secret = options[:otp_secret] || self.otp_secret
|
41
48
|
ROTP::TOTP.new(otp_secret, options).provisioning_uri(account)
|
@@ -47,6 +54,17 @@ module Devise
|
|
47
54
|
|
48
55
|
protected
|
49
56
|
|
57
|
+
# An OTP cannot be used more than once in a given timestep
|
58
|
+
# Storing timestep of last valid OTP is sufficient to satisfy this requirement
|
59
|
+
def consume_otp!
|
60
|
+
if self.consumed_timestep != current_otp_timestep
|
61
|
+
self.consumed_timestep = current_otp_timestep
|
62
|
+
return save(validate: false)
|
63
|
+
end
|
64
|
+
|
65
|
+
false
|
66
|
+
end
|
67
|
+
|
50
68
|
module ClassMethods
|
51
69
|
Devise::Models.config(self, :otp_secret_length,
|
52
70
|
:otp_allowed_drift,
|
@@ -5,29 +5,29 @@ shared_examples 'two_factor_authenticatable' do
|
|
5
5
|
|
6
6
|
describe 'required_fields' do
|
7
7
|
it 'should have the attr_encrypted fields for otp_secret' do
|
8
|
-
Devise::Models::TwoFactorAuthenticatable.required_fields(subject.class).
|
8
|
+
expect(Devise::Models::TwoFactorAuthenticatable.required_fields(subject.class)).to contain_exactly(:encrypted_otp_secret, :encrypted_otp_secret_iv, :encrypted_otp_secret_salt, :consumed_timestep)
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
12
|
describe '#otp_secret' do
|
13
13
|
it 'should be of the configured length' do
|
14
|
-
subject.otp_secret.length.
|
14
|
+
expect(subject.otp_secret.length).to eq(subject.class.otp_secret_length)
|
15
15
|
end
|
16
16
|
|
17
17
|
it 'stores the encrypted otp_secret' do
|
18
|
-
subject.encrypted_otp_secret.
|
18
|
+
expect(subject.encrypted_otp_secret).to_not be_nil
|
19
19
|
end
|
20
20
|
|
21
21
|
it 'stores an iv for otp_secret' do
|
22
|
-
subject.encrypted_otp_secret_iv.
|
22
|
+
expect(subject.encrypted_otp_secret_iv).to_not be_nil
|
23
23
|
end
|
24
24
|
|
25
25
|
it 'stores a salt for otp_secret' do
|
26
|
-
subject.encrypted_otp_secret_salt.
|
26
|
+
expect(subject.encrypted_otp_secret_salt).to_not be_nil
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
-
describe '#
|
30
|
+
describe '#validate_and_consume_otp!' do
|
31
31
|
let(:otp_secret) { '2z6hxkdwi3uvrnpn' }
|
32
32
|
|
33
33
|
before :each do
|
@@ -39,24 +39,50 @@ shared_examples 'two_factor_authenticatable' do
|
|
39
39
|
Timecop.return
|
40
40
|
end
|
41
41
|
|
42
|
+
context 'with a stored consumed_timestep' do
|
43
|
+
context 'given a precisely correct OTP' do
|
44
|
+
let(:consumed_otp) { ROTP::TOTP.new(otp_secret).at(Time.now) }
|
45
|
+
|
46
|
+
before do
|
47
|
+
subject.validate_and_consume_otp!(consumed_otp)
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'fails to validate' do
|
51
|
+
expect(subject.validate_and_consume_otp!(consumed_otp)).to be false
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'given a previously valid OTP within the allowed drift' do
|
56
|
+
let(:consumed_otp) { ROTP::TOTP.new(otp_secret).at(Time.now + subject.class.otp_allowed_drift, true) }
|
57
|
+
|
58
|
+
before do
|
59
|
+
subject.validate_and_consume_otp!(consumed_otp)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'fails to validate' do
|
63
|
+
expect(subject.validate_and_consume_otp!(consumed_otp)).to be false
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
42
68
|
it 'validates a precisely correct OTP' do
|
43
69
|
otp = ROTP::TOTP.new(otp_secret).at(Time.now)
|
44
|
-
subject.
|
70
|
+
expect(subject.validate_and_consume_otp!(otp)).to be true
|
45
71
|
end
|
46
72
|
|
47
73
|
it 'validates an OTP within the allowed drift' do
|
48
74
|
otp = ROTP::TOTP.new(otp_secret).at(Time.now + subject.class.otp_allowed_drift, true)
|
49
|
-
subject.
|
75
|
+
expect(subject.validate_and_consume_otp!(otp)).to be true
|
50
76
|
end
|
51
77
|
|
52
78
|
it 'does not validate an OTP above the allowed drift' do
|
53
79
|
otp = ROTP::TOTP.new(otp_secret).at(Time.now + subject.class.otp_allowed_drift * 2, true)
|
54
|
-
subject.
|
80
|
+
expect(subject.validate_and_consume_otp!(otp)).to be false
|
55
81
|
end
|
56
82
|
|
57
83
|
it 'does not validate an OTP below the allowed drift' do
|
58
84
|
otp = ROTP::TOTP.new(otp_secret).at(Time.now - subject.class.otp_allowed_drift * 2, true)
|
59
|
-
subject.
|
85
|
+
expect(subject.validate_and_consume_otp!(otp)).to be false
|
60
86
|
end
|
61
87
|
end
|
62
88
|
|
@@ -66,11 +92,11 @@ shared_examples 'two_factor_authenticatable' do
|
|
66
92
|
let(:issuer) { "Tinfoil" }
|
67
93
|
|
68
94
|
it "should return uri with specified account" do
|
69
|
-
subject.otp_provisioning_uri(account).
|
95
|
+
expect(subject.otp_provisioning_uri(account)).to match(%r{otpauth://totp/#{account}\?secret=\w{#{otp_secret_length}}})
|
70
96
|
end
|
71
97
|
|
72
98
|
it 'should return uri with issuer option' do
|
73
|
-
subject.otp_provisioning_uri(account, issuer: issuer).
|
99
|
+
expect(subject.otp_provisioning_uri(account, issuer: issuer)).to match(%r{otpauth://totp/#{account}\?secret=\w{#{otp_secret_length}}&issuer=#{issuer}$})
|
74
100
|
end
|
75
101
|
end
|
76
102
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
shared_examples 'two_factor_backupable' do
|
2
2
|
describe 'required_fields' do
|
3
3
|
it 'has the attr_encrypted fields for otp_backup_codes' do
|
4
|
-
Devise::Models::TwoFactorBackupable.required_fields(subject.class).
|
4
|
+
expect(Devise::Models::TwoFactorBackupable.required_fields(subject.class)).to contain_exactly(:otp_backup_codes)
|
5
5
|
end
|
6
6
|
end
|
7
7
|
|
@@ -12,24 +12,23 @@ shared_examples 'two_factor_backupable' do
|
|
12
12
|
end
|
13
13
|
|
14
14
|
it 'generates the correct number of new recovery codes' do
|
15
|
-
subject.otp_backup_codes.length.
|
16
|
-
eq(subject.class.otp_number_of_backup_codes)
|
15
|
+
expect(subject.otp_backup_codes.length).to eq(subject.class.otp_number_of_backup_codes)
|
17
16
|
end
|
18
17
|
|
19
18
|
it 'generates recovery codes of the correct length' do
|
20
19
|
@plaintext_codes.each do |code|
|
21
|
-
code.length.
|
20
|
+
expect(code.length).to eq(subject.class.otp_backup_code_length)
|
22
21
|
end
|
23
22
|
end
|
24
23
|
|
25
24
|
it 'generates distinct recovery codes' do
|
26
|
-
@plaintext_codes.uniq.
|
25
|
+
expect(@plaintext_codes.uniq).to contain_exactly(*@plaintext_codes)
|
27
26
|
end
|
28
27
|
|
29
28
|
it 'stores the codes as BCrypt hashes' do
|
30
29
|
subject.otp_backup_codes.each do |code|
|
31
30
|
# $algorithm$cost$(22 character salt + 31 character hash)
|
32
|
-
code.
|
31
|
+
expect(code).to match(/\A\$[0-9a-z]{2}\$[0-9]{2}\$[A-Za-z0-9\.\/]{53}\z/)
|
33
32
|
end
|
34
33
|
end
|
35
34
|
end
|
@@ -44,7 +43,7 @@ shared_examples 'two_factor_backupable' do
|
|
44
43
|
end
|
45
44
|
|
46
45
|
it 'invalidates the existing recovery codes' do
|
47
|
-
(subject.otp_backup_codes & old_codes_hashed).
|
46
|
+
expect((subject.otp_backup_codes & old_codes_hashed)).to match []
|
48
47
|
end
|
49
48
|
end
|
50
49
|
end
|
@@ -56,14 +55,14 @@ shared_examples 'two_factor_backupable' do
|
|
56
55
|
|
57
56
|
context 'given an invalid recovery code' do
|
58
57
|
it 'returns false' do
|
59
|
-
subject.invalidate_otp_backup_code!('password').
|
58
|
+
expect(subject.invalidate_otp_backup_code!('password')).to be false
|
60
59
|
end
|
61
60
|
end
|
62
61
|
|
63
62
|
context 'given a valid recovery code' do
|
64
63
|
it 'returns true' do
|
65
64
|
@plaintext_codes.each do |code|
|
66
|
-
subject.invalidate_otp_backup_code!(code).
|
65
|
+
expect(subject.invalidate_otp_backup_code!(code)).to be true
|
67
66
|
end
|
68
67
|
end
|
69
68
|
|
@@ -71,7 +70,7 @@ shared_examples 'two_factor_backupable' do
|
|
71
70
|
code = @plaintext_codes.sample
|
72
71
|
|
73
72
|
subject.invalidate_otp_backup_code!(code)
|
74
|
-
subject.invalidate_otp_backup_code!(code).
|
73
|
+
expect(subject.invalidate_otp_backup_code!(code)).to be false
|
75
74
|
end
|
76
75
|
|
77
76
|
it 'does not invalidate the other recovery codes' do
|
@@ -81,7 +80,7 @@ shared_examples 'two_factor_backupable' do
|
|
81
80
|
@plaintext_codes.delete(code)
|
82
81
|
|
83
82
|
@plaintext_codes.each do |code|
|
84
|
-
subject.invalidate_otp_backup_code!(code).
|
83
|
+
expect(subject.invalidate_otp_backup_code!(code)).to be true
|
85
84
|
end
|
86
85
|
end
|
87
86
|
end
|
@@ -22,7 +22,7 @@ module Devise
|
|
22
22
|
def validate_otp(resource)
|
23
23
|
return true unless resource.otp_required_for_login
|
24
24
|
return if params[scope]['otp_attempt'].nil?
|
25
|
-
resource.
|
25
|
+
resource.validate_and_consume_otp!(params[scope]['otp_attempt'])
|
26
26
|
end
|
27
27
|
end
|
28
28
|
end
|
@@ -6,6 +6,13 @@ class TwoFactorAuthenticatableDouble
|
|
6
6
|
extend ::Devise::Models
|
7
7
|
|
8
8
|
devise :two_factor_authenticatable, :otp_secret_encryption_key => 'test-key'
|
9
|
+
|
10
|
+
attr_accessor :consumed_timestep
|
11
|
+
|
12
|
+
def save(validate)
|
13
|
+
# noop for testing
|
14
|
+
true
|
15
|
+
end
|
9
16
|
end
|
10
17
|
|
11
18
|
describe ::Devise::Models::TwoFactorAuthenticatable do
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: devise-two-factor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shane Wilton
|
@@ -84,166 +84,160 @@ cert_chain:
|
|
84
84
|
5C31v4YyRBnNCp0pN66nxYX2avEiQ8riTBP5mlkPPOhsIoYQHHe2Uj75aVpu0LZ3
|
85
85
|
cdFzuO4GC1dV0Wv+dsDm+MyF7DT5E9pUPXpnMJuPvPrFpCb+wrFlszW9hGjXbQ==
|
86
86
|
-----END CERTIFICATE-----
|
87
|
-
date: 2015-
|
87
|
+
date: 2015-09-16 00:00:00.000000000 Z
|
88
88
|
dependencies:
|
89
89
|
- !ruby/object:Gem::Dependency
|
90
90
|
name: railties
|
91
91
|
requirement: !ruby/object:Gem::Requirement
|
92
92
|
requirements:
|
93
|
-
- -
|
93
|
+
- - '>='
|
94
94
|
- !ruby/object:Gem::Version
|
95
95
|
version: '0'
|
96
96
|
type: :runtime
|
97
97
|
prerelease: false
|
98
98
|
version_requirements: !ruby/object:Gem::Requirement
|
99
99
|
requirements:
|
100
|
-
- -
|
100
|
+
- - '>='
|
101
101
|
- !ruby/object:Gem::Version
|
102
102
|
version: '0'
|
103
103
|
- !ruby/object:Gem::Dependency
|
104
104
|
name: activesupport
|
105
105
|
requirement: !ruby/object:Gem::Requirement
|
106
106
|
requirements:
|
107
|
-
- -
|
107
|
+
- - '>='
|
108
108
|
- !ruby/object:Gem::Version
|
109
109
|
version: '0'
|
110
110
|
type: :runtime
|
111
111
|
prerelease: false
|
112
112
|
version_requirements: !ruby/object:Gem::Requirement
|
113
113
|
requirements:
|
114
|
-
- -
|
114
|
+
- - '>='
|
115
115
|
- !ruby/object:Gem::Version
|
116
116
|
version: '0'
|
117
117
|
- !ruby/object:Gem::Dependency
|
118
118
|
name: attr_encrypted
|
119
119
|
requirement: !ruby/object:Gem::Requirement
|
120
120
|
requirements:
|
121
|
-
- -
|
121
|
+
- - ~>
|
122
122
|
- !ruby/object:Gem::Version
|
123
123
|
version: 1.3.2
|
124
124
|
type: :runtime
|
125
125
|
prerelease: false
|
126
126
|
version_requirements: !ruby/object:Gem::Requirement
|
127
127
|
requirements:
|
128
|
-
- -
|
128
|
+
- - ~>
|
129
129
|
- !ruby/object:Gem::Version
|
130
130
|
version: 1.3.2
|
131
131
|
- !ruby/object:Gem::Dependency
|
132
132
|
name: devise
|
133
133
|
requirement: !ruby/object:Gem::Requirement
|
134
134
|
requirements:
|
135
|
-
- -
|
135
|
+
- - ~>
|
136
136
|
- !ruby/object:Gem::Version
|
137
137
|
version: 3.5.0
|
138
138
|
type: :runtime
|
139
139
|
prerelease: false
|
140
140
|
version_requirements: !ruby/object:Gem::Requirement
|
141
141
|
requirements:
|
142
|
-
- -
|
142
|
+
- - ~>
|
143
143
|
- !ruby/object:Gem::Version
|
144
144
|
version: 3.5.0
|
145
145
|
- !ruby/object:Gem::Dependency
|
146
146
|
name: rotp
|
147
147
|
requirement: !ruby/object:Gem::Requirement
|
148
148
|
requirements:
|
149
|
-
- -
|
149
|
+
- - ~>
|
150
150
|
- !ruby/object:Gem::Version
|
151
151
|
version: '2'
|
152
152
|
type: :runtime
|
153
153
|
prerelease: false
|
154
154
|
version_requirements: !ruby/object:Gem::Requirement
|
155
155
|
requirements:
|
156
|
-
- -
|
156
|
+
- - ~>
|
157
157
|
- !ruby/object:Gem::Version
|
158
158
|
version: '2'
|
159
159
|
- !ruby/object:Gem::Dependency
|
160
160
|
name: activemodel
|
161
161
|
requirement: !ruby/object:Gem::Requirement
|
162
162
|
requirements:
|
163
|
-
- -
|
163
|
+
- - '>='
|
164
164
|
- !ruby/object:Gem::Version
|
165
165
|
version: '0'
|
166
166
|
type: :development
|
167
167
|
prerelease: false
|
168
168
|
version_requirements: !ruby/object:Gem::Requirement
|
169
169
|
requirements:
|
170
|
-
- -
|
170
|
+
- - '>='
|
171
171
|
- !ruby/object:Gem::Version
|
172
172
|
version: '0'
|
173
173
|
- !ruby/object:Gem::Dependency
|
174
174
|
name: bundler
|
175
175
|
requirement: !ruby/object:Gem::Requirement
|
176
176
|
requirements:
|
177
|
-
- -
|
177
|
+
- - '>'
|
178
178
|
- !ruby/object:Gem::Version
|
179
179
|
version: '1.0'
|
180
180
|
type: :development
|
181
181
|
prerelease: false
|
182
182
|
version_requirements: !ruby/object:Gem::Requirement
|
183
183
|
requirements:
|
184
|
-
- -
|
184
|
+
- - '>'
|
185
185
|
- !ruby/object:Gem::Version
|
186
186
|
version: '1.0'
|
187
187
|
- !ruby/object:Gem::Dependency
|
188
188
|
name: rspec
|
189
189
|
requirement: !ruby/object:Gem::Requirement
|
190
190
|
requirements:
|
191
|
-
- -
|
192
|
-
- !ruby/object:Gem::Version
|
193
|
-
version: '2'
|
194
|
-
- - "<"
|
191
|
+
- - '>'
|
195
192
|
- !ruby/object:Gem::Version
|
196
193
|
version: '3'
|
197
194
|
type: :development
|
198
195
|
prerelease: false
|
199
196
|
version_requirements: !ruby/object:Gem::Requirement
|
200
197
|
requirements:
|
201
|
-
- -
|
202
|
-
- !ruby/object:Gem::Version
|
203
|
-
version: '2'
|
204
|
-
- - "<"
|
198
|
+
- - '>'
|
205
199
|
- !ruby/object:Gem::Version
|
206
200
|
version: '3'
|
207
201
|
- !ruby/object:Gem::Dependency
|
208
202
|
name: simplecov
|
209
203
|
requirement: !ruby/object:Gem::Requirement
|
210
204
|
requirements:
|
211
|
-
- -
|
205
|
+
- - '>='
|
212
206
|
- !ruby/object:Gem::Version
|
213
207
|
version: '0'
|
214
208
|
type: :development
|
215
209
|
prerelease: false
|
216
210
|
version_requirements: !ruby/object:Gem::Requirement
|
217
211
|
requirements:
|
218
|
-
- -
|
212
|
+
- - '>='
|
219
213
|
- !ruby/object:Gem::Version
|
220
214
|
version: '0'
|
221
215
|
- !ruby/object:Gem::Dependency
|
222
216
|
name: faker
|
223
217
|
requirement: !ruby/object:Gem::Requirement
|
224
218
|
requirements:
|
225
|
-
- -
|
219
|
+
- - '>='
|
226
220
|
- !ruby/object:Gem::Version
|
227
221
|
version: '0'
|
228
222
|
type: :development
|
229
223
|
prerelease: false
|
230
224
|
version_requirements: !ruby/object:Gem::Requirement
|
231
225
|
requirements:
|
232
|
-
- -
|
226
|
+
- - '>='
|
233
227
|
- !ruby/object:Gem::Version
|
234
228
|
version: '0'
|
235
229
|
- !ruby/object:Gem::Dependency
|
236
230
|
name: timecop
|
237
231
|
requirement: !ruby/object:Gem::Requirement
|
238
232
|
requirements:
|
239
|
-
- -
|
233
|
+
- - '>='
|
240
234
|
- !ruby/object:Gem::Version
|
241
235
|
version: '0'
|
242
236
|
type: :development
|
243
237
|
prerelease: false
|
244
238
|
version_requirements: !ruby/object:Gem::Requirement
|
245
239
|
requirements:
|
246
|
-
- -
|
240
|
+
- - '>='
|
247
241
|
- !ruby/object:Gem::Version
|
248
242
|
version: '0'
|
249
243
|
description: Barebones two-factor authentication with Devise
|
@@ -252,14 +246,15 @@ executables: []
|
|
252
246
|
extensions: []
|
253
247
|
extra_rdoc_files: []
|
254
248
|
files:
|
255
|
-
-
|
256
|
-
-
|
257
|
-
-
|
249
|
+
- .gitignore
|
250
|
+
- .rspec
|
251
|
+
- .travis.yml
|
258
252
|
- CONTRIBUTING.md
|
259
253
|
- Gemfile
|
260
254
|
- LICENSE
|
261
255
|
- README.md
|
262
256
|
- Rakefile
|
257
|
+
- UPGRADING.md
|
263
258
|
- certs/tinfoil-cacert.pem
|
264
259
|
- certs/tinfoilsecurity-gems-cert.pem
|
265
260
|
- devise-two-factor.gemspec
|
@@ -288,17 +283,17 @@ require_paths:
|
|
288
283
|
- lib
|
289
284
|
required_ruby_version: !ruby/object:Gem::Requirement
|
290
285
|
requirements:
|
291
|
-
- -
|
286
|
+
- - '>='
|
292
287
|
- !ruby/object:Gem::Version
|
293
288
|
version: '0'
|
294
289
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
295
290
|
requirements:
|
296
|
-
- -
|
291
|
+
- - '>='
|
297
292
|
- !ruby/object:Gem::Version
|
298
293
|
version: '0'
|
299
294
|
requirements: []
|
300
295
|
rubyforge_project: devise-two-factor
|
301
|
-
rubygems_version: 2.4.
|
296
|
+
rubygems_version: 2.4.7
|
302
297
|
signing_key:
|
303
298
|
specification_version: 4
|
304
299
|
summary: Barebones two-factor authentication with Devise
|
@@ -306,3 +301,4 @@ test_files:
|
|
306
301
|
- spec/devise/models/two_factor_authenticatable_spec.rb
|
307
302
|
- spec/devise/models/two_factor_backupable_spec.rb
|
308
303
|
- spec/spec_helper.rb
|
304
|
+
has_rdoc:
|
metadata.gz.sig
CHANGED
Binary file
|