rodauth 1.19.1 → 1.20.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.
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