rodauth 1.19.1 → 1.20.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +72 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +100 -7
  5. data/doc/base.rdoc +25 -0
  6. data/doc/email_auth.rdoc +1 -1
  7. data/doc/email_base.rdoc +5 -1
  8. data/doc/internals.rdoc +2 -2
  9. data/doc/jwt_refresh.rdoc +35 -0
  10. data/doc/lockout.rdoc +3 -0
  11. data/doc/login_password_requirements_base.rdoc +4 -1
  12. data/doc/otp.rdoc +22 -39
  13. data/doc/recovery_codes.rdoc +15 -28
  14. data/doc/release_notes/1.20.0.txt +175 -0
  15. data/doc/remember.rdoc +3 -0
  16. data/doc/reset_password.rdoc +2 -1
  17. data/doc/single_session.rdoc +3 -0
  18. data/doc/verify_account.rdoc +4 -3
  19. data/doc/verify_login_change.rdoc +1 -1
  20. data/lib/rodauth.rb +33 -4
  21. data/lib/rodauth/features/base.rb +93 -10
  22. data/lib/rodauth/features/change_login.rb +1 -1
  23. data/lib/rodauth/features/confirm_password.rb +1 -1
  24. data/lib/rodauth/features/create_account.rb +2 -2
  25. data/lib/rodauth/features/disallow_password_reuse.rb +5 -3
  26. data/lib/rodauth/features/email_auth.rb +4 -2
  27. data/lib/rodauth/features/email_base.rb +12 -6
  28. data/lib/rodauth/features/jwt.rb +9 -0
  29. data/lib/rodauth/features/jwt_refresh.rb +142 -0
  30. data/lib/rodauth/features/lockout.rb +8 -4
  31. data/lib/rodauth/features/login_password_requirements_base.rb +1 -0
  32. data/lib/rodauth/features/otp.rb +63 -6
  33. data/lib/rodauth/features/recovery_codes.rb +1 -0
  34. data/lib/rodauth/features/remember.rb +20 -2
  35. data/lib/rodauth/features/reset_password.rb +5 -2
  36. data/lib/rodauth/features/single_session.rb +15 -2
  37. data/lib/rodauth/features/verify_account.rb +11 -6
  38. data/lib/rodauth/features/verify_login_change.rb +5 -3
  39. data/lib/rodauth/version.rb +2 -2
  40. data/spec/disallow_password_reuse_spec.rb +115 -28
  41. data/spec/email_auth_spec.rb +2 -2
  42. data/spec/jwt_refresh_spec.rb +256 -0
  43. data/spec/lockout_spec.rb +4 -4
  44. data/spec/login_spec.rb +52 -11
  45. data/spec/migrate/001_tables.rb +10 -0
  46. data/spec/migrate_travis/001_tables.rb +8 -0
  47. data/spec/remember_spec.rb +27 -0
  48. data/spec/reset_password_spec.rb +2 -2
  49. data/spec/rodauth_spec.rb +25 -1
  50. data/spec/single_session_spec.rb +20 -0
  51. data/spec/spec_helper.rb +29 -0
  52. data/spec/two_factor_spec.rb +57 -3
  53. data/spec/verify_account_spec.rb +18 -1
  54. data/spec/verify_login_change_spec.rb +2 -2
  55. data/templates/add-recovery-codes.str +1 -1
  56. data/templates/change-password.str +2 -2
  57. data/templates/login-confirm-field.str +2 -2
  58. data/templates/login-field.str +2 -2
  59. data/templates/otp-auth-code-field.str +2 -2
  60. data/templates/otp-setup.str +4 -3
  61. data/templates/password-confirm-field.str +2 -2
  62. data/templates/password-field.str +2 -2
  63. data/templates/recovery-auth.str +2 -2
  64. data/templates/reset-password-request.str +1 -1
  65. data/templates/sms-code-field.str +2 -2
  66. data/templates/sms-setup.str +2 -2
  67. data/templates/unlock-account-request.str +1 -1
  68. data/templates/unlock-account.str +1 -1
  69. data/templates/verify-account-resend.str +1 -1
  70. metadata +15 -5
