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
@@ -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
|