rodauth 1.22.0 → 2.3.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 +190 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +210 -80
- 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 +75 -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 +6 -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/recovery_codes.rdoc +18 -12
- data/doc/release_notes/1.23.0.txt +32 -0
- 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/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 +36 -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 +144 -43
- data/lib/rodauth/features/change_password_notify.rb +2 -2
- 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 +1 -1
- data/lib/rodauth/features/email_auth.rb +31 -30
- data/lib/rodauth/features/email_base.rb +9 -4
- 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 +12 -14
- data/lib/rodauth/features/login.rb +64 -15
- data/lib/rodauth/features/login_password_requirements_base.rb +13 -8
- data/lib/rodauth/features/otp.rb +77 -80
- data/lib/rodauth/features/password_complexity.rb +8 -13
- data/lib/rodauth/features/password_expiration.rb +2 -2
- data/lib/rodauth/features/password_grace_period.rb +17 -10
- data/lib/rodauth/features/recovery_codes.rb +49 -53
- data/lib/rodauth/features/remember.rb +11 -27
- data/lib/rodauth/features/reset_password.rb +26 -26
- 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 +62 -72
- data/lib/rodauth/features/two_factor_base.rb +134 -30
- data/lib/rodauth/features/verify_account.rb +29 -21
- data/lib/rodauth/features/verify_account_grace_period.rb +18 -9
- data/lib/rodauth/features/verify_login_change.rb +12 -11
- 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 +2 -3
- 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 +3 -3
- 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 +4 -4
- data/templates/unlock-account.str +1 -1
- data/templates/verify-account-resend.str +3 -3
- 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 +94 -54
- data/Rakefile +0 -179
- data/doc/verify_change_login.rdoc +0 -11
- data/lib/rodauth/features/verify_change_login.rb +0 -20
- data/spec/account_expiration_spec.rb +0 -225
- data/spec/all.rb +0 -1
- data/spec/change_login_spec.rb +0 -156
- data/spec/change_password_notify_spec.rb +0 -33
- data/spec/change_password_spec.rb +0 -202
- data/spec/close_account_spec.rb +0 -162
- data/spec/confirm_password_spec.rb +0 -70
- data/spec/create_account_spec.rb +0 -127
- data/spec/disallow_common_passwords_spec.rb +0 -93
- data/spec/disallow_password_reuse_spec.rb +0 -179
- data/spec/email_auth_spec.rb +0 -285
- data/spec/http_basic_auth_spec.rb +0 -143
- data/spec/jwt_cors_spec.rb +0 -57
- data/spec/jwt_refresh_spec.rb +0 -256
- data/spec/jwt_spec.rb +0 -235
- data/spec/lockout_spec.rb +0 -250
- data/spec/login_spec.rb +0 -328
- data/spec/migrate/001_tables.rb +0 -184
- data/spec/migrate/002_account_password_hash_column.rb +0 -11
- data/spec/migrate_password/001_tables.rb +0 -73
- data/spec/migrate_travis/001_tables.rb +0 -141
- data/spec/password_complexity_spec.rb +0 -109
- data/spec/password_expiration_spec.rb +0 -244
- data/spec/password_grace_period_spec.rb +0 -93
- data/spec/remember_spec.rb +0 -451
- data/spec/reset_password_spec.rb +0 -229
- data/spec/rodauth_spec.rb +0 -343
- data/spec/session_expiration_spec.rb +0 -58
- data/spec/single_session_spec.rb +0 -127
- data/spec/spec_helper.rb +0 -327
- data/spec/two_factor_spec.rb +0 -1462
- data/spec/update_password_hash_spec.rb +0 -40
- data/spec/verify_account_grace_period_spec.rb +0 -171
- data/spec/verify_account_spec.rb +0 -240
- data/spec/verify_change_login_spec.rb +0 -46
- data/spec/verify_login_change_spec.rb +0 -232
- data/spec/views/layout-other.str +0 -11
- data/spec/views/layout.str +0 -11
- data/spec/views/login.str +0 -21
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,8 +216,7 @@ module Rodauth
|
|
|
217
216
|
end
|
|
218
217
|
|
|
219
218
|
def send_unlock_account_email
|
|
220
|
-
|
|
221
|
-
create_unlock_account_email.deliver!
|
|
219
|
+
send_email(create_unlock_account_email)
|
|
222
220
|
end
|
|
223
221
|
|
|
224
222
|
def unlock_account_email_link
|
|
@@ -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,44 +62,84 @@ 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?
|
|
71
102
|
return false unless use_multi_phase_login?
|
|
72
|
-
|
|
103
|
+
valid_login_entered?
|
|
73
104
|
end
|
|
74
105
|
|
|
75
106
|
def skip_password_field_on_login?
|
|
76
107
|
return false unless use_multi_phase_login?
|
|
77
|
-
|
|
108
|
+
!valid_login_entered?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def valid_login_entered?
|
|
112
|
+
@valid_login_entered
|
|
78
113
|
end
|
|
79
114
|
|
|
80
115
|
def login_hidden_field
|
|
81
116
|
"<input type='hidden' name=\"#{login_param}\" value=\"#{scope.h param(login_param)}\" />"
|
|
82
117
|
end
|
|
83
118
|
|
|
119
|
+
def render_multi_phase_login_forms
|
|
120
|
+
multi_phase_login_forms.sort.map{|_, form, _| form}.join("\n")
|
|
121
|
+
end
|
|
122
|
+
|
|
84
123
|
private
|
|
85
124
|
|
|
86
|
-
def
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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)
|
|
94
143
|
end
|
|
95
144
|
end
|
|
96
145
|
end
|