data/spec/lockout_spec.rb CHANGED
@@ -48,7 +48,7 @@ describe 'Rodauth lockout feature' do
48
48
  email_link(/(\/unlock-account\?key=.+)$/).must_equal link
49
49
 
50
50
  visit link[0...-1]
51
- page.find('#error_flash').text.must_equal 'No matching unlock account key'
51
+ page.find('#error_flash').text.must_equal "There was an error unlocking your account: invalid or expired unlock account key"
52
52
 
53
53
  visit link
54
54
  click_button 'Unlock Account'
@@ -212,7 +212,7 @@ describe 'Rodauth lockout feature' do
212
212
  end
213
213
 
214
214
  res = json_request('/unlock-account-request', :login=>'foo@example.com')
215
- res.must_equal [401, {'error'=>"no matching login"}]
215
+ res.must_equal [401, {'error'=>"No matching login"}]
216
216
 
217
217
  res = json_login(:pass=>'1', :no_check=>true)
218
218
  res.must_equal [401, {'error'=>"There was an error logging in", "field-error"=>["password", "invalid password"]}]
@@ -231,14 +231,14 @@ describe 'Rodauth lockout feature' do
231
231
  end
232
232
 
233
233
  res = json_request('/unlock-account')
234
- res.must_equal [401, {'error'=>"No matching unlock account key"}]
234
+ res.must_equal [401, {'error'=>"There was an error unlocking your account: invalid or expired unlock account key"}]
235
235
 
236
236
  res = json_request('/unlock-account-request', :login=>'foo@example.com')
237
237
  res.must_equal [200, {'success'=>"An email has been sent to you with a link to unlock your account"}]
238
238
 
239
239
  link = email_link(/key=.+$/)
240
240
  res = json_request('/unlock-account', :key=>link[4...-1])
241
- res.must_equal [401, {'error'=>"No matching unlock account key"}]
241
+ res.must_equal [401, {'error'=>"There was an error unlocking your account: invalid or expired unlock account key"}]
242
242
 
243
243
  res = json_request('/unlock-account', :key=>link[4..-1])
244
244
  res.must_equal [200, {'success'=>"Your account has been unlocked"}]
data/spec/login_spec.rb CHANGED
@@ -15,6 +15,7 @@ describe 'Rodauth login feature' do
15
15
  login(:login=>'foo@example2.com', :visit=>false)
16
16
  page.find('#error_flash').text.must_equal 'There was an error logging in'
17
17
  page.html.must_include("no matching login")
18
+ page.all('[type=text]').first.value.must_equal 'foo@example2.com'
18
19
 
19
20
  login(:pass=>'012345678', :visit=>false)
20
21
  page.find('#error_flash').text.must_equal 'There was an error logging in'
@@ -38,6 +39,32 @@ describe 'Rodauth login feature' do
38
39
  rodauth do
39
40
  enable :login, :logout
40
41
  use_multi_phase_login? true
42
+ login_input_type 'email'
43
+ input_field_label_suffix ' (Required)'
44
+ input_field_error_class ' bad-input'
45
+ input_field_error_message_class 'err-msg'
46
+ mark_input_fields_as_required? true
47
+ field_attributes do |field|
48
+ if field == 'login'
49
+ 'custom_field="custom_value"'
50
+ else
51
+ super(field)
52
+ end
53
+ end
54
+ field_error_attributes do |field|
55
+ if field == 'login'
56
+ 'custom_error_field="custom_error_value"'
57
+ else
58
+ super(field)
59
+ end
60
+ end
61
+ formatted_field_error do |field, error|
62
+ if field == 'login'
63
+ super(field, error)
64
+ else
65
+ "<span class='err-msg2'>1#{error}2</span>"
66
+ end
67
+ end
41
68
  end
42
69
  roda do |r|
43
70
  r.rodauth
@@ -48,25 +75,39 @@ describe 'Rodauth login feature' do
48
75
  visit '/login'
49
76
  page.title.must_equal 'Login'
50
77
 
