two_factor_authentication 1.1.4 → 1.1.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +13 -6
  3. data/CHANGELOG.md +109 -0
  4. data/Gemfile +8 -2
  5. data/README.md +182 -54
  6. data/app/controllers/devise/two_factor_authentication_controller.rb +1 -1
  7. data/config/locales/fr.yml +7 -0
  8. data/lib/generators/active_record/templates/migration.rb +6 -11
  9. data/lib/two_factor_authentication.rb +3 -0
  10. data/lib/two_factor_authentication/hooks/two_factor_authenticatable.rb +26 -2
  11. data/lib/two_factor_authentication/models/two_factor_authenticatable.rb +89 -23
  12. data/lib/two_factor_authentication/schema.rb +12 -4
  13. data/lib/two_factor_authentication/version.rb +1 -1
  14. data/spec/controllers/two_factor_authentication_controller_spec.rb +33 -0
  15. data/spec/features/two_factor_authenticatable_spec.rb +164 -28
  16. data/spec/generators/active_record/two_factor_authentication_generator_spec.rb +36 -0
  17. data/spec/lib/two_factor_authentication/models/two_factor_authenticatable_spec.rb +213 -117
  18. data/spec/rails_app/app/models/encrypted_user.rb +14 -0
  19. data/spec/rails_app/app/models/user.rb +1 -2
  20. data/spec/rails_app/config/environments/test.rb +3 -0
  21. data/spec/rails_app/config/initializers/devise.rb +3 -1
  22. data/spec/rails_app/db/migrate/20151224171231_add_encrypted_columns_to_user.rb +9 -0
  23. data/spec/rails_app/db/migrate/20151224180310_populate_otp_column.rb +19 -0
  24. data/spec/rails_app/db/migrate/20151228230340_remove_otp_secret_key_from_user.rb +5 -0
  25. data/spec/rails_app/db/schema.rb +16 -14
  26. data/spec/spec_helper.rb +1 -0
  27. data/spec/support/authenticated_model_helper.rb +26 -2
  28. data/spec/support/controller_helper.rb +16 -0
  29. data/spec/support/features_spec_helper.rb +24 -1
  30. data/two_factor_authentication.gemspec +1 -0
  31. metadata +25 -3
  32. data/spec/controllers/two_factor_auth_spec.rb +0 -18
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+
3
+ require 'generators/active_record/two_factor_authentication_generator'
4
+
5
+ describe ActiveRecord::Generators::TwoFactorAuthenticationGenerator, type: :generator do
6
+ destination File.expand_path('../../../../../tmp', __FILE__)
7
+
8
+ before do
9
+ prepare_destination
10
+ end
11
+
12
+ it 'runs all methods in the generator' do
13
+ gen = generator %w(users)
14
+ expect(gen).to receive(:copy_two_factor_authentication_migration)
15
+ gen.invoke_all
16
+ end
17
+
18
+ describe 'the generated files' do
19
+ before do
20
+ run_generator %w(users)
21
+ end
22
+
23
+ describe 'the migration' do
24
+ subject { migration_file('db/migrate/two_factor_authentication_add_to_users.rb') }
25
+
26
+ it { is_expected.to exist }
27
+ it { is_expected.to be_a_migration }
28
+ it { is_expected.to contain /def change/ }
29
+ it { is_expected.to contain /add_column :users, :second_factor_attempts_count, :integer, default: 0/ }
30
+ it { is_expected.to contain /add_column :users, :encrypted_otp_secret_key, :string/ }
31
+ it { is_expected.to contain /add_column :users, :encrypted_otp_secret_key_iv, :string/ }
32
+ it { is_expected.to contain /add_column :users, :encrypted_otp_secret_key_salt, :string/ }
33
+ it { is_expected.to contain /add_index :users, :encrypted_otp_secret_key, unique: true/ }
34
+ end
35
+ end
36
+ end
@@ -1,168 +1,264 @@
1
1
  require 'spec_helper'
2
2
  include AuthenticatedModelHelper
3
3
 
