devise-two-factor 1.1.0 → 2.0.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.

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