51
- page.all('input[type=password]').must_be :empty?
52
- fill_in 'Login', :with=>'foo2@example.com'
78
+ page.find('[custom_field=custom_value]').value.must_equal ''
79
+ page.all('[custom_error_field=custom_error_value]').must_be_empty
80
+ page.all('input[type=password]').must_be_empty
81
+ fill_in 'Login (Required)', :with=>'foo2@example.com'
53
82
  click_button 'Login'
54
83
  page.find('#error_flash').text.must_equal 'There was an error logging in'
55
- page.html.must_include("no matching login")
56
-
57
- page.all('input[type=password]').must_be :empty?
58
- fill_in 'Login', :with=>'foo@example.com'
84
+ page.find('[custom_field=custom_value]').value.must_equal 'foo2@example.com'
85
+ page.find('[custom_error_field=custom_error_value]').value.must_equal 'foo2@example.com'
86
+ page.find('[type=email]').value.must_equal 'foo2@example.com'
87
+ page.find('.bad-input').value.must_equal 'foo2@example.com'
88
+ page.find('.err-msg').text.must_equal 'no matching login'
89
+
90
+ page.all('input[type=password]').must_be_empty
91
+ fill_in 'Login (Required)', :with=>'foo@example.com'
59
92
  click_button 'Login'
60
93
  page.find('#notice_flash').text.must_equal 'Login recognized, please enter your password'
61
94
 
62
- page.all('input[type=text]').must_be :empty?
63
- fill_in 'Password', :with=>'012345678'
95
+ page.all('[custom_field=custom_value]').must_be_empty
96
+ page.all('[custom_error_field=custom_error_value]').must_be_empty
97
+ page.all('[aria-invalid=true]').must_be_empty
98
+ page.all('[aria-describedby]').must_be_empty
99
+ page.find('[required=required]').value.to_s.must_equal ''
100
+ page.all('input[type=text]').must_be_empty
101
+ fill_in 'Password (Required)', :with=>'012345678'
64
102
  click_button 'Login'
65
103
  page.find('#error_flash').text.must_equal 'There was an error logging in'
66
- page.html.must_include("invalid password")
104
+ page.find('[aria-invalid=true]').value.to_s.must_equal ''
105
+ page.find('[aria-describedby=password_error_message]').value.to_s.must_equal ''
106
+ page.all('[custom_error_field=custom_error_value]').must_be_empty
107
+ page.find('.err-msg2').text.must_equal '1invalid password2'
67
108
 
68
- page.all('input[type=text]').must_be :empty?
69
- fill_in 'Password', :with=>'0123456789'
109
+ page.all('input[type=text]').must_be_empty
110
+ fill_in 'Password (Required)', :with=>'0123456789'
70
111
  click_button 'Login'
71
112
  page.current_path.must_equal '/'
72
113
  page.find('#notice_flash').text.must_equal 'You have been logged in'
@@ -39,6 +39,14 @@ Sequel.migration do
39
39
  DateTime :email_last_sent, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
40
40
  end
41
41
 
42
+ # Used by the refresh token feature
43
+ create_table(:account_jwt_refresh_keys) do
44
+ primary_key :id, :type=>:Bignum
45
+ foreign_key :account_id, :accounts, :type=>:Bignum
46
+ String :key, :null=>false
47
+ DateTime :deadline, deadline_opts[1]
48
+ end
49
+
42
50
  # Used by the account verification feature
43
51
  create_table(:account_verification_keys) do
44
52
  foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
@@ -139,6 +147,7 @@ Sequel.migration do
139
147
  run "GRANT ALL ON account_statuses TO #{user}"
140
148
  run "GRANT ALL ON accounts TO #{user}"
141
149
  run "GRANT ALL ON account_password_reset_keys TO #{user}"
150
+ run "GRANT ALL ON account_jwt_refresh_keys TO #{user}"
142
151
  run "GRANT ALL ON account_verification_keys TO #{user}"
143
152
  run "GRANT ALL ON account_login_change_keys TO #{user}"
144
153
  run "GRANT ALL ON account_remember_keys TO #{user}"
@@ -167,6 +176,7 @@ Sequel.migration do
167
176
  :account_remember_keys,
