rodauth 1.19.1 → 1.20.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|