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.
- checksums.yaml +4 -4
- data/CHANGELOG +72 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +100 -7
- data/doc/base.rdoc +25 -0
- data/doc/email_auth.rdoc +1 -1
- data/doc/email_base.rdoc +5 -1
- data/doc/internals.rdoc +2 -2
- data/doc/jwt_refresh.rdoc +35 -0
- data/doc/lockout.rdoc +3 -0
- data/doc/login_password_requirements_base.rdoc +4 -1
- data/doc/otp.rdoc +22 -39
- data/doc/recovery_codes.rdoc +15 -28
- data/doc/release_notes/1.20.0.txt +175 -0
- data/doc/remember.rdoc +3 -0
- data/doc/reset_password.rdoc +2 -1
- data/doc/single_session.rdoc +3 -0
- data/doc/verify_account.rdoc +4 -3
- data/doc/verify_login_change.rdoc +1 -1
- data/lib/rodauth.rb +33 -4
- data/lib/rodauth/features/base.rb +93 -10
- data/lib/rodauth/features/change_login.rb +1 -1
- data/lib/rodauth/features/confirm_password.rb +1 -1
- data/lib/rodauth/features/create_account.rb +2 -2
- data/lib/rodauth/features/disallow_password_reuse.rb +5 -3
- data/lib/rodauth/features/email_auth.rb +4 -2
- data/lib/rodauth/features/email_base.rb +12 -6
- data/lib/rodauth/features/jwt.rb +9 -0
- data/lib/rodauth/features/jwt_refresh.rb +142 -0
- data/lib/rodauth/features/lockout.rb +8 -4
- data/lib/rodauth/features/login_password_requirements_base.rb +1 -0
- data/lib/rodauth/features/otp.rb +63 -6
- data/lib/rodauth/features/recovery_codes.rb +1 -0
- data/lib/rodauth/features/remember.rb +20 -2
- data/lib/rodauth/features/reset_password.rb +5 -2
- data/lib/rodauth/features/single_session.rb +15 -2
- data/lib/rodauth/features/verify_account.rb +11 -6
- data/lib/rodauth/features/verify_login_change.rb +5 -3
- data/lib/rodauth/version.rb +2 -2
- data/spec/disallow_password_reuse_spec.rb +115 -28
- data/spec/email_auth_spec.rb +2 -2
- data/spec/jwt_refresh_spec.rb +256 -0
- data/spec/lockout_spec.rb +4 -4
- data/spec/login_spec.rb +52 -11
- data/spec/migrate/001_tables.rb +10 -0
- data/spec/migrate_travis/001_tables.rb +8 -0
- data/spec/remember_spec.rb +27 -0
- data/spec/reset_password_spec.rb +2 -2
- data/spec/rodauth_spec.rb +25 -1
- data/spec/single_session_spec.rb +20 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/two_factor_spec.rb +57 -3
- data/spec/verify_account_spec.rb +18 -1
- data/spec/verify_login_change_spec.rb +2 -2
- data/templates/add-recovery-codes.str +1 -1
- data/templates/change-password.str +2 -2
- data/templates/login-confirm-field.str +2 -2
- data/templates/login-field.str +2 -2
- data/templates/otp-auth-code-field.str +2 -2
- data/templates/otp-setup.str +4 -3
- data/templates/password-confirm-field.str +2 -2
- data/templates/password-field.str +2 -2
- data/templates/recovery-auth.str +2 -2
- data/templates/reset-password-request.str +1 -1
- data/templates/sms-code-field.str +2 -2
- data/templates/sms-setup.str +2 -2
- data/templates/unlock-account-request.str +1 -1
- data/templates/unlock-account.str +1 -1
- data/templates/verify-account-resend.str +1 -1
- 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
|
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'=>"
|
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'=>"
|
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'=>"
|
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.
|
52
|
-
|
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.
|
56
|
-
|
57
|
-
page.
|
58
|
-
|
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('
|
63
|
-
|
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.
|
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]').
|
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'
|
data/spec/migrate/001_tables.rb
CHANGED
@@ -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
|
data/spec/remember_spec.rb
CHANGED
@@ -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
|
data/spec/reset_password_spec.rb
CHANGED
@@ -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
|
|
data/spec/single_session_spec.rb
CHANGED
@@ -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
|
data/spec/two_factor_spec.rb
CHANGED
@@ -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
|