168
177
  :account_login_change_keys,
169
178
  :account_verification_keys,
179
+ :account_jwt_refresh_keys,
170
180
  :account_password_reset_keys,
171
181
  :accounts,
172
182
  :account_statuses)
@@ -47,6 +47,14 @@ Sequel.migration do
47
47
  DateTime :email_last_sent, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
48
48
  end
49
49
 
50
+ # Used by the refresh token feature
51
+ create_table(:account_jwt_refresh_keys) do
52
+ primary_key :id, :type=>:Bignum
53
+ foreign_key :account_id, :accounts, :type=>:Bignum
54
+ String :key, :null=>false
55
+ DateTime :deadline, deadline_opts[1]
56
+ end
57
+
50
58
  create_table(:account_verification_keys) do
51
59
  foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
52
60
  String :key, :null=>false
@@ -2,8 +2,12 @@ require File.expand_path("spec_helper", File.dirname(__FILE__))
2
2
 
3
3
  describe 'Rodauth remember feature' do
4
4
  it "should support login via remember token" do
5
+ secret = nil
6
+ raw_before = Time.now - 100000000
5
7
  rodauth do
6
8
  enable :login, :remember
9
+ hmac_secret{secret}
10
+ raw_remember_token_deadline{raw_before}
7
11
  end
8
12
  roda do |r|
9
13
  r.rodauth
@@ -43,6 +47,19 @@ describe 'Rodauth remember feature' do
43
47
  visit '/'
44
48
  page.body.must_include 'Not Logged In'
45
49
 
50
+ secret = SecureRandom.random_bytes(32)
51
+ visit '/load'
52
+ page.body.must_include 'Not Logged In'
53
+
54
+ secret = nil
55
+ raw_before = Time.now + 100000000
56
+ login
57
+ visit '/remember'
58
+ choose 'Remember Me'
59
+ click_button 'Change Remember Setting'
60
+ remove_cookie('rack.session')
61
+
62
+ secret = SecureRandom.random_bytes(32)
46
63
  visit '/load'
47
64
  page.body.must_include 'Logged In via Remember'
48
65
 
@@ -75,6 +92,16 @@ describe 'Rodauth remember feature' do
75
92
  set_cookie('_remember', key)
76
93
  visit '/load'
77
94
  page.body.must_include 'Not Logged In'
95
+
96
+ login
97
+ visit '/remember'
98
+ choose 'Remember Me'
99
+ click_button 'Change Remember Setting'
100
+
101
+ secret = SecureRandom.random_bytes(32)
102
+ remove_cookie('rack.session')
103
+ visit '/load'
104
+ page.body.must_include 'Not Logged In'
78
105
  end
79
106
 
80
107
  it "should forget remember token when explicitly logging out" do
@@ -23,7 +23,7 @@ describe 'Rodauth reset_password feature' do
23
23
  link = email_link(/(\/reset-password\?key=.+)$/)
24
24
 
25
25
  visit link[0...-1]
26
- page.find('#error_flash').text.must_equal "invalid password reset key"
26
+ page.find('#error_flash').text.must_equal "There was an error resetting your password: invalid or expired password reset key"
27
27
 
28
28
  visit '/login'
29
29
  click_link 'Forgot Password?'
@@ -92,7 +92,7 @@ describe 'Rodauth reset_password feature' do
92
92
  DB[:account_password_reset_keys].update(:deadline => Time.now - 60).must_equal 1
93
93
  link = email_link(/(\/reset-password\?key=.+)$/)
94
94
  visit link
95
- page.find('#error_flash').text.must_equal "invalid password reset key"
95
+ page.find('#error_flash').text.must_equal "There was an error resetting your password: invalid or expired password reset key"
96
96
  end
97
97
 
98
98
  it "should support resetting passwords for accounts without confirmation" do
data/spec/rodauth_spec.rb CHANGED
@@ -57,6 +57,30 @@ describe 'Rodauth' do
57
57
  page.title.must_equal 'Login'
58
58
  end
59
59
 
