rodauth 1.23.0 → 2.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +184 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +221 -79
- data/doc/account_expiration.rdoc +12 -26
- data/doc/active_sessions.rdoc +49 -0
- data/doc/audit_logging.rdoc +44 -0
- data/doc/base.rdoc +76 -128
- data/doc/change_login.rdoc +7 -14
- data/doc/change_password.rdoc +9 -13
- data/doc/change_password_notify.rdoc +2 -2
- data/doc/close_account.rdoc +9 -16
- data/doc/confirm_password.rdoc +12 -5
- data/doc/create_account.rdoc +11 -22
- data/doc/disallow_password_reuse.rdoc +6 -13
- data/doc/email_auth.rdoc +15 -14
- data/doc/email_base.rdoc +5 -15
- data/doc/guides/admin_activation.rdoc +46 -0
- data/doc/guides/already_authenticated.rdoc +10 -0
- data/doc/guides/alternative_login.rdoc +46 -0
- data/doc/guides/create_account_programmatically.rdoc +38 -0
- data/doc/guides/delay_password.rdoc +25 -0
- data/doc/guides/email_only.rdoc +16 -0
- data/doc/guides/i18n.rdoc +26 -0
- data/doc/{internals.rdoc → guides/internals.rdoc} +0 -0
- data/doc/guides/links.rdoc +12 -0
- data/doc/guides/login_return.rdoc +37 -0
- data/doc/guides/password_column.rdoc +25 -0
- data/doc/guides/password_confirmation.rdoc +37 -0
- data/doc/guides/password_requirements.rdoc +30 -0
- data/doc/guides/paths.rdoc +36 -0
- data/doc/guides/query_params.rdoc +9 -0
- data/doc/guides/redirects.rdoc +17 -0
- data/doc/guides/registration_field.rdoc +68 -0
- data/doc/guides/require_mfa.rdoc +30 -0
- data/doc/guides/reset_password_autologin.rdoc +21 -0
- data/doc/guides/status_column.rdoc +28 -0
- data/doc/guides/totp_or_recovery.rdoc +16 -0
- data/doc/http_basic_auth.rdoc +10 -1
- data/doc/jwt.rdoc +22 -22
- data/doc/jwt_cors.rdoc +2 -3
- data/doc/jwt_refresh.rdoc +23 -8
- data/doc/lockout.rdoc +17 -15
- data/doc/login.rdoc +17 -2
- data/doc/login_password_requirements_base.rdoc +18 -37
- data/doc/logout.rdoc +2 -2
- data/doc/otp.rdoc +25 -19
- data/doc/password_complexity.rdoc +10 -26
- data/doc/password_expiration.rdoc +11 -25
- data/doc/password_grace_period.rdoc +16 -2
- data/doc/password_pepper.rdoc +44 -0
- data/doc/recovery_codes.rdoc +18 -12
- data/doc/release_notes/2.0.0.txt +361 -0
- data/doc/release_notes/2.1.0.txt +31 -0
- data/doc/release_notes/2.2.0.txt +39 -0
- data/doc/release_notes/2.3.0.txt +37 -0
- data/doc/release_notes/2.4.0.txt +22 -0
- data/doc/remember.rdoc +40 -64
- data/doc/reset_password.rdoc +12 -9
- data/doc/session_expiration.rdoc +1 -0
- data/doc/single_session.rdoc +16 -25
- data/doc/sms_codes.rdoc +24 -14
- data/doc/two_factor_base.rdoc +60 -22
- data/doc/verify_account.rdoc +14 -12
- data/doc/verify_account_grace_period.rdoc +6 -2
- data/doc/verify_login_change.rdoc +9 -8
- data/doc/webauthn.rdoc +115 -0
- data/doc/webauthn_login.rdoc +15 -0
- data/doc/webauthn_verify_account.rdoc +9 -0
- data/javascript/webauthn_auth.js +45 -0
- data/javascript/webauthn_setup.js +35 -0
- data/lib/roda/plugins/rodauth.rb +1 -1
- data/lib/rodauth.rb +33 -28
- data/lib/rodauth/features/account_expiration.rb +5 -5
- data/lib/rodauth/features/active_sessions.rb +158 -0
- data/lib/rodauth/features/audit_logging.rb +98 -0
- data/lib/rodauth/features/base.rb +152 -49
- data/lib/rodauth/features/change_password_notify.rb +1 -1
- data/lib/rodauth/features/close_account.rb +8 -6
- data/lib/rodauth/features/confirm_password.rb +40 -2
- data/lib/rodauth/features/create_account.rb +8 -13
- data/lib/rodauth/features/disallow_common_passwords.rb +1 -1
- data/lib/rodauth/features/disallow_password_reuse.rb +5 -3
- data/lib/rodauth/features/email_auth.rb +30 -28
- data/lib/rodauth/features/email_base.rb +3 -3
- data/lib/rodauth/features/http_basic_auth.rb +55 -35
- data/lib/rodauth/features/jwt.rb +63 -16
- data/lib/rodauth/features/jwt_cors.rb +15 -15
- data/lib/rodauth/features/jwt_refresh.rb +42 -13
- data/lib/rodauth/features/lockout.rb +11 -13
- data/lib/rodauth/features/login.rb +58 -13
- data/lib/rodauth/features/login_password_requirements_base.rb +13 -8
- data/lib/rodauth/features/otp.rb +76 -82
- data/lib/rodauth/features/password_complexity.rb +8 -13
- data/lib/rodauth/features/password_expiration.rb +1 -1
- data/lib/rodauth/features/password_grace_period.rb +17 -10
- data/lib/rodauth/features/password_pepper.rb +45 -0
- data/lib/rodauth/features/recovery_codes.rb +47 -51
- data/lib/rodauth/features/remember.rb +13 -27
- data/lib/rodauth/features/reset_password.rb +25 -25
- data/lib/rodauth/features/session_expiration.rb +7 -10
- data/lib/rodauth/features/single_session.rb +8 -6
- data/lib/rodauth/features/sms_codes.rb +58 -68
- data/lib/rodauth/features/two_factor_base.rb +134 -30
- data/lib/rodauth/features/verify_account.rb +28 -20
- data/lib/rodauth/features/verify_account_grace_period.rb +18 -9
- data/lib/rodauth/features/verify_login_change.rb +11 -10
- data/lib/rodauth/features/webauthn.rb +505 -0
- data/lib/rodauth/features/webauthn_login.rb +70 -0
- data/lib/rodauth/features/webauthn_verify_account.rb +46 -0
- data/lib/rodauth/migrations.rb +16 -5
- data/lib/rodauth/version.rb +2 -2
- data/templates/button.str +1 -3
- data/templates/change-login.str +1 -2
- data/templates/change-password.str +3 -5
- data/templates/close-account.str +2 -2
- data/templates/confirm-password.str +1 -1
- data/templates/create-account.str +1 -1
- data/templates/email-auth-request-form.str +1 -2
- data/templates/email-auth.str +1 -1
- data/templates/global-logout-field.str +6 -0
- data/templates/login-confirm-field.str +2 -4
- data/templates/login-display.str +3 -2
- data/templates/login-field.str +2 -4
- data/templates/login-form-footer.str +6 -0
- data/templates/login-form.str +7 -0
- data/templates/login.str +1 -9
- data/templates/logout.str +1 -1
- data/templates/multi-phase-login.str +3 -0
- data/templates/otp-auth-code-field.str +5 -3
- data/templates/otp-auth.str +1 -1
- data/templates/otp-disable.str +1 -1
- data/templates/otp-setup.str +3 -3
- data/templates/password-confirm-field.str +2 -4
- data/templates/password-field.str +2 -4
- data/templates/recovery-auth.str +3 -6
- data/templates/recovery-codes.str +1 -1
- data/templates/remember.str +15 -20
- data/templates/reset-password-request.str +2 -2
- data/templates/reset-password.str +1 -2
- data/templates/sms-auth.str +1 -1
- data/templates/sms-code-field.str +5 -3
- data/templates/sms-confirm.str +1 -2
- data/templates/sms-disable.str +1 -2
- data/templates/sms-request.str +1 -1
- data/templates/sms-setup.str +6 -4
- data/templates/two-factor-auth.str +5 -0
- data/templates/two-factor-disable.str +6 -0
- data/templates/two-factor-manage.str +16 -0
- data/templates/unlock-account-request.str +2 -2
- data/templates/unlock-account.str +1 -1
- data/templates/verify-account-resend.str +1 -1
- data/templates/verify-account.str +1 -2
- data/templates/verify-login-change.str +1 -1
- data/templates/webauthn-auth.str +11 -0
- data/templates/webauthn-remove.str +14 -0
- data/templates/webauthn-setup.str +12 -0
- metadata +96 -13
- data/doc/verify_change_login.rdoc +0 -11
- data/lib/rodauth/features/verify_change_login.rb +0 -20
data/lib/rodauth/features/jwt.rb
CHANGED
@@ -4,25 +4,25 @@ require 'jwt'
|
|
4
4
|
|
5
5
|
module Rodauth
|
6
6
|
Feature.define(:jwt, :Jwt) do
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
translatable_method :invalid_jwt_format_error_message, "invalid JWT format or claim in Authorization header"
|
8
|
+
translatable_method :json_non_post_error_message, 'non-POST method used in JSON API'
|
9
|
+
translatable_method :json_not_accepted_error_message, 'Unsupported Accept header. Must accept "application/json" or compatible content type'
|
10
10
|
auth_value_method :json_accept_regexp, /(?:(?:\*|\bapplication)\/\*|\bapplication\/(?:vnd\.api\+)?json\b)/i
|
11
11
|
auth_value_method :json_request_content_type_regexp, /\bapplication\/(?:vnd\.api\+)?json\b/i
|
12
12
|
auth_value_method :json_response_content_type, 'application/json'
|
13
13
|
auth_value_method :json_response_error_status, 400
|
14
|
-
auth_value_method :json_response_custom_error_status?,
|
14
|
+
auth_value_method :json_response_custom_error_status?, true
|
15
15
|
auth_value_method :json_response_error_key, "error"
|
16
16
|
auth_value_method :json_response_field_error_key, "field-error"
|
17
|
-
auth_value_method :json_response_success_key,
|
17
|
+
auth_value_method :json_response_success_key, "success"
|
18
18
|
auth_value_method :jwt_algorithm, "HS256"
|
19
19
|
auth_value_method :jwt_authorization_ignore, /\A(?:Basic|Digest) /
|
20
20
|
auth_value_method :jwt_authorization_remove, /\ABearer:?\s+/
|
21
|
-
auth_value_method :jwt_check_accept?,
|
22
|
-
auth_value_method :jwt_decode_opts, {}
|
21
|
+
auth_value_method :jwt_check_accept?, true
|
22
|
+
auth_value_method :jwt_decode_opts, {}.freeze
|
23
23
|
auth_value_method :jwt_session_key, nil
|
24
24
|
auth_value_method :jwt_symbolize_deeply?, false
|
25
|
-
|
25
|
+
translatable_method :non_json_request_error_message, 'Only JSON format requests are allowed'
|
26
26
|
|
27
27
|
auth_value_methods(
|
28
28
|
:only_json?,
|
@@ -50,7 +50,7 @@ module Rodauth
|
|
50
50
|
json_response[json_response_error_key] = invalid_jwt_format_error_message
|
51
51
|
response.status ||= json_response_error_status
|
52
52
|
response['Content-Type'] ||= json_response_content_type
|
53
|
-
response.write(
|
53
|
+
response.write(_json_response_body(json_response))
|
54
54
|
request.halt
|
55
55
|
end
|
56
56
|
|
@@ -138,15 +138,25 @@ module Rodauth
|
|
138
138
|
!!(jwt_token && jwt_payload)
|
139
139
|
end
|
140
140
|
|
141
|
+
def view(page, title)
|
142
|
+
return super unless use_jwt?
|
143
|
+
return_json_response
|
144
|
+
end
|
145
|
+
|
141
146
|
private
|
142
147
|
|
148
|
+
def check_csrf?
|
149
|
+
return false if use_jwt?
|
150
|
+
super
|
151
|
+
end
|
152
|
+
|
143
153
|
def before_rodauth
|
144
154
|
if json_request?
|
145
155
|
if jwt_check_accept? && (accept = request.env['HTTP_ACCEPT']) && accept !~ json_accept_regexp
|
146
156
|
response.status = 406
|
147
157
|
json_response[json_response_error_key] = json_not_accepted_error_message
|
148
158
|
response['Content-Type'] ||= json_response_content_type
|
149
|
-
response.write(
|
159
|
+
response.write(_json_response_body(json_response))
|
150
160
|
request.halt
|
151
161
|
end
|
152
162
|
|
@@ -173,6 +183,43 @@ module Rodauth
|
|
173
183
|
end
|
174
184
|
end
|
175
185
|
|
186
|
+
def before_webauthn_setup_route
|
187
|
+
super if defined?(super)
|
188
|
+
if use_jwt? && !param_or_nil(webauthn_setup_param)
|
189
|
+
cred = new_webauthn_credential
|
190
|
+
json_response[webauthn_setup_param] = cred.as_json
|
191
|
+
json_response[webauthn_setup_challenge_param] = cred.challenge
|
192
|
+
json_response[webauthn_setup_challenge_hmac_param] = compute_hmac(cred.challenge)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def before_webauthn_auth_route
|
197
|
+
super if defined?(super)
|
198
|
+
if use_jwt? && !param_or_nil(webauthn_auth_param)
|
199
|
+
cred = webauth_credential_options_for_get
|
200
|
+
json_response[webauthn_auth_param] = cred.as_json
|
201
|
+
json_response[webauthn_auth_challenge_param] = cred.challenge
|
202
|
+
json_response[webauthn_auth_challenge_hmac_param] = compute_hmac(cred.challenge)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def before_webauthn_login_route
|
207
|
+
super if defined?(super)
|
208
|
+
if use_jwt? && !param_or_nil(webauthn_auth_param) && account_from_login(param(login_param))
|
209
|
+
cred = webauth_credential_options_for_get
|
210
|
+
json_response[webauthn_auth_param] = cred.as_json
|
211
|
+
json_response[webauthn_auth_challenge_param] = cred.challenge
|
212
|
+
json_response[webauthn_auth_challenge_hmac_param] = compute_hmac(cred.challenge)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def before_webauthn_remove_route
|
217
|
+
super if defined?(super)
|
218
|
+
if use_jwt? && !param_or_nil(webauthn_remove_param)
|
219
|
+
json_response[webauthn_remove_param] = account_webauthn_usage
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
176
223
|
def before_otp_setup_route
|
177
224
|
super if defined?(super)
|
178
225
|
if use_jwt? && otp_keys_use_hmac? && !param_or_nil(otp_setup_raw_param)
|
@@ -204,14 +251,14 @@ module Rodauth
|
|
204
251
|
value
|
205
252
|
end
|
206
253
|
|
207
|
-
def
|
208
|
-
|
254
|
+
def remove_session_value(key)
|
255
|
+
value = super
|
256
|
+
set_jwt if use_jwt?
|
257
|
+
value
|
209
258
|
end
|
210
259
|
|
211
|
-
def
|
212
|
-
|
213
|
-
return super if meth == :render
|
214
|
-
return_json_response
|
260
|
+
def json_response
|
261
|
+
@json_response ||= {}
|
215
262
|
end
|
216
263
|
|
217
264
|
def _json_response_body(hash)
|
@@ -13,27 +13,27 @@ module Rodauth
|
|
13
13
|
auth_methods(:jwt_cors_allow?)
|
14
14
|
|
15
15
|
def jwt_cors_allow?
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
16
|
+
return false unless origin = request.env['HTTP_ORIGIN']
|
17
|
+
|
18
|
+
case allowed = jwt_cors_allow_origin
|
19
|
+
when String
|
20
|
+
timing_safe_eql?(origin, allowed)
|
21
|
+
when Array
|
22
|
+
allowed.any?{|s| timing_safe_eql?(origin, s)}
|
23
|
+
when Regexp
|
24
|
+
allowed =~ origin
|
25
|
+
when true
|
26
|
+
true
|
27
|
+
else
|
28
|
+
false
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
32
|
private
|
33
33
|
|
34
34
|
def before_rodauth
|
35
|
-
if
|
36
|
-
response['Access-Control-Allow-Origin'] =
|
35
|
+
if jwt_cors_allow?
|
36
|
+
response['Access-Control-Allow-Origin'] = request.env['HTTP_ORIGIN']
|
37
37
|
|
38
38
|
# Handle CORS preflight request
|
39
39
|
if request.request_method == 'OPTIONS'
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen-string-literal: true
|
2
2
|
|
3
3
|
module Rodauth
|
4
|
-
|
4
|
+
Feature.define(:jwt_refresh, :JwtRefresh) do
|
5
5
|
depends :jwt
|
6
6
|
|
7
7
|
after 'refresh_token'
|
@@ -10,32 +10,38 @@ module Rodauth
|
|
10
10
|
auth_value_method :jwt_access_token_key, 'access_token'
|
11
11
|
auth_value_method :jwt_access_token_not_before_period, 5
|
12
12
|
auth_value_method :jwt_access_token_period, 1800
|
13
|
-
|
13
|
+
translatable_method :jwt_refresh_invalid_token_message, 'invalid JWT refresh token'
|
14
14
|
auth_value_method :jwt_refresh_token_account_id_column, :account_id
|
15
15
|
auth_value_method :jwt_refresh_token_deadline_column, :deadline
|
16
|
-
auth_value_method :jwt_refresh_token_deadline_interval, {:days=>14}
|
16
|
+
auth_value_method :jwt_refresh_token_deadline_interval, {:days=>14}.freeze
|
17
17
|
auth_value_method :jwt_refresh_token_id_column, :id
|
18
18
|
auth_value_method :jwt_refresh_token_key, 'refresh_token'
|
19
19
|
auth_value_method :jwt_refresh_token_key_column, :key
|
20
20
|
auth_value_method :jwt_refresh_token_key_param, 'refresh_token'
|
21
21
|
auth_value_method :jwt_refresh_token_table, :account_jwt_refresh_keys
|
22
|
+
translatable_method :jwt_refresh_without_access_token_message, 'no JWT access token provided during refresh'
|
23
|
+
auth_value_method :jwt_refresh_without_access_token_status, 401
|
22
24
|
|
23
25
|
auth_private_methods(
|
24
26
|
:account_from_refresh_token
|
25
27
|
)
|
26
28
|
|
27
29
|
route do |r|
|
30
|
+
before_jwt_refresh_route
|
31
|
+
|
28
32
|
r.post do
|
29
|
-
if
|
30
|
-
|
33
|
+
if !session_value
|
34
|
+
response.status ||= jwt_refresh_without_access_token_status
|
35
|
+
json_response[json_response_error_key] = jwt_refresh_without_access_token_message
|
36
|
+
elsif (refresh_token = param_or_nil(jwt_refresh_token_key_param)) && account_from_refresh_token(refresh_token)
|
31
37
|
transaction do
|
32
38
|
before_refresh_token
|
33
39
|
formatted_token = generate_refresh_token
|
34
40
|
remove_jwt_refresh_token_key(refresh_token)
|
41
|
+
json_response[jwt_refresh_token_key] = formatted_token
|
42
|
+
json_response[jwt_access_token_key] = session_jwt
|
35
43
|
after_refresh_token
|
36
44
|
end
|
37
|
-
json_response[jwt_refresh_token_key] = formatted_token
|
38
|
-
json_response[jwt_access_token_key] = session_jwt
|
39
45
|
else
|
40
46
|
json_response[json_response_error_key] = jwt_refresh_invalid_token_message
|
41
47
|
response.status ||= json_response_error_status
|
@@ -80,14 +86,10 @@ module Rodauth
|
|
80
86
|
private
|
81
87
|
|
82
88
|
def _account_from_refresh_token(token)
|
83
|
-
id,
|
84
|
-
return unless id && token
|
85
|
-
|
86
|
-
token_id, key = split_token(token)
|
87
|
-
return unless token_id && key
|
89
|
+
id, token_id, key = _account_refresh_token_split(token)
|
88
90
|
|
91
|
+
return unless key
|
89
92
|
return unless actual = get_active_refresh_token(id, token_id)
|
90
|
-
|
91
93
|
return unless timing_safe_eql?(key, convert_token_key(actual))
|
92
94
|
|
93
95
|
ds = account_ds(id)
|
@@ -95,6 +97,16 @@ module Rodauth
|
|
95
97
|
ds.first
|
96
98
|
end
|
97
99
|
|
100
|
+
def _account_refresh_token_split(token)
|
101
|
+
id, token = split_token(token)
|
102
|
+
return unless id && token
|
103
|
+
|
104
|
+
token_id, key = split_token(token)
|
105
|
+
return unless token_id && key
|
106
|
+
|
107
|
+
[id, token_id, key]
|
108
|
+
end
|
109
|
+
|
98
110
|
def get_active_refresh_token(account_id, token_id)
|
99
111
|
jwt_refresh_token_account_ds(account_id).
|
100
112
|
where(Sequel::CURRENT_TIMESTAMP > jwt_refresh_token_deadline_column).
|
@@ -134,6 +146,23 @@ module Rodauth
|
|
134
146
|
hash
|
135
147
|
end
|
136
148
|
|
149
|
+
def before_logout
|
150
|
+
if token = param_or_nil(jwt_refresh_token_key_param)
|
151
|
+
if token == 'all'
|
152
|
+
jwt_refresh_token_account_ds(session_value).delete
|
153
|
+
else
|
154
|
+
id, token_id, key = _account_refresh_token_split(token)
|
155
|
+
|
156
|
+
if id && token_id && key && (actual = get_active_refresh_token(session_value, token_id)) && timing_safe_eql?(key, convert_token_key(actual))
|
157
|
+
jwt_refresh_token_account_ds(id).
|
158
|
+
where(jwt_refresh_token_id_column=>token_id).
|
159
|
+
delete
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
super if defined?(super)
|
164
|
+
end
|
165
|
+
|
137
166
|
def after_close_account
|
138
167
|
jwt_refresh_token_account_ds(account_id).delete
|
139
168
|
super if defined?(super)
|
@@ -4,8 +4,6 @@ module Rodauth
|
|
4
4
|
Feature.define(:lockout, :Lockout) do
|
5
5
|
depends :login, :email_base
|
6
6
|
|
7
|
-
def_deprecated_alias :no_matching_unlock_account_key_error_flash, :no_matching_unlock_account_key_message
|
8
|
-
|
9
7
|
loaded_templates %w'unlock-account-request unlock-account password-field unlock-account-email'
|
10
8
|
view 'unlock-account-request', 'Request Account Unlock', 'unlock_account_request'
|
11
9
|
view 'unlock-account', 'Unlock Account', 'unlock_account'
|
@@ -19,7 +17,7 @@ module Rodauth
|
|
19
17
|
button 'Unlock Account', 'unlock_account'
|
20
18
|
button 'Request Account Unlock', 'unlock_account_request'
|
21
19
|
error_flash "There was an error unlocking your account", 'unlock_account'
|
22
|
-
error_flash "This account is currently locked out and cannot be logged in to
|
20
|
+
error_flash "This account is currently locked out and cannot be logged in to", "login_lockout"
|
23
21
|
error_flash "An email has recently been sent to you with a link to unlock the account", 'unlock_account_email_recently_sent'
|
24
22
|
error_flash "There was an error unlocking your account: invalid or expired unlock account key", 'no_matching_unlock_account_key'
|
25
23
|
notice_flash "Your account has been unlocked", 'unlock_account'
|
@@ -36,12 +34,12 @@ module Rodauth
|
|
36
34
|
auth_value_method :account_lockouts_table, :account_lockouts
|
37
35
|
auth_value_method :account_lockouts_id_column, :id
|
38
36
|
auth_value_method :account_lockouts_key_column, :key
|
39
|
-
auth_value_method :account_lockouts_email_last_sent_column,
|
37
|
+
auth_value_method :account_lockouts_email_last_sent_column, :email_last_sent
|
40
38
|
auth_value_method :account_lockouts_deadline_column, :deadline
|
41
|
-
auth_value_method :account_lockouts_deadline_interval, {:days=>1}
|
42
|
-
|
43
|
-
|
44
|
-
|
39
|
+
auth_value_method :account_lockouts_deadline_interval, {:days=>1}.freeze
|
40
|
+
translatable_method :unlock_account_email_subject, 'Unlock Account'
|
41
|
+
translatable_method :unlock_account_explanatory_text, '<p>This account is currently locked out. You can unlock the account:</p>'
|
42
|
+
translatable_method :unlock_account_request_explanatory_text, '<p>This account is currently locked out. You can request that the account be unlocked:</p>'
|
45
43
|
auth_value_method :unlock_account_key_param, 'key'
|
46
44
|
auth_value_method :unlock_account_requires_password?, false
|
47
45
|
auth_value_method :unlock_account_skip_resend_email_within, 300
|
@@ -75,6 +73,7 @@ module Rodauth
|
|
75
73
|
redirect unlock_account_email_recently_sent_redirect
|
76
74
|
end
|
77
75
|
|
76
|
+
@unlock_account_key_value = get_unlock_account_key
|
78
77
|
transaction do
|
79
78
|
before_unlock_account_request
|
80
79
|
set_unlock_account_email_last_sent
|
@@ -98,7 +97,7 @@ module Rodauth
|
|
98
97
|
|
99
98
|
r.get do
|
100
99
|
if key = param_or_nil(unlock_account_key_param)
|
101
|
-
|
100
|
+
set_session_value(unlock_account_session_key, key)
|
102
101
|
redirect(r.path)
|
103
102
|
end
|
104
103
|
|
@@ -106,7 +105,7 @@ module Rodauth
|
|
106
105
|
if account_from_unlock_key(key)
|
107
106
|
unlock_account_view
|
108
107
|
else
|
109
|
-
|
108
|
+
remove_session_value(unlock_account_session_key)
|
110
109
|
set_redirect_error_flash no_matching_unlock_account_key_error_flash
|
111
110
|
redirect require_login_redirect
|
112
111
|
end
|
@@ -127,11 +126,11 @@ module Rodauth
|
|
127
126
|
unlock_account
|
128
127
|
after_unlock_account
|
129
128
|
if unlock_account_autologin?
|
130
|
-
|
129
|
+
autologin_session('unlock_account')
|
131
130
|
end
|
132
131
|
end
|
133
132
|
|
134
|
-
|
133
|
+
remove_session_value(unlock_account_session_key)
|
135
134
|
set_notice_flash unlock_account_notice_flash
|
136
135
|
redirect unlock_account_redirect
|
137
136
|
else
|
@@ -217,7 +216,6 @@ module Rodauth
|
|
217
216
|
end
|
218
217
|
|
219
218
|
def send_unlock_account_email
|
220
|
-
@unlock_account_key_value = get_unlock_account_key
|
221
219
|
send_email(create_unlock_account_email)
|
222
220
|
end
|
223
221
|
|
@@ -5,16 +5,24 @@ module Rodauth
|
|
5
5
|
notice_flash "You have been logged in"
|
6
6
|
notice_flash "Login recognized, please enter your password", "need_password"
|
7
7
|
error_flash "There was an error logging in"
|
8
|
-
loaded_templates %w'login login-field password-field login-display'
|
8
|
+
loaded_templates %w'login login-form login-form-footer multi-phase-login login-field password-field login-display'
|
9
9
|
view 'login', 'Login'
|
10
|
+
view 'multi-phase-login', 'Login', 'multi_phase_login'
|
10
11
|
additional_form_tags
|
11
12
|
button 'Login'
|
12
13
|
redirect
|
13
14
|
|
14
15
|
auth_value_method :login_error_status, 401
|
15
|
-
|
16
|
+
translatable_method :login_form_footer_links_heading, '<h2 class="rodauth-login-form-footer-links-heading">Other Options</h2>'
|
17
|
+
auth_value_method :login_return_to_requested_location?, false
|
16
18
|
auth_value_method :use_multi_phase_login?, false
|
17
19
|
|
20
|
+
session_key :login_redirect_session_key, :login_redirect
|
21
|
+
|
22
|
+
auth_cached_method :multi_phase_login_forms
|
23
|
+
auth_cached_method :login_form_footer_links
|
24
|
+
auth_cached_method :login_form_footer
|
25
|
+
|
18
26
|
route do |r|
|
19
27
|
check_already_logged_in
|
20
28
|
before_login_route
|
@@ -24,8 +32,8 @@ module Rodauth
|
|
24
32
|
end
|
25
33
|
|
26
34
|
r.post do
|
27
|
-
clear_session
|
28
35
|
skip_error_flash = false
|
36
|
+
view = :login_view
|
29
37
|
|
30
38
|
catch_error do
|
31
39
|
unless account_from_login(param(login_param))
|
@@ -40,6 +48,7 @@ module Rodauth
|
|
40
48
|
|
41
49
|
if use_multi_phase_login?
|
42
50
|
@valid_login_entered = true
|
51
|
+
view = :multi_phase_login_view
|
43
52
|
|
44
53
|
unless param_or_nil(password_param)
|
45
54
|
after_login_entered_during_multi_phase_login
|
@@ -53,18 +62,40 @@ module Rodauth
|
|
53
62
|
throw_error_status(login_error_status, password_param, invalid_password_message)
|
54
63
|
end
|
55
64
|
|
56
|
-
|
65
|
+
login('password')
|
57
66
|
end
|
58
67
|
|
59
68
|
set_error_flash login_error_flash unless skip_error_flash
|
60
|
-
|
69
|
+
send(view)
|
61
70
|
end
|
62
71
|
end
|
63
72
|
|
64
73
|
attr_reader :login_form_header
|
65
74
|
|
75
|
+
def login(auth_type)
|
76
|
+
saved_login_redirect = remove_session_value(login_redirect_session_key)
|
77
|
+
transaction do
|
78
|
+
before_login
|
79
|
+
login_session(auth_type)
|
80
|
+
yield if block_given?
|
81
|
+
after_login
|
82
|
+
end
|
83
|
+
set_notice_flash login_notice_flash
|
84
|
+
redirect(saved_login_redirect || login_redirect)
|
85
|
+
end
|
86
|
+
|
87
|
+
def login_required
|
88
|
+
if login_return_to_requested_location?
|
89
|
+
set_session_value(login_redirect_session_key, request.fullpath)
|
90
|
+
end
|
91
|
+
super
|
92
|
+
end
|
93
|
+
|
66
94
|
def after_login_entered_during_multi_phase_login
|
67
95
|
set_notice_now_flash need_password_notice_flash
|
96
|
+
if multi_phase_login_forms.length == 1 && (meth = multi_phase_login_forms[0][2])
|
97
|
+
send(meth)
|
98
|
+
end
|
68
99
|
end
|
69
100
|
|
70
101
|
def skip_login_field_on_login?
|
@@ -85,16 +116,30 @@ module Rodauth
|
|
85
116
|
"<input type='hidden' name=\"#{login_param}\" value=\"#{scope.h param(login_param)}\" />"
|
86
117
|
end
|
87
118
|
|
119
|
+
def render_multi_phase_login_forms
|
120
|
+
multi_phase_login_forms.sort.map{|_, form, _| form}.join("\n")
|
121
|
+
end
|
122
|
+
|
88
123
|
private
|
89
124
|
|
90
|
-
def
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
125
|
+
def _login_form_footer_links
|
126
|
+
[]
|
127
|
+
end
|
128
|
+
|
129
|
+
def _multi_phase_login_forms
|
130
|
+
forms = []
|
131
|
+
forms << [10, render("login-form"), nil] if has_password?
|
132
|
+
forms
|
133
|
+
end
|
134
|
+
|
135
|
+
def _login_form_footer
|
136
|
+
return '' if _login_form_footer_links.empty?
|
137
|
+
render('login-form-footer')
|
138
|
+
end
|
139
|
+
|
140
|
+
def _login(auth_type)
|
141
|
+
warn("Deprecated #_login method called, use #login instead.")
|
142
|
+
login(auth_type)
|
98
143
|
end
|
99
144
|
end
|
100
145
|
end
|