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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f606d2579650285b6cc4377b1da513596d979e3c
4
- data.tar.gz: 36d5f5df579e84d85db796eacdb07376abdfec47
3
+ metadata.gz: 139ae00abf04084452d11ccb85fba46e18ba36bb
4
+ data.tar.gz: f8ebe13d8db9f67663306e854be967bd08d39460
5
5
  SHA512:
6
- metadata.gz: 5539b146deb18560ac1be676606a8165d860acef7661b80d73ce000d006d9a530c06121393707a0d9d91951c415703e7ece16cdda6698bf71f081267322d149f
7
- data.tar.gz: d0c422b2f12ffdae6d507732d71fe06684fe69c2501c1a24c920b50ddfcfb037d5f69229869fbd62f54f345a43ea2788e90fb4e8942dfb20faf564d95069fed9
6
+ metadata.gz: 4114ed8083d8daacbd15355132dc02c1bc33770bc8018a8a858e0888e1200eae1420be1eccc79af96b7ba5374af60c6a81562300b39b629ccdaba9f373c3b968
7
+ data.tar.gz: 74bc2b4571a54206c9d3cf256eaa27bedf1e57f3c99ae1a7424beef6217afd8bfb4b22b5e6f5be3fcf8a3a7c894d820e46e019c1f14c47cd83e2f80a16089dd9
Binary file
data.tar.gz.sig CHANGED
Binary file
@@ -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
+ ```
@@ -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', '> 2', '< 3'
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 option
23
- def valid_otp?(code, options = {})
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).should =~ ([:encrypted_otp_secret, :encrypted_otp_secret_iv, :encrypted_otp_secret_salt])
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.should eq(subject.class.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.should_not be_nil
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.should_not be_nil
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.should_not be_nil
26
+ expect(subject.encrypted_otp_secret_salt).to_not be_nil
27
27
  end
28
28
  end
29
29
 
30
- describe '#valid_otp?' do
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.valid_otp?(otp).should be true
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.valid_otp?(otp).should be true
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.valid_otp?(otp).should be false
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.valid_otp?(otp).should be false
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).should match(%r{otpauth://totp/#{account}\?secret=\w{#{otp_secret_length}}})
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).should match(%r{otpauth://totp/#{account}\?secret=\w{#{otp_secret_length}}&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).should =~ [:otp_backup_codes]
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.should
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.should eq(subject.class.otp_backup_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.should =~ @plaintext_codes
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.should =~ /\A\$[0-9a-z]{2}\$[0-9]{2}\$[A-Za-z0-9\.\/]{53}\z/
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).should =~ []
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').should be false
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).should be true
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).should be false
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).should be true
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.valid_otp?(params[scope]['otp_attempt'])
25
+ resource.validate_and_consume_otp!(params[scope]['otp_attempt'])
26
26
  end
27
27
  end
28
28
  end
@@ -1,3 +1,3 @@
1
1
  module DeviseTwoFactor
2
- VERSION = '1.1.0'.freeze
2
+ VERSION = '2.0.0'.freeze
3
3
  end
@@ -22,6 +22,7 @@ module DeviseTwoFactor
22
22
  "encrypted_otp_secret:string",
23
23
  "encrypted_otp_secret_iv:string",
24
24
  "encrypted_otp_secret_salt:string",
25
+ "consumed_timestep:integer",
25
26
  "otp_required_for_login:boolean"
26
27
  ]
27
28
 
@@ -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: 1.1.0
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-07-10 00:00:00.000000000 Z
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
- - ".gitignore"
256
- - ".rspec"
257
- - ".travis.yml"
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.6
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