60
+ it "should warn when using deprecated configuration methods" do
61
+ warning = nil
62
+ rodauth do
63
+ enable :email_auth
64
+ (class << self; self end).send(:define_method, :warn) do |*a|
65
+ warning = a.first
66
+ end
67
+ auth_class_eval do
68
+ define_method(:warn) do |*a|
69
+ warning = a.first
70
+ end
71
+ end
72
+ no_matching_email_auth_key_message 'foo'
73
+ end
74
+ roda do |r|
75
+ rodauth.no_matching_email_auth_key_message
76
+ end
77
+
78
+ warning.must_equal "Deprecated no_matching_email_auth_key_message method used during configuration, switch to using no_matching_email_auth_key_error_flash"
79
+ visit '/'
80
+ body.must_equal 'foo'
81
+ warning.must_equal "Deprecated no_matching_email_auth_key_message method called at runtime, switch to using no_matching_email_auth_key_error_flash"
82
+ end
83
+
60
84
  it "should pick up template changes if not caching templates" do
61
85
  begin
62
86
  @no_freeze = true
@@ -294,7 +318,7 @@ describe 'Rodauth' do
294
318
  auth_class = nil
295
319
  no_freeze!
296
320
  rodauth{auth_class = auth}
297
- roda(:csrf=>false, :flash=>false){}
321
+ roda(:csrf=>false, :flash=>false){|r|}
298
322
  Class.new(app).rodauth.must_equal auth_class
299
323
  end
300
324
 
@@ -2,8 +2,12 @@ require File.expand_path("spec_helper", File.dirname(__FILE__))
2
2
 
3
3
  describe 'Rodauth single session feature' do
4
4
  it "should limit accounts to a single logged in session" do
5
+ secret = nil
6
+ allow_raw = true
5
7
  rodauth do
6
8
  enable :login, :logout, :single_session
9
+ hmac_secret{secret}
10
+ allow_raw_single_session_key?{allow_raw}
7
11
  end
8
12
  roda do |r|
9
13
  rodauth.check_single_session
@@ -47,6 +51,22 @@ describe 'Rodauth single session feature' do
47
51
  visit '/clear'
48
52
  page.current_path.must_equal '/'
49
53
  page.body.must_include "Logged In"
54
+
55
+ secret = SecureRandom.random_bytes(32)
56
+ visit '/'
57
+ page.body.must_include "Logged In"
58
+
59
+ allow_raw = false
60
+ visit '/'
61
+ page.body.must_include "Not Logged"
62
+
63
+ login
64
+ page.body.must_include "Logged In"
65
+
66
+ allow_raw = true
67
+ secret = SecureRandom.random_bytes(32)
68
+ visit '/'
69
+ page.body.must_include "Not Logged"
50
70
  end
51
71
 
52
72
  it "should limit accounts to a single logged in session" do
data/spec/spec_helper.rb CHANGED
@@ -73,6 +73,7 @@ ENV['RACK_ENV'] = 'test'
73
73
  end
74
74
 
75
75
  Base = Class.new(Roda)
76
+ Base.opts[:check_dynamic_arity] = Base.opts[:check_arity] = :warn
76
77
  Base.plugin :flash
77
78
  Base.plugin :render, :layout_opts=>{:path=>'spec/views/layout.str'}
78
79
  Base.plugin(:not_found){raise "path #{request.path_info} not found"}
@@ -90,11 +91,20 @@ else
90
91
  Base.use Rack::Session::Cookie, :secret => '0123456789'
91
92
  end
92
93
 
94
+ unless defined?(Rack::Test::VERSION) && Rack::Test::VERSION >= '0.8'
95
+ class Rack::Test::Cookie
96
+ def path
97
+ ([*(@options['path'] == "" ? "/" : @options['path'])].first.split(',').first || '/').strip
98
+ end
99
+ end
100
+ end
101
+
93
102
  class Base
94
103
  attr_writer :title
95
104
  end
96
105
 
97
106
  JsonBase = Class.new(Roda)
107
+ JsonBase.opts[:check_dynamic_arity] = JsonBase.opts[:check_arity] = :warn
98
108
  JsonBase.plugin(:not_found){raise "path #{request.path_info} not found"}
99
109
 