4
- describe Devise::Models::TwoFactorAuthenticatable, '#otp_code' do
5
- let(:instance) { build_guest_user }
6
- subject { instance.otp_code(time) }
7
- let(:time) { 1392852456 }
8
-
9
- it "should return an error if no secret is set" do
10
- expect {
11
- subject
12
- }.to raise_error Exception
4
+ describe Devise::Models::TwoFactorAuthenticatable do
5
+ describe '#otp_code' do
6
+ shared_examples 'otp_code' do |instance|
7
+ subject { instance.otp_code(time) }
8
+ let(:time) { 1_392_852_456 }
9
+
10
+ it 'returns an error if no secret is set' do
11
+ expect { subject }.to raise_error Exception
12
+ end
13
+
14
+ context 'secret is set' do
15
+ before :each do
16
+ instance.otp_secret_key = '2z6hxkdwi3uvrnpn'
17
+ end
18
+
19
+ it 'does not return an error' do
20
+ subject
21
+ end
22
+
23
+ it 'matches Devise configured length' do
24
+ expect(subject.length).to eq(Devise.otp_length)
25
+ end
26
+
27
+ context 'with a known time' do
28
+ let(:time) { 1_392_852_756 }
29
+
30
+ it 'returns a known result' do
31
+ expect(subject).
32
+ to eq('0000000524562202'.split(//).last(Devise.otp_length).join)
33
+ end
34
+ end
35
+
36
+ context 'with a known time yielding a result with less than 6 digits' do
37
+ let(:time) { 1_393_065_856 }
38
+
39
+ it 'returns a known result padded with zeroes' do
40
+ expect(subject).
41
+ to eq('0000001608007672'.split(//).last(Devise.otp_length).join)
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ it_behaves_like 'otp_code', GuestUser.new
48
+ it_behaves_like 'otp_code', EncryptedUser.new
13
49
  end
14
50
 
15
- context "secret is set" do
16
- before :each do
17
- instance.otp_secret_key = "2z6hxkdwi3uvrnpn"
51
+ describe '#authenticate_otp' do
52
+ shared_examples 'authenticate_otp' do |instance|
53
+ before :each do
54
+ instance.otp_secret_key = '2z6hxkdwi3uvrnpn'
55
+ end
56
+
57
+ def do_invoke(code, user)
58
+ user.authenticate_otp(code)
59
+ end
60
+
61
+ it 'authenticates a recently created code' do
62
+ code = instance.otp_code
63
+ expect(do_invoke(code, instance)).to eq(true)
64
+ end
65
+
66
+ it 'does not authenticate an old code' do
67
+ code = instance.otp_code(1.minutes.ago.to_i)
68
+ expect(do_invoke(code, instance)).to eq(false)
69
+ end
18
70
  end
19
71
 
20
- it "should not return an error" do
21
- subject
72
+ it_behaves_like 'authenticate_otp', GuestUser.new
73
+ it_behaves_like 'authenticate_otp', EncryptedUser.new
74
+ end
75
+
76
+ describe '#send_two_factor_authentication_code' do
77
+ let(:instance) { build_guest_user }
78
+
79
+ it 'raises an error by default' do
80
+ expect { instance.send_two_factor_authentication_code }.
81
+ to raise_error(NotImplementedError)
22
82
  end
23
83
 
24
- it "should be configured length" do
25
- expect(subject.length).to eq(Devise.otp_length)
84
+ it 'is overrideable' do
85
+ def instance.send_two_factor_authentication_code
86
+ 'Code sent'
87
+ end
88
+ expect(instance.send_two_factor_authentication_code).to eq('Code sent')
26
89
  end
90
+ end
27
91
 
28
- context "with a known time" do
29
- let(:time) { 1392852756 }
92
+ describe '#provisioning_uri' do
93
+ shared_examples 'provisioning_uri' do |instance|
94
+ before do
95
+ instance.email = 'houdini@example.com'
96
+ instance.run_callbacks :create
97
+ end
30
98
 
31
- it "should return a known result" do
32
- expect(subject).to eq("0000000524562202".split(//).last(Devise.otp_length).join)
99
+ it "returns uri with user's email" do
100
+ expect(instance.provisioning_uri).
101
+ to match(%r{otpauth://totp/houdini@example.com\?secret=\w{16}})
102
+ end
103
+
104
+ it 'returns uri with issuer option' do
105
+ expect(instance.provisioning_uri('houdini')).
106
+ to match(%r{otpauth://totp/houdini\?secret=\w{16}$})
33
107
  end
34
- end
35
108
 
36
- context "with a known time yielding a result with less than 6 digits" do
37
- let(:time) { 1393065856 }
109
+ it 'returns uri with issuer option' do
110
+ require 'cgi'
38
111
 
39
- it "should return a known result padded with zeroes" do
40
- expect(subject).to eq("0000001608007672".split(//).last(Devise.otp_length).join)
112
+ uri = URI.parse(instance.provisioning_uri('houdini', issuer: 'Magic'))
113
+ params = CGI.parse(uri.query)
114
+
115
+ expect(uri.scheme).to eq('otpauth')
116
+ expect(uri.host).to eq('totp')
117
+ expect(uri.path).to eq('/houdini')
118
+ expect(params['issuer'].shift).to eq('Magic')
119
+ expect(params['secret'].shift).to match(/\w{16}/)
41
120
  end
42
121
  end
122
+
123
+ it_behaves_like 'provisioning_uri', GuestUser.new
124
+ it_behaves_like 'provisioning_uri', EncryptedUser.new
43
125
  end
44
- end
45
126
 
46
- describe Devise::Models::TwoFactorAuthenticatable, '#authenticate_otp' do
47
- let(:instance) { build_guest_user }
127
+ describe '#populate_otp_column' do
128
+ shared_examples 'populate_otp_column' do |klass|
129
+ let(:instance) { klass.new }
48
130
 
49
- before :each do
50
- instance.otp_secret_key = "2z6hxkdwi3uvrnpn"
51
- end
131
+ it 'populates otp_column on create' do
132
+ expect(instance.otp_secret_key).to be_nil
52
133
 
53
- def do_invoke code, options = {}
54
- instance.authenticate_otp(code, options)
55
- end
134
+ # populate_otp_column called via before_create
135
+ instance.run_callbacks :create
56
136
 
57
- it "should be able to authenticate a recently created code" do
58
- code = instance.otp_code
59
- expect(do_invoke(code)).to eq(true)
60
- end
137
+ expect(instance.otp_secret_key).to match(/\w{16}/)
138
+ end
61
139
 
62
- it "should not authenticate an old code" do
63
- code = instance.otp_code(1.minutes.ago.to_i)
64
- expect(do_invoke(code)).to eq(false)
65
- end
66
- end
140
+ it 'repopulates otp_column' do
141
+ instance.run_callbacks :create
142
+ original_key = instance.otp_secret_key
67
143
 
68
- describe Devise::Models::TwoFactorAuthenticatable, '#send_two_factor_authentication_code' do
69
- let(:instance) { build_guest_user }
144
+ instance.populate_otp_column
70
145
 
71
- it "should raise an error by default" do
72
- expect {
73
- instance.send_two_factor_authentication_code
74
- }.to raise_error(NotImplementedError)
146
+ expect(instance.otp_secret_key).to match(/\w{16}/)
147
+ expect(instance.otp_secret_key).to_not eq(original_key)
148
+ end
149
+ end
150
+
151
+ it_behaves_like 'populate_otp_column', GuestUser
152
+ it_behaves_like 'populate_otp_column', EncryptedUser
75
153
  end
76
154
 
77
- it "should be overrideable" do
78
- def instance.send_two_factor_authentication_code
79
- "Code sent"
155
+ describe '#max_login_attempts' do
156
+ let(:instance) { build_guest_user }
157
+
158
+ before do
159
+ @original_max_login_attempts = GuestUser.max_login_attempts
160
+ GuestUser.max_login_attempts = 3
80
161
  end
81
- expect(instance.send_two_factor_authentication_code).to eq("Code sent")
82
- end
83
- end
84
162
 
85
- describe Devise::Models::TwoFactorAuthenticatable, '#provisioning_uri' do
86
- let(:instance) { build_guest_user }
163
+ after { GuestUser.max_login_attempts = @original_max_login_attempts }
87
164
 
88
- before do
89
- instance.email = "houdini@example.com"
90
- instance.run_callbacks :create
91
- end
165
+ it 'returns class setting' do
166
+ expect(instance.max_login_attempts).to eq(3)
167
+ end
92
168
 
93
- it "should return uri with user's email" do
94
- expect(instance.provisioning_uri).to match(%r{otpauth://totp/houdini@example.com\?secret=\w{16}})
95
- end
169
+ it 'returns false as boolean' do
170
+ instance.second_factor_attempts_count = nil
171
+ expect(instance.max_login_attempts?).to be_falsey
172
+ instance.second_factor_attempts_count = 0
173
+ expect(instance.max_login_attempts?).to be_falsey
174
+ instance.second_factor_attempts_count = 1
175
+ expect(instance.max_login_attempts?).to be_falsey
176
+ instance.second_factor_attempts_count = 2
177
+ expect(instance.max_login_attempts?).to be_falsey
178
+ end
96
179
 
97
- it "should return uri with issuer option" do
98
- expect(instance.provisioning_uri("houdini")).to match(%r{otpauth://totp/houdini\?secret=\w{16}$})
180
+ it 'returns true as boolean after too many attempts' do
181
+ instance.second_factor_attempts_count = 3
182
+ expect(instance.max_login_attempts?).to be_truthy
183
+ instance.second_factor_attempts_count = 4
184
+ expect(instance.max_login_attempts?).to be_truthy
185
+ end
99
186
  end
100
187
 
101
- it "should return uri with issuer option" do
102
- require 'cgi'
188
+ describe '.has_one_time_password' do
189
+ context 'when encrypted: true option is passed' do
190
+ let(:instance) { EncryptedUser.new }
103
191
 
104
- uri = URI.parse(instance.provisioning_uri("houdini", issuer: 'Magic'))
105
- params = CGI::parse(uri.query)
192
+ it 'encrypts otp_secret_key with iv, salt, and encoding' do
193
+ instance.otp_secret_key = '2z6hxkdwi3uvrnpn'
106
194
 
107
- expect(uri.scheme).to eq("otpauth")
108
- expect(uri.host).to eq("totp")
109
- expect(uri.path).to eq("/houdini")
110
- expect(params['issuer'].shift).to eq('Magic')
111
- expect(params['secret'].shift).to match(%r{\w{16}})
112
- end
113
- end
195
+ expect(instance.encrypted_otp_secret_key).to match(/.{44}/)
196
+
197
+ expect(instance.encrypted_otp_secret_key_iv).to match(/.{24}/)
114
198
 
115
- describe Devise::Models::TwoFactorAuthenticatable, '#populate_otp_column' do
116
- let(:instance) { build_guest_user }
199
+ expect(instance.encrypted_otp_secret_key_salt).to match(/.{25}/)
200
+ end
117
201
 
118
- it "populates otp_column on create" do
119
- expect(instance.otp_secret_key).to be_nil
202
+ it 'does not encrypt a nil otp_secret_key' do
203
+ instance.otp_secret_key = nil
120
204
 
121
- instance.run_callbacks :create # populate_otp_column called via before_create
205
+ expect(instance.encrypted_otp_secret_key).to be_nil
122
206
 
123
- expect(instance.otp_secret_key).to match(%r{\w{16}})
124
- end
207
+ expect(instance.encrypted_otp_secret_key_iv).to be_nil
125
208
 
126
- it "repopulates otp_column" do
127
- instance.run_callbacks :create
128
- original_key = instance.otp_secret_key
209
+ expect(instance.encrypted_otp_secret_key_salt).to be_nil
210
+ end
129
211
 
130
- instance.populate_otp_column
212
+ it 'does not encrypt an empty otp_secret_key' do
213
+ instance.otp_secret_key = ''
131
214
 
132
- expect(instance.otp_secret_key).to match(%r{\w{16}})
133
- expect(instance.otp_secret_key).to_not eq(original_key)
134
- end
135
- end
215
+ expect(instance.encrypted_otp_secret_key).to eq ''
136
216
 
137
- describe Devise::Models::TwoFactorAuthenticatable, '#max_login_attempts' do
138
- let(:instance) { build_guest_user }
217
+ expect(instance.encrypted_otp_secret_key_iv).to be_nil
139
218
 
140
- before do
141
- @original_max_login_attempts = GuestUser.max_login_attempts
142
- GuestUser.max_login_attempts = 3
143
- end
219
+ expect(instance.encrypted_otp_secret_key_salt).to be_nil
220
+ end
144
221
 
145
- after { GuestUser.max_login_attempts = @original_max_login_attempts }
222
+ it 'raises an error when Devise.otp_secret_encryption_key is not set' do
223
+ allow(Devise).to receive(:otp_secret_encryption_key).and_return nil
146
224
 
147
- it "returns class setting" do
148
- expect(instance.max_login_attempts).to eq(3)
149
- end
225
+ # This error is raised by the encryptor gem
226
+ expect { instance.otp_secret_key = '2z6hxkdwi3uvrnpn' }.
227
+ to raise_error ArgumentError
228
+ end
150
229
 
151
- it "returns false as boolean" do
152
- instance.second_factor_attempts_count = nil
153
- expect(instance.max_login_attempts?).to be_falsey
154
- instance.second_factor_attempts_count = 0
155
- expect(instance.max_login_attempts?).to be_falsey
156
- instance.second_factor_attempts_count = 1
157
- expect(instance.max_login_attempts?).to be_falsey
158
- instance.second_factor_attempts_count = 2
159
- expect(instance.max_login_attempts?).to be_falsey
160
- end
230
+ it 'passes in the correct options to Encryptor' do
231
+ instance.otp_secret_key = 'testing'
232
+ iv = instance.encrypted_otp_secret_key_iv
233
+ salt = instance.encrypted_otp_secret_key_salt
234
+
235
+ encrypted = Encryptor.encrypt(
236
+ value: 'testing',
237
+ key: Devise.otp_secret_encryption_key,
238
+ iv: iv.unpack('m').first,
239
+ salt: salt.unpack('m').first
240
+ )
161
241
 
162
- it "returns true as boolean after too many attempts" do
163
- instance.second_factor_attempts_count = 3
164
- expect(instance.max_login_attempts?).to be_truthy
165
- instance.second_factor_attempts_count = 4
166
- expect(instance.max_login_attempts?).to be_truthy
242
+ expect(instance.encrypted_otp_secret_key).to eq [encrypted].pack('m')
243
+ end
244
+
245
+ it 'varies the iv per instance' do
246
+ instance.otp_secret_key = 'testing'
247
+ user2 = EncryptedUser.new
248
+ user2.otp_secret_key = 'testing'
249
+
250
+ expect(user2.encrypted_otp_secret_key_iv).
251
+ to_not eq instance.encrypted_otp_secret_key_iv
252
+ end
253
+
254
+ it 'varies the salt per instance' do
255
+ instance.otp_secret_key = 'testing'
256
+ user2 = EncryptedUser.new
257
+ user2.otp_secret_key = 'testing'
258
+
259
+ expect(user2.encrypted_otp_secret_key_salt).
260
+ to_not eq instance.encrypted_otp_secret_key_salt
261
+ end
262
+ end
167
263
  end
168
264
  end
@@ -0,0 +1,14 @@
1
+ class EncryptedUser
2
+ extend ActiveModel::Callbacks
3
+ include ActiveModel::Validations
4
+ include Devise::Models::TwoFactorAuthenticatable
5
+
6
+ define_model_callbacks :create
7
+ attr_accessor :encrypted_otp_secret_key,
8
+ :encrypted_otp_secret_key_iv,
9
+ :encrypted_otp_secret_key_salt,
10
+ :email,
11
+ :second_factor_attempts_count
12
+
13
+ has_one_time_password(encrypted: true)
14
+ end
@@ -1,7 +1,6 @@
1
1
  class User < ActiveRecord::Base
2
2
  devise :two_factor_authenticatable, :database_authenticatable, :registerable,
3
- :recoverable, :rememberable, :trackable, :validatable,
4
- :two_factor_authenticatable
3
+ :recoverable, :rememberable, :trackable, :validatable
5
4
 
6
5
  has_one_time_password
7
6
 
@@ -35,4 +35,7 @@ Dummy::Application.configure do
35
35
 
36
36
  # Print deprecation notices to the stderr
37
37
  config.active_support.deprecation = :stderr
38
+
39
+ # For testing session variables in Capybara specs
40
+ config.middleware.use RackSessionAccess::Middleware
38
41
  end