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
@@ -32,6 +32,7 @@ module Rodauth
|
|
32
32
|
view 'recovery-codes', 'View Authentication Recovery Codes', 'recovery_codes'
|
33
33
|
|
34
34
|
auth_value_method :add_recovery_codes_param, 'add'
|
35
|
+
auth_value_method :add_recovery_codes_heading, '<h2>Add Additional Recovery Codes</h2>'
|
35
36
|
auth_value_method :invalid_recovery_code_message, "Invalid recovery code"
|
36
37
|
auth_value_method :recovery_codes_limit, 16
|
37
38
|
auth_value_method :recovery_codes_column, :code
|
@@ -16,6 +16,7 @@ module Rodauth
|
|
16
16
|
after 'load_memory'
|
17
17
|
redirect
|
18
18
|
|
19
|
+
auth_value_method :raw_remember_token_deadline, nil
|
19
20
|
auth_value_method :remember_cookie_options, {}
|
20
21
|
auth_value_method :extend_remember_deadline?, false
|
21
22
|
auth_value_method :remember_period, {:days=>14}
|
@@ -88,7 +89,22 @@ module Rodauth
|
|
88
89
|
id, key = cookie.split('_', 2)
|
89
90
|
return unless id && key
|
90
91
|
|
91
|
-
|
92
|
+
actual, deadline = active_remember_key_ds(id).get([remember_key_column, remember_deadline_column])
|
93
|
+
unless actual
|
94
|
+
forget_login
|
95
|
+
return
|
96
|
+
end
|
97
|
+
|
98
|
+
if hmac_secret
|
99
|
+
unless valid = timing_safe_eql?(key, compute_hmac(actual))
|
100
|
+
unless raw_remember_token_deadline && raw_remember_token_deadline > convert_timestamp(deadline)
|
101
|
+
forget_login
|
102
|
+
return
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
unless valid || timing_safe_eql?(key, actual)
|
92
108
|
forget_login
|
93
109
|
return
|
94
110
|
end
|
@@ -117,7 +133,9 @@ module Rodauth
|
|
117
133
|
def remember_login
|
118
134
|
get_remember_key
|
119
135
|
opts = Hash[remember_cookie_options]
|
120
|
-
|
136
|
+
key = remember_key_value
|
137
|
+
key = compute_hmac(key) if hmac_secret
|
138
|
+
opts[:value] = "#{account_id}_#{key}"
|
121
139
|
opts[:expires] = convert_timestamp(active_remember_key_ds.get(remember_deadline_column))
|
122
140
|
::Rack::Utils.set_cookie_header!(response.headers, remember_cookie_key, opts)
|
123
141
|
end
|
@@ -4,11 +4,14 @@ module Rodauth
|
|
4
4
|
Feature.define(:reset_password, :ResetPassword) do
|
5
5
|
depends :login, :email_base, :login_password_requirements_base
|
6
6
|
|
7
|
+
def_deprecated_alias :no_matching_reset_password_key_error_flash, :no_matching_reset_password_key_message
|
8
|
+
|
7
9
|
notice_flash "Your password has been reset"
|
8
10
|
notice_flash "An email has been sent to you with a link to reset the password for your account", 'reset_password_email_sent'
|
9
11
|
error_flash "There was an error resetting your password"
|
10
12
|
error_flash "There was an error requesting a password reset", 'reset_password_request'
|
11
13
|
error_flash "An email has recently been sent to you with a link to reset your password", 'reset_password_email_recently_sent'
|
14
|
+
error_flash "There was an error resetting your password: invalid or expired password reset key", 'no_matching_reset_password_key'
|
12
15
|
loaded_templates %w'reset-password-request reset-password password-field password-confirm-field reset-password-email'
|
13
16
|
view 'reset-password', 'Reset Password'
|
14
17
|
view 'reset-password-request', 'Request Password Reset', 'reset_password_request'
|
@@ -26,7 +29,6 @@ module Rodauth
|
|
26
29
|
|
27
30
|
auth_value_method :reset_password_deadline_column, :deadline
|
28
31
|
auth_value_method :reset_password_deadline_interval, {:days=>1}
|
29
|
-
auth_value_method :no_matching_reset_password_key_message, "invalid password reset key"
|
30
32
|
auth_value_method :reset_password_email_subject, 'Reset Password'
|
31
33
|
auth_value_method :reset_password_key_param, 'key'
|
32
34
|
auth_value_method :reset_password_autologin?, false
|
@@ -34,6 +36,7 @@ module Rodauth
|
|
34
36
|
auth_value_method :reset_password_id_column, :id
|
35
37
|
auth_value_method :reset_password_key_column, :key
|
36
38
|
auth_value_method :reset_password_email_last_sent_column, nil
|
39
|
+
auth_value_method :reset_password_explanatory_text, "<p>If you have forgotten your password, you can request a password reset:</p>"
|
37
40
|
auth_value_method :reset_password_skip_resend_email_within, 300
|
38
41
|
session_key :reset_password_session_key, :reset_password_key
|
39
42
|
|
@@ -105,7 +108,7 @@ module Rodauth
|
|
105
108
|
reset_password_view
|
106
109
|
else
|
107
110
|
session[reset_password_session_key] = nil
|
108
|
-
set_redirect_error_flash
|
111
|
+
set_redirect_error_flash no_matching_reset_password_key_error_flash
|
109
112
|
redirect require_login_redirect
|
110
113
|
end
|
111
114
|
end
|
@@ -5,6 +5,7 @@ module Rodauth
|
|
5
5
|
error_flash 'This session has been logged out as another session has become active'
|
6
6
|
redirect
|
7
7
|
|
8
|
+
auth_value_method :allow_raw_single_session_key?, false
|
8
9
|
auth_value_method :single_session_id_column, :id
|
9
10
|
auth_value_method :single_session_key_column, :key
|
10
11
|
session_key :single_session_session_key, :single_session_key
|
@@ -35,7 +36,14 @@ module Rodauth
|
|
35
36
|
end
|
36
37
|
true
|
37
38
|
elsif current_key
|
38
|
-
|
39
|
+
if hmac_secret
|
40
|
+
valid = timing_safe_eql?(single_session_key, compute_hmac(current_key))
|
41
|
+
if !valid && !allow_raw_single_session_key?
|
42
|
+
return false
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
valid || timing_safe_eql?(single_session_key, current_key)
|
39
47
|
end
|
40
48
|
end
|
41
49
|
|
@@ -53,7 +61,7 @@ module Rodauth
|
|
53
61
|
|
54
62
|
def update_single_session_key
|
55
63
|
key = random_key
|
56
|
-
|
64
|
+
set_single_session_key(key)
|
57
65
|
if single_session_ds.update(single_session_key_column=>key) == 0
|
58
66
|
# Don't handle uniqueness violations here. While we could get the stored key from the
|
59
67
|
# database, it could lead to two sessions sharing the same key, which this feature is
|
@@ -74,6 +82,11 @@ module Rodauth
|
|
74
82
|
super if defined?(super)
|
75
83
|
end
|
76
84
|
|
85
|
+
def set_single_session_key(data)
|
86
|
+
data = compute_hmac(data) if hmac_secret
|
87
|
+
set_session_value(single_session_session_key, data)
|
88
|
+
end
|
89
|
+
|
77
90
|
def update_session
|
78
91
|
super
|
79
92
|
update_single_session_key
|
@@ -4,9 +4,16 @@ module Rodauth
|
|
4
4
|
Feature.define(:verify_account, :VerifyAccount) do
|
5
5
|
depends :login, :create_account, :email_base
|
6
6
|
|
7
|
+
def_deprecated_alias :attempt_to_create_unverified_account_error_flash, :attempt_to_create_unverified_account_notice_message
|
8
|
+
def_deprecated_alias :attempt_to_login_to_unverified_account_error_flash, :attempt_to_login_to_unverified_account_notice_message
|
9
|
+
def_deprecated_alias :no_matching_verify_account_key_error_flash, :no_matching_verify_account_key_message
|
10
|
+
|
7
11
|
error_flash "Unable to verify account"
|
8
12
|
error_flash "Unable to resend verify account email", 'verify_account_resend'
|
9
13
|
error_flash "An email has recently been sent to you with a link to verify your account", 'verify_account_email_recently_sent'
|
14
|
+
error_flash "There was an error verifying your account: invalid verify account key", 'no_matching_verify_account_key'
|
15
|
+
error_flash "The account you tried to create is currently awaiting verification", 'attempt_to_create_unverified_account'
|
16
|
+
error_flash "The account you tried to login with is currently awaiting verification", 'attempt_to_login_to_unverified_account'
|
10
17
|
notice_flash "Your account has been verified"
|
11
18
|
notice_flash "An email has been sent to you with a link to verify your account", 'verify_account_email_sent'
|
12
19
|
loaded_templates %w'verify-account verify-account-resend verify-account-email'
|
@@ -24,9 +31,6 @@ module Rodauth
|
|
24
31
|
redirect(:verify_account_email_sent){default_post_email_redirect}
|
25
32
|
redirect(:verify_account_email_recently_sent){default_post_email_redirect}
|
26
33
|
|
27
|
-
auth_value_method :no_matching_verify_account_key_message, "invalid verify account key"
|
28
|
-
auth_value_method :attempt_to_create_unverified_account_notice_message, "The account you tried to create is currently awaiting verification"
|
29
|
-
auth_value_method :attempt_to_login_to_unverified_account_notice_message, "The account you tried to login with is currently awaiting verification"
|
30
34
|
auth_value_method :verify_account_email_subject, 'Verify Account'
|
31
35
|
auth_value_method :verify_account_key_param, 'key'
|
32
36
|
auth_value_method :verify_account_autologin?, true
|
@@ -35,6 +39,7 @@ module Rodauth
|
|
35
39
|
auth_value_method :verify_account_email_last_sent_column, nil
|
36
40
|
auth_value_method :verify_account_skip_resend_email_within, 300
|
37
41
|
auth_value_method :verify_account_key_column, :key
|
42
|
+
auth_value_method :verify_account_resend_explanatory_text, "<p>If you no longer have the email to verify the account, you can request that it be resent to you:</p>"
|
38
43
|
session_key :verify_account_session_key, :verify_account_key
|
39
44
|
auth_value_method :verify_account_set_password?, false
|
40
45
|
|
@@ -102,7 +107,7 @@ module Rodauth
|
|
102
107
|
verify_account_view
|
103
108
|
else
|
104
109
|
session[verify_account_session_key] = nil
|
105
|
-
set_redirect_error_flash
|
110
|
+
set_redirect_error_flash no_matching_verify_account_key_error_flash
|
106
111
|
redirect require_login_redirect
|
107
112
|
end
|
108
113
|
end
|
@@ -180,7 +185,7 @@ module Rodauth
|
|
180
185
|
def new_account(login)
|
181
186
|
if account_from_login(login) && allow_resending_verify_account_email?
|
182
187
|
set_redirect_error_status(unopen_account_error_status)
|
183
|
-
set_error_flash
|
188
|
+
set_error_flash attempt_to_create_unverified_account_error_flash
|
184
189
|
response.write resend_verify_account_view
|
185
190
|
request.halt
|
186
191
|
end
|
@@ -251,7 +256,7 @@ module Rodauth
|
|
251
256
|
def before_login_attempt
|
252
257
|
unless open_account?
|
253
258
|
set_redirect_error_status(unopen_account_error_status)
|
254
|
-
set_error_flash
|
259
|
+
set_error_flash attempt_to_login_to_unverified_account_error_flash
|
255
260
|
response.write resend_verify_account_view
|
256
261
|
request.halt
|
257
262
|
end
|
@@ -4,8 +4,11 @@ module Rodauth
|
|
4
4
|
Feature.define(:verify_login_change, :VerifyLoginChange) do
|
5
5
|
depends :change_login, :email_base
|
6
6
|
|
7
|
+
def_deprecated_alias :no_matching_verify_login_change_key_error_flash, :no_matching_verify_login_change_key_message
|
8
|
+
|
7
9
|
error_flash "Unable to verify login change"
|
8
10
|
error_flash "Unable to change login as there is already an account with the new login", :verify_login_change_duplicate_account
|
11
|
+
error_flash "There was an error verifying your login change: invalid verify login change key", 'no_matching_verify_login_change_key'
|
9
12
|
notice_flash "Your login change has been verified"
|
10
13
|
loaded_templates %w'verify-login-change verify-login-change-email'
|
11
14
|
view 'verify-login-change', 'Verify Login Change'
|
@@ -18,7 +21,6 @@ module Rodauth
|
|
18
21
|
redirect
|
19
22
|
redirect(:verify_login_change_duplicate_account){require_login_redirect}
|
20
23
|
|
21
|
-
auth_value_method :no_matching_verify_login_change_key_message, "invalid verify login change key"
|
22
24
|
auth_value_method :verify_login_change_autologin?, false
|
23
25
|
auth_value_method :verify_login_change_deadline_column, :deadline
|
24
26
|
auth_value_method :verify_login_change_deadline_interval, {:days=>1}
|
@@ -64,7 +66,7 @@ module Rodauth
|
|
64
66
|
verify_login_change_view
|
65
67
|
else
|
66
68
|
session[verify_login_change_session_key] = nil
|
67
|
-
set_redirect_error_flash
|
69
|
+
set_redirect_error_flash no_matching_verify_login_change_key_error_flash
|
68
70
|
redirect require_login_redirect
|
69
71
|
end
|
70
72
|
end
|
@@ -147,7 +149,7 @@ module Rodauth
|
|
147
149
|
|
148
150
|
def update_login(login)
|
149
151
|
if _account_from_login(login)
|
150
|
-
@login_requirement_message =
|
152
|
+
@login_requirement_message = already_an_account_with_this_login_message
|
151
153
|
return false
|
152
154
|
end
|
153
155
|
|
data/lib/rodauth/version.rb
CHANGED
@@ -6,11 +6,11 @@ module Rodauth
|
|
6
6
|
MAJOR = 1
|
7
7
|
|
8
8
|
# The minor version of Rodauth, updated for new feature releases of Rodauth.
|
9
|
-
MINOR =
|
9
|
+
MINOR = 20
|
10
10
|
|
11
11
|
# The patch version of Rodauth, updated only for bug fixes from the last
|
12
12
|
# feature release.
|
13
|
-
TINY =
|
13
|
+
TINY = 0
|
14
14
|
|
15
15
|
# The full version of Rodauth as a string
|
16
16
|
VERSION = "#{MAJOR}.#{MINOR}.#{TINY}".freeze
|
@@ -54,39 +54,126 @@ describe 'Rodauth disallow_password_reuse feature' do
|
|
54
54
|
DB[table].get{count(:id)}.must_equal 0
|
55
55
|
end
|
56
56
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
57
|
+
[true, false].each do |ph|
|
58
|
+
it "should handle create account when account_password_hash_column is #{ph}" do
|
59
|
+
rodauth do
|
60
|
+
enable :login, :create_account, :change_password, :disallow_password_reuse
|
61
|
+
if ENV['RODAUTH_SEPARATE_SCHEMA']
|
62
|
+
previous_password_hash_table Sequel[:rodauth_test_password][:account_previous_password_hashes]
|
63
|
+
end
|
64
|
+
account_password_hash_column :ph if ph
|
65
|
+
change_password_requires_password? false
|
62
66
|
end
|
63
|
-
|
64
|
-
|
67
|
+
roda do |r|
|
68
|
+
r.rodauth
|
69
|
+
r.root{view :content=>""}
|
70
|
+
end
|
71
|
+
|
72
|
+
visit '/create-account'
|
73
|
+
fill_in 'Login', :with=>'bar@example.com'
|
74
|
+
fill_in 'Confirm Login', :with=>'bar@example.com'
|
75
|
+
fill_in 'Password', :with=>'0123456789'
|
76
|
+
fill_in 'Confirm Password', :with=>'0123456789'
|
77
|
+
click_button 'Create Account'
|
78
|
+
page.current_path.must_equal '/'
|
79
|
+
page.find('#notice_flash').text.must_equal "Your account has been created"
|
80
|
+
|
81
|
+
visit '/change-password'
|
82
|
+
fill_in 'New Password', :with=>"012345678"
|
83
|
+
fill_in 'Confirm Password', :with=>"012345678"
|
84
|
+
click_button 'Change Password'
|
85
|
+
page.find('#notice_flash').text.must_equal "Your password has been changed"
|
86
|
+
|
87
|
+
visit '/change-password'
|
88
|
+
fill_in 'New Password', :with=>"0123456789"
|
89
|
+
fill_in 'Confirm Password', :with=>"0123456789"
|
90
|
+
click_button 'Change Password'
|
91
|
+
page.html.must_include("invalid password, does not meet requirements (same as previous password)")
|
65
92
|
end
|
66
|
-
|
67
|
-
|
68
|
-
|
93
|
+
|
94
|
+
it "should handle verify account when account_password_hash_column is #{ph}" do
|
95
|
+
rodauth do
|
96
|
+
enable :login, :verify_account, :change_password, :disallow_password_reuse
|
97
|
+
if ENV['RODAUTH_SEPARATE_SCHEMA']
|
98
|
+
previous_password_hash_table Sequel[:rodauth_test_password][:account_previous_password_hashes]
|
99
|
+
end
|
100
|
+
account_password_hash_column :ph if ph
|
101
|
+
change_password_requires_password? false
|
102
|
+
end
|
103
|
+
roda do |r|
|
104
|
+
r.rodauth
|
105
|
+
r.root{view :content=>""}
|
106
|
+
end
|
107
|
+
|
108
|
+
visit '/create-account'
|
109
|
+
fill_in 'Login', :with=>'bar@example.com'
|
110
|
+
fill_in 'Confirm Login', :with=>'bar@example.com'
|
111
|
+
fill_in 'Password', :with=>'0123456789'
|
112
|
+
fill_in 'Confirm Password', :with=>'0123456789'
|
113
|
+
click_button 'Create Account'
|
114
|
+
page.current_path.must_equal '/'
|
115
|
+
page.find('#notice_flash').text.must_equal "An email has been sent to you with a link to verify your account"
|
116
|
+
link = email_link(/(\/verify-account\?key=.+)$/, 'bar@example.com')
|
117
|
+
|
118
|
+
visit link
|
119
|
+
click_button 'Verify Account'
|
120
|
+
page.find('#notice_flash').text.must_equal "Your account has been verified"
|
121
|
+
page.current_path.must_equal '/'
|
122
|
+
|
123
|
+
visit '/change-password'
|
124
|
+
fill_in 'New Password', :with=>"012345678"
|
125
|
+
fill_in 'Confirm Password', :with=>"012345678"
|
126
|
+
click_button 'Change Password'
|
127
|
+
page.find('#notice_flash').text.must_equal "Your password has been changed"
|
128
|
+
|
129
|
+
visit '/change-password'
|
130
|
+
fill_in 'New Password', :with=>"0123456789"
|
131
|
+
fill_in 'Confirm Password', :with=>"0123456789"
|
132
|
+
click_button 'Change Password'
|
133
|
+
page.html.must_include("invalid password, does not meet requirements (same as previous password)")
|
69
134
|
end
|
70
135
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
136
|
+
it "should handle verify account when account_password_hash_column is #{ph} and verify_account_set_password? is true" do
|
137
|
+
rodauth do
|
138
|
+
enable :login, :verify_account, :change_password, :disallow_password_reuse
|
139
|
+
if ENV['RODAUTH_SEPARATE_SCHEMA']
|
140
|
+
previous_password_hash_table Sequel[:rodauth_test_password][:account_previous_password_hashes]
|
141
|
+
end
|
142
|
+
account_password_hash_column :ph if ph
|
143
|
+
change_password_requires_password? false
|
144
|
+
verify_account_set_password? true
|
145
|
+
end
|
146
|
+
roda do |r|
|
147
|
+
r.rodauth
|
148
|
+
r.root{view :content=>""}
|
149
|
+
end
|
79
150
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
151
|
+
visit '/create-account'
|
152
|
+
fill_in 'Login', :with=>'bar@example.com'
|
153
|
+
fill_in 'Confirm Login', :with=>'bar@example.com'
|
154
|
+
click_button 'Create Account'
|
155
|
+
page.current_path.must_equal '/'
|
156
|
+
page.find('#notice_flash').text.must_equal "An email has been sent to you with a link to verify your account"
|
157
|
+
link = email_link(/(\/verify-account\?key=.+)$/, 'bar@example.com')
|
85
158
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
159
|
+
visit link
|
160
|
+
fill_in 'Password', :with=>'0123456789'
|
161
|
+
fill_in 'Confirm Password', :with=>'0123456789'
|
162
|
+
click_button 'Verify Account'
|
163
|
+
page.find('#notice_flash').text.must_equal "Your account has been verified"
|
164
|
+
page.current_path.must_equal '/'
|
165
|
+
|
166
|
+
visit '/change-password'
|
167
|
+
fill_in 'New Password', :with=>"012345678"
|
168
|
+
fill_in 'Confirm Password', :with=>"012345678"
|
169
|
+
click_button 'Change Password'
|
170
|
+
page.find('#notice_flash').text.must_equal "Your password has been changed"
|
171
|
+
|
172
|
+
visit '/change-password'
|
173
|
+
fill_in 'New Password', :with=>"0123456789"
|
174
|
+
fill_in 'Confirm Password', :with=>"0123456789"
|
175
|
+
click_button 'Change Password'
|
176
|
+
page.html.must_include("invalid password, does not meet requirements (same as previous password)")
|
177
|
+
end
|
91
178
|
end
|
92
179
|
end
|
data/spec/email_auth_spec.rb
CHANGED
@@ -26,7 +26,7 @@ describe 'Rodauth email auth feature' do
|
|
26
26
|
link = email_link(/(\/email-auth\?key=.+)$/)
|
27
27
|
|
28
28
|
visit link[0...-1]
|
29
|
-
page.find('#error_flash').text.must_equal "invalid email authentication key"
|
29
|
+
page.find('#error_flash').text.must_equal "There was an error logging you in: invalid email authentication key"
|
30
30
|
|
31
31
|
visit '/login'
|
32
32
|
fill_in 'Login', :with=>'foo@example.com'
|
@@ -105,7 +105,7 @@ describe 'Rodauth email auth feature' do
|
|
105
105
|
link = email_link(/(\/email-auth\?key=.+)$/)
|
106
106
|
|
107
107
|
visit link[0...-1]
|
108
|
-
page.find('#error_flash').text.must_equal "invalid email authentication key"
|
108
|
+
page.find('#error_flash').text.must_equal "There was an error logging you in: invalid email authentication key"
|
109
109
|
|
110
110
|
visit '/login'
|
111
111
|
fill_in 'Login', :with=>'foo@example.com'
|
@@ -0,0 +1,256 @@
|
|
1
|
+
require File.expand_path("spec_helper", File.dirname(__FILE__))
|
2
|
+
|
3
|
+
describe 'Rodauth login feature' do
|
4
|
+
it "should not have jwt refresh feature assume JWT token given during Basic/Digest authentication" do
|
5
|
+
rodauth do
|
6
|
+
enable :login, :logout, :jwt_refresh
|
7
|
+
end
|
8
|
+
roda(:jwt) do |r|
|
9
|
+
rodauth.require_authentication
|
10
|
+
'1'
|
11
|
+
end
|
12
|
+
|
13
|
+
res = json_request("/jwt-refresh", :headers=>{'HTTP_AUTHORIZATION'=>'Basic foo'})
|
14
|
+
res.must_equal [401, {'error'=>'Please login to continue'}]
|
15
|
+
|
16
|
+
res = json_request("/", :headers=>{'HTTP_AUTHORIZATION'=>'Digest foo'})
|
17
|
+
res.must_equal [401, {'error'=>'Please login to continue'}]
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should require json request content type in only json mode for rodauth endpoints only" do
|
21
|
+
oj = false
|
22
|
+
rodauth do
|
23
|
+
enable :login, :logout, :jwt_refresh
|
24
|
+
jwt_secret '1'
|
25
|
+
json_response_success_key 'success'
|
26
|
+
only_json?{oj}
|
27
|
+
end
|
28
|
+
roda(:csrf=>false, :json=>true) do |r|
|
29
|
+
r.rodauth
|
30
|
+
rodauth.require_authentication
|
31
|
+
'1'
|
32
|
+
end
|
33
|
+
|
34
|
+
res = json_request("/", :content_type=>'application/x-www-form-urlencoded', :include_headers=>true, :method=>'GET')
|
35
|
+
res[1].delete('Set-Cookie')
|
36
|
+
res.must_equal [302, {"Content-Type"=>'text/html', "Content-Length"=>'0', "Location"=>"/login",}, []]
|
37
|
+
|
38
|
+
res = json_request("/", :content_type=>'application/vnd.api+json', :method=>'GET')
|
39
|
+
res.must_equal [400, ['{"error":"Please login to continue"}']]
|
40
|
+
|
41
|
+
oj = true
|
42
|
+
|
43
|
+
res = json_request("/", :content_type=>'application/x-www-form-urlencoded', :method=>'GET')
|
44
|
+
res.must_equal [400, ['{"error":"Please login to continue"}']]
|
45
|
+
|
46
|
+
res = json_request("/", :method=>'GET')
|
47
|
+
res.must_equal [400, {'error'=>'Please login to continue'}]
|
48
|
+
|
49
|
+
res = json_request("/login", :content_type=>'application/x-www-form-urlencoded', :include_headers=>true, :method=>'GET')
|
50
|
+
msg = "Only JSON format requests are allowed"
|
51
|
+
res[1].delete('Set-Cookie')
|
52
|
+
res.must_equal [400, {"Content-Type"=>'text/html', "Content-Length"=>msg.length.to_s}, [msg]]
|
53
|
+
|
54
|
+
jwt_refresh_login
|
55
|
+
|
56
|
+
res = json_request("/", :content_type=>'application/x-www-form-urlencoded', :include_headers=>true, :method=>'GET')
|
57
|
+
# res.must_equal [200, {"Content-Type"=>'text/html', "Content-Length"=>'1'}, ['1']]
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should allow non-json requests if only_json? is false" do
|
61
|
+
rodauth do
|
62
|
+
enable :login, :logout, :jwt_refresh
|
63
|
+
jwt_secret '1'
|
64
|
+
only_json? false
|
65
|
+
end
|
66
|
+
roda(:jwt_html) do |r|
|
67
|
+
r.rodauth
|
68
|
+
rodauth.require_authentication
|
69
|
+
view(:content=>'1')
|
70
|
+
end
|
71
|
+
|
72
|
+
login
|
73
|
+
page.find('#notice_flash').text.must_equal 'You have been logged in'
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should require POST for json requests" do
|
77
|
+
rodauth do
|
78
|
+
enable :login, :logout, :jwt_refresh
|
79
|
+
jwt_secret '1'
|
80
|
+
json_response_success_key 'success'
|
81
|
+
end
|
82
|
+
roda(:jwt) do |r|
|
83
|
+
r.rodauth
|
84
|
+
end
|
85
|
+
|
86
|
+
res = json_request("/login", :method=>'GET')
|
87
|
+
res.must_equal [405, {'error'=>'non-POST method used in JSON API'}]
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should require Accept contain application/json if jwt_check_accept? is true and Accept is present" do
|
91
|
+
rodauth do
|
92
|
+
enable :login, :logout, :jwt_refresh
|
93
|
+
jwt_secret '1'
|
94
|
+
json_response_success_key 'success'
|
95
|
+
jwt_check_accept? true
|
96
|
+
end
|
97
|
+
roda(:jwt) do |r|
|
98
|
+
r.rodauth
|
99
|
+
end
|
100
|
+
|
101
|
+
res = json_request("/login", :headers=>{'HTTP_ACCEPT'=>'text/html'})
|
102
|
+
res.must_equal [406, {'error'=>'Unsupported Accept header. Must accept "application/json" or compatible content type'}]
|
103
|
+
|
104
|
+
jwt_refresh_validate_login(json_request("/login", :login=>'foo@example.com', :password=>'0123456789'))
|
105
|
+
jwt_refresh_validate_login(json_request("/login", :headers=>{'HTTP_ACCEPT'=>'*/*'}, :login=>'foo@example.com', :password=>'0123456789'))
|
106
|
+
jwt_refresh_validate_login(json_request("/login", :headers=>{'HTTP_ACCEPT'=>'application/*'}, :login=>'foo@example.com', :password=>'0123456789'))
|
107
|
+
jwt_refresh_validate_login(json_request("/login", :headers=>{'HTTP_ACCEPT'=>'application/vnd.api+json'}, :login=>'foo@example.com', :password=>'0123456789'))
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should clear jwt refresh token when closing account" do
|
111
|
+
rodauth do
|
112
|
+
enable :login, :jwt_refresh, :close_account
|
113
|
+
jwt_secret '1'
|
114
|
+
end
|
115
|
+
roda(:jwt) do |r|
|
116
|
+
r.rodauth
|
117
|
+
rodauth.require_authentication
|
118
|
+
response['Content-Type'] = 'application/json'
|
119
|
+
{'hello' => 'world'}.to_json
|
120
|
+
end
|
121
|
+
|
122
|
+
jwt_refresh_login
|
123
|
+
|
124
|
+
DB[:account_jwt_refresh_keys].count.must_equal 1
|
125
|
+
res = json_request('/close-account', :password=>'0123456789')
|
126
|
+
res[1].delete('access_token').must_be_kind_of(String)
|
127
|
+
res.must_equal [200, {'success'=>"Your account has been closed"}]
|
128
|
+
DB[:account_jwt_refresh_keys].count.must_equal 0
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
it "should set refresh tokens when creating accounts when using autologin" do
|
133
|
+
rodauth do
|
134
|
+
enable :login, :create_account, :jwt_refresh
|
135
|
+
after_create_account{json_response[:account_id] = account_id}
|
136
|
+
create_account_autologin? true
|
137
|
+
end
|
138
|
+
roda(:jwt) do |r|
|
139
|
+
r.rodauth
|
140
|
+
rodauth.require_authentication
|
141
|
+
response['Content-Type'] = 'application/json'
|
142
|
+
{'hello' => 'world'}.to_json
|
143
|
+
end
|
144
|
+
|
145
|
+
res = json_request('/create-account', :login=>'foo@example2.com', "login-confirm"=>'foo@example2.com', :password=>'0123456789', "password-confirm"=>'0123456789')
|
146
|
+
refresh_token = res.last.delete('refresh_token')
|
147
|
+
@authorization = res.last.delete('access_token')
|
148
|
+
res.must_equal [200, {'success'=>"Your account has been created", 'account_id'=>DB[:accounts].max(:id)}]
|
149
|
+
|
150
|
+
res = json_request("/")
|
151
|
+
res.must_equal [200, {'hello'=>'world'}]
|
152
|
+
|
153
|
+
# We can refresh our token
|
154
|
+
res = json_request("/jwt-refresh", :refresh_token=>refresh_token)
|
155
|
+
jwt_refresh_validate(res)
|
156
|
+
@authorization = res.last.delete('access_token')
|
157
|
+
|
158
|
+
# Which we can use to access protected resources
|
159
|
+
res = json_request("/")
|
160
|
+
res.must_equal [200, {'hello'=>'world'}]
|
161
|
+
end
|
162
|
+
|
163
|
+
[false, true].each do |hs|
|
164
|
+
it "generates and refreshes Refresh Tokens #{'with hmac_secret' if hs}" do
|
165
|
+
initial_secret = secret = SecureRandom.random_bytes(32) if hs
|
166
|
+
rodauth do
|
167
|
+
enable :login, :logout, :jwt_refresh
|
168
|
+
hmac_secret{secret} if hs
|
169
|
+
jwt_secret '1'
|
170
|
+
end
|
171
|
+
roda(:jwt) do |r|
|
172
|
+
r.rodauth
|
173
|
+
rodauth.require_authentication
|
174
|
+
response['Content-Type'] = 'application/json'
|
175
|
+
{'hello' => 'world'}.to_json
|
176
|
+
end
|
177
|
+
res = json_request("/")
|
178
|
+
res.must_equal [401, {'error'=>'Please login to continue'}]
|
179
|
+
|
180
|
+
# We can login
|
181
|
+
res = jwt_refresh_login
|
182
|
+
refresh_token = res.last['refresh_token']
|
183
|
+
|
184
|
+
# Which gives us an access token which grants us access to protected resources
|
185
|
+
@authorization = res.last['access_token']
|
186
|
+
res = json_request("/")
|
187
|
+
res.must_equal [200, {'hello'=>'world'}]
|
188
|
+
|
189
|
+
# We can refresh our token
|
190
|
+
res = json_request("/jwt-refresh", :refresh_token=>refresh_token)
|
191
|
+
jwt_refresh_validate(res)
|
192
|
+
second_refresh_token = res.last['refresh_token']
|
193
|
+
|
194
|
+
# Which we can use to access protected resources
|
195
|
+
@authorization = res.last['access_token']
|
196
|
+
res = json_request("/")
|
197
|
+
res.must_equal [200, {'hello'=>'world'}]
|
198
|
+
|
199
|
+
# Subsequent refresh token is valid
|
200
|
+
res = json_request("/jwt-refresh", :refresh_token=>second_refresh_token)
|
201
|
+
jwt_refresh_validate(res)
|
202
|
+
third_refresh_token = res.last['refresh_token']
|
203
|
+
|
204
|
+
# First refresh token is now no longer valid
|
205
|
+
res = json_request("/jwt-refresh", :refresh_token=>refresh_token)
|
206
|
+
res.must_equal [400, {"error"=>"invalid JWT refresh token"}]
|
207
|
+
|
208
|
+
# Third refresh token is valid
|
209
|
+
res = json_request("/jwt-refresh", :refresh_token=>third_refresh_token)
|
210
|
+
jwt_refresh_validate(res)
|
211
|
+
fourth_refresh_token = res.last['refresh_token']
|
212
|
+
|
213
|
+
# And still gives us a valid access token
|
214
|
+
@authorization = res.last['access_token']
|
215
|
+
res = json_request("/")
|
216
|
+
res.must_equal [200, {'hello'=>'world'}]
|
217
|
+
|
218
|
+
if hs
|
219
|
+
# Refresh secret doesn't work if hmac_secret changed
|
220
|
+
secret = SecureRandom.random_bytes(32)
|
221
|
+
res = json_request("/jwt-refresh", :refresh_token=>fourth_refresh_token)
|
222
|
+
res.first.must_equal 400
|
223
|
+
res.must_equal [400, {'error'=>'invalid JWT refresh token'}]
|
224
|
+
|
225
|
+
# Refresh secret works if hmac_secret changed back
|
226
|
+
secret = initial_secret
|
227
|
+
res = json_request("/jwt-refresh", :refresh_token=>fourth_refresh_token)
|
228
|
+
jwt_refresh_validate(res)
|
229
|
+
|
230
|
+
# And still gives us a valid access token
|
231
|
+
@authorization = res.last['access_token']
|
232
|
+
res = json_request("/")
|
233
|
+
res.must_equal [200, {'hello'=>'world'}]
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
it "should not return access_token for failed login attempt" do
|
239
|
+
rodauth do
|
240
|
+
enable :login, :create_account, :jwt_refresh
|
241
|
+
after_create_account{json_response[:account_id] = account_id}
|
242
|
+
create_account_autologin? true
|
243
|
+
end
|
244
|
+
roda(:jwt) do |r|
|
245
|
+
r.rodauth
|
246
|
+
rodauth.require_authentication
|
247
|
+
response['Content-Type'] = 'application/json'
|
248
|
+
{'hello' => 'world'}.to_json
|
249
|
+
end
|
250
|
+
|
251
|
+
json_request('/create-account', :login=>'foo@example2.com', "login-confirm"=>'foo@example2.com', :password=>'0123456789', "password-confirm"=>'0123456789')
|
252
|
+
|
253
|
+
res = json_request('/login', :login=>'foo@example2.com', :password=>'123123')
|
254
|
+
res.must_equal [401, {"field-error"=>['password', 'invalid password'], "error"=>"There was an error logging in"}]
|
255
|
+
end
|
256
|
+
end
|