100
110
  class Minitest::HooksSpec
@@ -256,6 +266,25 @@ class Minitest::HooksSpec
256
266
  res
257
267
  end
258
268
 
269
+ def jwt_refresh_login
270
+ res = json_login({:no_check => true})
271
+ jwt_refresh_validate_login(res)
272
+ res
273
+ end
274
+
275
+ def jwt_refresh_validate_login(res)
276
+ res.first.must_equal 200
277
+ res.last.keys.sort.must_equal ['access_token', 'refresh_token', 'success']
278
+ res.last['success'].must_equal 'You have been logged in'
279
+ res
280
+ end
281
+
282
+ def jwt_refresh_validate(res)
283
+ res.first.must_equal 200
284
+ res.last.keys.sort.must_equal ['access_token', 'refresh_token']
285
+ res
286
+ end
287
+
259
288
  def json_logout
260
289
  json_request("/logout").must_equal [200, {"success"=>'You have been logged out'}]
261
290
  end
@@ -3,7 +3,7 @@ require File.expand_path("spec_helper", File.dirname(__FILE__))
3
3
  require 'rotp'
4
4
 
5
5
  describe 'Rodauth OTP feature' do
6
- secret_length = ROTP::Base32.random_base32.length
6
+ secret_length = (ROTP::Base32.respond_to?(:random_base32) ? ROTP::Base32.random_base32 : ROTP::Base32.random).length
7
7
 
8
8
  def reset_otp_last_use
9
9
  DB[:account_otp_keys].update(:last_use=>Sequel.date_sub(Sequel::CURRENT_TIMESTAMP, :seconds=>600))
@@ -11,9 +11,13 @@ describe 'Rodauth OTP feature' do
11
11
 
12
12
  it "should allow two factor authentication setup, login, recovery, removal" do
13
13
  sms_phone = sms_message = nil
14
+ hmac_secret = '123'
14
15
  rodauth do
15
16
  enable :login, :logout, :otp, :recovery_codes, :sms_codes
16
17
  otp_drift 10
18
+ hmac_secret do
19
+ hmac_secret
20
+ end
17
21
  sms_send do |phone, msg|
18
22
  proc{super(phone, msg)}.must_raise NotImplementedError
19
23
  sms_phone = phone
@@ -57,6 +61,14 @@ describe 'Rodauth OTP feature' do
57
61
  page.find('#error_flash').text.must_equal 'Error setting up two factor authentication'
58
62
  page.html.must_include 'Invalid authentication code'
59
63
 
64
+ hmac_secret = "321"
65
+ fill_in 'Password', :with=>'0123456789'
66
+ fill_in 'Authentication Code', :with=>totp.now
67
+ click_button 'Setup Two Factor Authentication'
68
+ page.find('#error_flash').text.must_equal 'Error setting up two factor authentication'
69
+
70
+ secret = page.html.match(/Secret: ([a-z2-7]{#{secret_length}})/)[1]
71
+ totp = ROTP::TOTP.new(secret)
60
72
  fill_in 'Password', :with=>'0123456789'
61
73
  fill_in 'Authentication Code', :with=>totp.now
62
74
  click_button 'Setup Two Factor Authentication'
@@ -68,6 +80,8 @@ describe 'Rodauth OTP feature' do
68
80
  login
69
81
  page.current_path.must_equal '/otp-auth'
70
82
 
83
+ page.find_by_id('otp-auth-code')[:autocomplete].must_equal 'off'
84
+
71
85
  %w'/otp-disable /recovery-codes /otp-setup /sms-setup /sms-disable /sms-confirm'.each do |path|
72
86
  visit path
73
87
  page.find('#error_flash').text.must_equal 'You need to authenticate via 2nd factor before continuing.'
@@ -86,6 +100,14 @@ describe 'Rodauth OTP feature' do
86
100
  page.html.must_include 'Invalid authentication code'
87
101
  reset_otp_last_use
88
102
 
103
+ hmac_secret = '123'
104
+ fill_in 'Authentication Code', :with=>"#{totp.now[0..2]} #{totp.now[3..-1]}"
105
+ click_button 'Authenticate via 2nd Factor'
106
+ page.find('#error_flash').text.must_equal 'Error logging in via two factor authentication'
107
+ page.html.must_include 'Invalid authentication code'
108
+ reset_otp_last_use
109
+
110
+ hmac_secret = '321'
89
111
  fill_in 'Authentication Code', :with=>"#{totp.now[0..2]} #{totp.now[3..-1]}"
90
112
  click_button 'Authenticate via 2nd Factor'
91
113
  page.find('#notice_flash').text.must_equal 'You have been authenticated via 2nd factor'
@@ -1085,10 +1107,13 @@ describe 'Rodauth OTP feature' do
1085
1107
  end
1086
1108
 
1087
1109
  it "should allow two factor authentication via jwt" do
1088
- sms_phone = sms_message = sms_code = nil
1110
+ hmac_secret = sms_phone = sms_message = sms_code = nil
1089
1111
  rodauth do
1090
1112
  enable :login, :logout, :otp, :recovery_codes, :sms_codes
1091
1113
  otp_drift 10
1114
+ hmac_secret do
1115
+ hmac_secret
1116
+ end
1092
1117
  sms_send do |phone, msg|
1093
1118
  sms_phone = phone
1094
1119
  sms_message = msg
@@ -1124,7 +1149,7 @@ describe 'Rodauth OTP feature' do
1124
1149
  json_request(path).must_equal [403, {'error'=>'SMS authentication has not been setup yet.'}]
1125
1150
  end
1126
1151
 
1127
- secret = ROTP::Base32.random_base32
1152
+ secret = (ROTP::Base32.respond_to?(:random_base32) ? ROTP::Base32.random_base32 : ROTP::Base32.random.downcase)
1128
1153
  totp = ROTP::TOTP.new(secret)
1129
1154
 
1130
1155
  res = json_request('/otp-setup', :password=>'123456', :otp_secret=>secret)
@@ -1319,6 +1344,35 @@ describe 'Rodauth OTP feature' do
1319
1344
  [:account_otp_keys, :account_recovery_codes, :account_sms_codes].each do |t|
1320
1345
  DB[t].count.must_equal 0
1321
1346
  end
1347
+
1348
+ hmac_secret = "123"
1349
+ res = json_request('/otp-setup')
1350
+ secret = res[1].delete("otp_secret")
1351
+ raw_secret = res[1].delete("otp_raw_secret")
1352
+ res.must_equal [422, {'error'=>'Error setting up two factor authentication', "field-error"=>["otp_secret", 'invalid secret']}]
1353
+
1354
+ totp = ROTP::TOTP.new(secret)
1355
+ hmac_secret = "321"
1356
+ res = json_request('/otp-setup', :password=>'0123456789', :otp=>totp.now, :otp_secret=>secret, :otp_raw_secret=>raw_secret)
1357
+ res.must_equal [422, {'error'=>'Error setting up two factor authentication', "field-error"=>["otp_secret", 'invalid secret']}]
1358
+
1359
+ reset_otp_last_use
1360
+ hmac_secret = "123"
1361
+ res = json_request('/otp-setup', :password=>'0123456789', :otp=>totp.now, :otp_secret=>secret, :otp_raw_secret=>raw_secret)
1362
+ res.must_equal [200, {'success'=>'Two factor authentication is now setup'}]
1363
+ reset_otp_last_use
1364
+
1365
+ json_logout
1366
+ json_login
1367
+
1368
+ hmac_secret = "321"
1369
+ res = json_request('/otp-auth', :otp=>totp.now)
1370
+ res.must_equal [401, {'error'=>'Error logging in via two factor authentication', "field-error"=>["otp", 'Invalid authentication code']}]
1371
+
1372
+ hmac_secret = "123"
1373
+ res = json_request('/otp-auth', :otp=>totp.now)
1374
+ res.must_equal [200, {'success'=>'You have been authenticated via 2nd factor'}]
1375
+ json_request.must_equal [200, [1]]
1322
1376
  end
1323
1377
 
1324
1378
  it "should allow two factor authentication setup, login, recovery, removal" do