rodauth 1.20.0 → 2.1.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 +170 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +211 -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 +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/http_basic_auth.rdoc +10 -1
- data/doc/internals.rdoc +1 -1
- data/doc/jwt.rdoc +22 -22
- data/doc/jwt_cors.rdoc +22 -0
- data/doc/jwt_refresh.rdoc +12 -8
- data/doc/lockout.rdoc +17 -15
- data/doc/login.rdoc +10 -2
- data/doc/login_password_requirements_base.rdoc +15 -37
- data/doc/logout.rdoc +2 -2
- data/doc/otp.rdoc +24 -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.21.0.txt +12 -0
- data/doc/release_notes/1.22.0.txt +11 -0
- 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/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 +32 -24
- data/lib/rodauth/features/account_expiration.rb +5 -5
- data/lib/rodauth/features/active_sessions.rb +160 -0
- data/lib/rodauth/features/audit_logging.rb +96 -0
- data/lib/rodauth/features/base.rb +144 -43
- data/lib/rodauth/features/change_password_notify.rb +2 -2
- 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 +30 -29
- 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 +58 -10
- data/lib/rodauth/features/jwt_cors.rb +53 -0
- data/lib/rodauth/features/jwt_refresh.rb +3 -3
- data/lib/rodauth/features/lockout.rb +12 -14
- data/lib/rodauth/features/login.rb +54 -10
- data/lib/rodauth/features/login_password_requirements_base.rb +4 -4
- data/lib/rodauth/features/otp.rb +72 -74
- data/lib/rodauth/features/password_complexity.rb +4 -11
- 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 +6 -4
- data/lib/rodauth/features/single_session.rb +7 -5
- data/lib/rodauth/features/sms_codes.rb +62 -71
- data/lib/rodauth/features/two_factor_base.rb +132 -28
- data/lib/rodauth/features/verify_account.rb +25 -21
- data/lib/rodauth/features/verify_account_grace_period.rb +20 -9
- data/lib/rodauth/features/verify_login_change.rb +12 -11
- data/lib/rodauth/features/webauthn.rb +507 -0
- data/lib/rodauth/features/webauthn_login.rb +70 -0
- data/lib/rodauth/features/webauthn_verify_account.rb +46 -0
- 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-email.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-email.str +1 -1
- 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-email.str +1 -1
- data/templates/unlock-account-request.str +4 -4
- data/templates/unlock-account.str +1 -1
- data/templates/verify-account-email.str +1 -1
- data/templates/verify-account-resend.str +3 -3
- data/templates/verify-account.str +1 -2
- data/templates/verify-login-change-email.str +2 -1
- 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 +89 -50
- 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_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 -1423
- 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
|
|
|
@@ -140,13 +140,18 @@ module Rodauth
|
|
|
140
140
|
|
|
141
141
|
private
|
|
142
142
|
|
|
143
|
+
def check_csrf?
|
|
144
|
+
return false if use_jwt?
|
|
145
|
+
super
|
|
146
|
+
end
|
|
147
|
+
|
|
143
148
|
def before_rodauth
|
|
144
149
|
if json_request?
|
|
145
150
|
if jwt_check_accept? && (accept = request.env['HTTP_ACCEPT']) && accept !~ json_accept_regexp
|
|
146
151
|
response.status = 406
|
|
147
152
|
json_response[json_response_error_key] = json_not_accepted_error_message
|
|
148
153
|
response['Content-Type'] ||= json_response_content_type
|
|
149
|
-
response.write(
|
|
154
|
+
response.write(_json_response_body(json_response))
|
|
150
155
|
request.halt
|
|
151
156
|
end
|
|
152
157
|
|
|
@@ -173,6 +178,43 @@ module Rodauth
|
|
|
173
178
|
end
|
|
174
179
|
end
|
|
175
180
|
|
|
181
|
+
def before_webauthn_setup_route
|
|
182
|
+
super if defined?(super)
|
|
183
|
+
if use_jwt? && !param_or_nil(webauthn_setup_param)
|
|
184
|
+
cred = new_webauthn_credential
|
|
185
|
+
json_response[webauthn_setup_param] = cred.as_json
|
|
186
|
+
json_response[webauthn_setup_challenge_param] = cred.challenge
|
|
187
|
+
json_response[webauthn_setup_challenge_hmac_param] = compute_hmac(cred.challenge)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def before_webauthn_auth_route
|
|
192
|
+
super if defined?(super)
|
|
193
|
+
if use_jwt? && !param_or_nil(webauthn_auth_param)
|
|
194
|
+
cred = webauth_credential_options_for_get
|
|
195
|
+
json_response[webauthn_auth_param] = cred.as_json
|
|
196
|
+
json_response[webauthn_auth_challenge_param] = cred.challenge
|
|
197
|
+
json_response[webauthn_auth_challenge_hmac_param] = compute_hmac(cred.challenge)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def before_webauthn_login_route
|
|
202
|
+
super if defined?(super)
|
|
203
|
+
if use_jwt? && !param_or_nil(webauthn_auth_param) && account_from_login(param(login_param))
|
|
204
|
+
cred = webauth_credential_options_for_get
|
|
205
|
+
json_response[webauthn_auth_param] = cred.as_json
|
|
206
|
+
json_response[webauthn_auth_challenge_param] = cred.challenge
|
|
207
|
+
json_response[webauthn_auth_challenge_hmac_param] = compute_hmac(cred.challenge)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def before_webauthn_remove_route
|
|
212
|
+
super if defined?(super)
|
|
213
|
+
if use_jwt? && !param_or_nil(webauthn_remove_param)
|
|
214
|
+
json_response[webauthn_remove_param] = account_webauthn_usage
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
176
218
|
def before_otp_setup_route
|
|
177
219
|
super if defined?(super)
|
|
178
220
|
if use_jwt? && otp_keys_use_hmac? && !param_or_nil(otp_setup_raw_param)
|
|
@@ -204,6 +246,12 @@ module Rodauth
|
|
|
204
246
|
value
|
|
205
247
|
end
|
|
206
248
|
|
|
249
|
+
def remove_session_value(key)
|
|
250
|
+
value = super
|
|
251
|
+
set_jwt if use_jwt?
|
|
252
|
+
value
|
|
253
|
+
end
|
|
254
|
+
|
|
207
255
|
def json_response
|
|
208
256
|
@json_response ||= {}
|
|
209
257
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Rodauth
|
|
4
|
+
Feature.define(:jwt_cors, :JwtCors) do
|
|
5
|
+
depends :jwt
|
|
6
|
+
|
|
7
|
+
auth_value_method :jwt_cors_allow_origin, false
|
|
8
|
+
auth_value_method :jwt_cors_allow_methods, 'POST'
|
|
9
|
+
auth_value_method :jwt_cors_allow_headers, 'Content-Type, Authorization, Accept'
|
|
10
|
+
auth_value_method :jwt_cors_expose_headers, 'Authorization'
|
|
11
|
+
auth_value_method :jwt_cors_max_age, 86400
|
|
12
|
+
|
|
13
|
+
auth_methods(:jwt_cors_allow?)
|
|
14
|
+
|
|
15
|
+
def jwt_cors_allow?
|
|
16
|
+
if origin = request.env['HTTP_ORIGIN']
|
|
17
|
+
case allowed = jwt_cors_allow_origin
|
|
18
|
+
when String
|
|
19
|
+
timing_safe_eql?(origin, allowed)
|
|
20
|
+
when Array
|
|
21
|
+
allowed.any?{|s| timing_safe_eql?(origin, s)}
|
|
22
|
+
when Regexp
|
|
23
|
+
allowed =~ origin
|
|
24
|
+
when true
|
|
25
|
+
true
|
|
26
|
+
else
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def before_rodauth
|
|
35
|
+
if (origin = request.env['HTTP_ORIGIN']) && jwt_cors_allow?
|
|
36
|
+
response['Access-Control-Allow-Origin'] = origin
|
|
37
|
+
|
|
38
|
+
# Handle CORS preflight request
|
|
39
|
+
if request.request_method == 'OPTIONS'
|
|
40
|
+
response['Access-Control-Allow-Methods'] = jwt_cors_allow_methods
|
|
41
|
+
response['Access-Control-Allow-Headers'] = jwt_cors_allow_headers
|
|
42
|
+
response['Access-Control-Max-Age'] = jwt_cors_max_age.to_s
|
|
43
|
+
response.status = 204
|
|
44
|
+
request.halt(response.finish)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
response['Access-Control-Expose-Headers'] = jwt_cors_expose_headers
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
super
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -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,10 +10,10 @@ 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
|
|
@@ -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,79 @@ module Rodauth
|
|
|
53
62
|
throw_error_status(login_error_status, password_param, invalid_password_message)
|
|
54
63
|
end
|
|
55
64
|
|
|
56
|
-
_login
|
|
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_required
|
|
76
|
+
if login_return_to_requested_location?
|
|
77
|
+
set_session_value(login_redirect_session_key, request.fullpath)
|
|
78
|
+
end
|
|
79
|
+
super
|
|
80
|
+
end
|
|
81
|
+
|
|
66
82
|
def after_login_entered_during_multi_phase_login
|
|
67
83
|
set_notice_now_flash need_password_notice_flash
|
|
84
|
+
if multi_phase_login_forms.length == 1 && (meth = multi_phase_login_forms[0][2])
|
|
85
|
+
send(meth)
|
|
86
|
+
end
|
|
68
87
|
end
|
|
69
88
|
|
|
70
89
|
def skip_login_field_on_login?
|
|
71
90
|
return false unless use_multi_phase_login?
|
|
72
|
-
|
|
91
|
+
valid_login_entered?
|
|
73
92
|
end
|
|
74
93
|
|
|
75
94
|
def skip_password_field_on_login?
|
|
76
95
|
return false unless use_multi_phase_login?
|
|
77
|
-
|
|
96
|
+
!valid_login_entered?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def valid_login_entered?
|
|
100
|
+
@valid_login_entered
|
|
78
101
|
end
|
|
79
102
|
|
|
80
103
|
def login_hidden_field
|
|
81
104
|
"<input type='hidden' name=\"#{login_param}\" value=\"#{scope.h param(login_param)}\" />"
|
|
82
105
|
end
|
|
83
106
|
|
|
107
|
+
def render_multi_phase_login_forms
|
|
108
|
+
multi_phase_login_forms.sort.map{|_, form, _| form}.join("\n")
|
|
109
|
+
end
|
|
110
|
+
|
|
84
111
|
private
|
|
85
112
|
|
|
86
|
-
def
|
|
113
|
+
def _login_form_footer_links
|
|
114
|
+
[]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def _multi_phase_login_forms
|
|
118
|
+
forms = []
|
|
119
|
+
forms << [10, render("login-form"), nil] if has_password?
|
|
120
|
+
forms
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def _login_form_footer
|
|
124
|
+
return '' if _login_form_footer_links.empty?
|
|
125
|
+
render('login-form-footer')
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def _login(auth_type)
|
|
129
|
+
saved_login_redirect = remove_session_value(login_redirect_session_key)
|
|
87
130
|
transaction do
|
|
88
131
|
before_login
|
|
89
|
-
|
|
132
|
+
login_session(auth_type)
|
|
133
|
+
yield if block_given?
|
|
90
134
|
after_login
|
|
91
135
|
end
|
|
92
136
|
set_notice_flash login_notice_flash
|
|
93
|
-
redirect login_redirect
|
|
137
|
+
redirect(saved_login_redirect || login_redirect)
|
|
94
138
|
end
|
|
95
139
|
end
|
|
96
140
|
end
|
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module Rodauth
|
|
4
4
|
Feature.define(:login_password_requirements_base, :LoginPasswordRequirementsBase) do
|
|
5
|
-
|
|
5
|
+
translatable_method :already_an_account_with_this_login_message, 'already an account with this login'
|
|
6
6
|
auth_value_method :login_confirm_param, 'login-confirm'
|
|
7
7
|
auth_value_method :login_minimum_length, 3
|
|
8
8
|
auth_value_method :login_maximum_length, 255
|
|
9
|
-
|
|
9
|
+
translatable_method :logins_do_not_match_message, 'logins do not match'
|
|
10
10
|
auth_value_method :password_confirm_param, 'password-confirm'
|
|
11
11
|
auth_value_method :password_minimum_length, 6
|
|
12
|
-
|
|
12
|
+
translatable_method :passwords_do_not_match_message, 'passwords do not match'
|
|
13
13
|
auth_value_method :require_email_address_logins?, true
|
|
14
14
|
auth_value_method :require_login_confirmation?, true
|
|
15
15
|
auth_value_method :require_password_confirmation?, true
|
|
16
|
-
|
|
16
|
+
translatable_method :same_as_existing_password_message, "invalid password, same as current password"
|
|
17
17
|
|
|
18
18
|
auth_value_methods(
|
|
19
19
|
:login_confirm_label,
|
data/lib/rodauth/features/otp.rb
CHANGED
|
@@ -19,62 +19,59 @@ module Rodauth
|
|
|
19
19
|
before 'otp_setup'
|
|
20
20
|
before 'otp_disable'
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
before_otp_auth_route(&block)
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
button 'Authenticate via 2nd Factor', 'otp_auth'
|
|
30
|
-
button 'Disable Two Factor Authentication', 'otp_disable'
|
|
31
|
-
button 'Setup Two Factor Authentication', 'otp_setup'
|
|
22
|
+
button 'Authenticate Using TOTP', 'otp_auth'
|
|
23
|
+
button 'Disable TOTP Authentication', 'otp_disable'
|
|
24
|
+
button 'Setup TOTP Authentication', 'otp_setup'
|
|
32
25
|
|
|
33
|
-
error_flash "Error disabling
|
|
34
|
-
error_flash "Error logging in via
|
|
35
|
-
error_flash "Error setting up
|
|
36
|
-
error_flash "You have already setup
|
|
26
|
+
error_flash "Error disabling TOTP authentication", 'otp_disable'
|
|
27
|
+
error_flash "Error logging in via TOTP authentication", 'otp_auth'
|
|
28
|
+
error_flash "Error setting up TOTP authentication", 'otp_setup'
|
|
29
|
+
error_flash "You have already setup TOTP authentication", 'otp_already_setup'
|
|
30
|
+
error_flash "TOTP authentication code use locked out due to numerous failures", 'otp_lockout'
|
|
37
31
|
|
|
38
|
-
notice_flash "
|
|
39
|
-
notice_flash "
|
|
32
|
+
notice_flash "TOTP authentication has been disabled", 'otp_disable'
|
|
33
|
+
notice_flash "TOTP authentication is now setup", 'otp_setup'
|
|
40
34
|
|
|
41
35
|
redirect :otp_disable
|
|
42
36
|
redirect :otp_already_setup
|
|
43
37
|
redirect :otp_setup
|
|
38
|
+
redirect(:otp_lockout){two_factor_auth_required_redirect}
|
|
44
39
|
|
|
45
40
|
loaded_templates %w'otp-disable otp-auth otp-setup otp-auth-code-field password-field'
|
|
46
|
-
view 'otp-disable', 'Disable
|
|
41
|
+
view 'otp-disable', 'Disable TOTP Authentication', 'otp_disable'
|
|
47
42
|
view 'otp-auth', 'Enter Authentication Code', 'otp_auth'
|
|
48
|
-
view 'otp-setup', 'Setup
|
|
43
|
+
view 'otp-setup', 'Setup TOTP Authentication', 'otp_setup'
|
|
44
|
+
|
|
45
|
+
translatable_method :otp_auth_link_text, "Authenticate Using TOTP"
|
|
46
|
+
translatable_method :otp_setup_link_text, "Setup TOTP Authentication"
|
|
47
|
+
translatable_method :otp_disable_link_text, "Disable TOTP Authentication"
|
|
49
48
|
|
|
50
49
|
auth_value_method :otp_auth_failures_limit, 5
|
|
51
|
-
|
|
50
|
+
translatable_method :otp_auth_label, 'Authentication Code'
|
|
52
51
|
auth_value_method :otp_auth_param, 'otp'
|
|
53
52
|
auth_value_method :otp_class, ROTP::TOTP
|
|
54
53
|
auth_value_method :otp_digits, nil
|
|
55
|
-
auth_value_method :otp_drift,
|
|
54
|
+
auth_value_method :otp_drift, 30
|
|
56
55
|
auth_value_method :otp_interval, nil
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
translatable_method :otp_invalid_auth_code_message, "Invalid authentication code"
|
|
57
|
+
translatable_method :otp_invalid_secret_message, "invalid secret"
|
|
59
58
|
auth_value_method :otp_keys_column, :key
|
|
60
59
|
auth_value_method :otp_keys_id_column, :id
|
|
61
60
|
auth_value_method :otp_keys_failures_column, :num_failures
|
|
62
61
|
auth_value_method :otp_keys_table, :account_otp_keys
|
|
63
62
|
auth_value_method :otp_keys_last_use_column, :last_use
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
translatable_method :otp_provisioning_uri_label, 'Provisioning URL'
|
|
64
|
+
translatable_method :otp_secret_label, 'Secret'
|
|
66
65
|
auth_value_method :otp_setup_param, 'otp_secret'
|
|
67
66
|
auth_value_method :otp_setup_raw_param, 'otp_raw_secret'
|
|
67
|
+
translatable_method :otp_auth_form_footer, ''
|
|
68
68
|
|
|
69
69
|
auth_cached_method :otp_key
|
|
70
70
|
auth_cached_method :otp
|
|
71
71
|
private :otp
|
|
72
72
|
|
|
73
73
|
auth_value_methods(
|
|
74
|
-
:otp_auth_form_footer,
|
|
75
74
|
:otp_issuer,
|
|
76
|
-
:otp_lockout_error_flash,
|
|
77
|
-
:otp_lockout_redirect,
|
|
78
75
|
:otp_keys_use_hmac?
|
|
79
76
|
)
|
|
80
77
|
|
|
@@ -103,7 +100,7 @@ module Rodauth
|
|
|
103
100
|
route(:otp_auth) do |r|
|
|
104
101
|
require_login
|
|
105
102
|
require_account_session
|
|
106
|
-
require_two_factor_not_authenticated
|
|
103
|
+
require_two_factor_not_authenticated('totp')
|
|
107
104
|
require_otp_setup
|
|
108
105
|
|
|
109
106
|
if otp_locked_out?
|
|
@@ -121,7 +118,7 @@ module Rodauth
|
|
|
121
118
|
r.post do
|
|
122
119
|
if otp_valid_code?(param(otp_auth_param)) && otp_update_last_use
|
|
123
120
|
before_otp_authentication
|
|
124
|
-
two_factor_authenticate(
|
|
121
|
+
two_factor_authenticate('totp')
|
|
125
122
|
end
|
|
126
123
|
|
|
127
124
|
otp_record_authentication_failure
|
|
@@ -173,7 +170,9 @@ module Rodauth
|
|
|
173
170
|
transaction do
|
|
174
171
|
before_otp_setup
|
|
175
172
|
otp_add_key
|
|
176
|
-
|
|
173
|
+
unless two_factor_authenticated?
|
|
174
|
+
two_factor_update_session('totp')
|
|
175
|
+
end
|
|
177
176
|
after_otp_setup
|
|
178
177
|
end
|
|
179
178
|
set_notice_flash otp_setup_notice_flash
|
|
@@ -199,7 +198,9 @@ module Rodauth
|
|
|
199
198
|
transaction do
|
|
200
199
|
before_otp_disable
|
|
201
200
|
otp_remove
|
|
202
|
-
|
|
201
|
+
if two_factor_login_type_match?('totp')
|
|
202
|
+
two_factor_remove_session('totp')
|
|
203
|
+
end
|
|
203
204
|
after_otp_disable
|
|
204
205
|
end
|
|
205
206
|
set_notice_flash otp_disable_notice_flash
|
|
@@ -213,18 +214,6 @@ module Rodauth
|
|
|
213
214
|
end
|
|
214
215
|
end
|
|
215
216
|
|
|
216
|
-
def two_factor_authentication_setup?
|
|
217
|
-
super || otp_exists?
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
def two_factor_need_setup_redirect
|
|
221
|
-
"#{prefix}/#{otp_setup_route}"
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
def two_factor_auth_required_redirect
|
|
225
|
-
"#{prefix}/#{otp_auth_route}"
|
|
226
|
-
end
|
|
227
|
-
|
|
228
217
|
def two_factor_remove
|
|
229
218
|
super
|
|
230
219
|
otp_remove
|
|
@@ -235,19 +224,6 @@ module Rodauth
|
|
|
235
224
|
otp_remove_auth_failures
|
|
236
225
|
end
|
|
237
226
|
|
|
238
|
-
def otp_auth_form_footer
|
|
239
|
-
super if defined?(super)
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
def otp_lockout_redirect
|
|
243
|
-
return super if defined?(super)
|
|
244
|
-
default_redirect
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
def otp_lockout_error_flash
|
|
248
|
-
"Authentication code use locked out due to numerous failures.#{super if defined?(super)}"
|
|
249
|
-
end
|
|
250
|
-
|
|
251
227
|
def require_otp_setup
|
|
252
228
|
unless otp_exists?
|
|
253
229
|
set_redirect_error_status(two_factor_not_setup_error_status)
|
|
@@ -265,11 +241,11 @@ module Rodauth
|
|
|
265
241
|
ot_pass = ot_pass.gsub(/\s+/, '')
|
|
266
242
|
if drift = otp_drift
|
|
267
243
|
if otp.respond_to?(:verify_with_drift)
|
|
244
|
+
# :nocov:
|
|
268
245
|
otp.verify_with_drift(ot_pass, drift)
|
|
269
|
-
else
|
|
270
246
|
# :nocov:
|
|
247
|
+
else
|
|
271
248
|
otp.verify(ot_pass, :drift_behind=>drift, :drift_ahead=>drift)
|
|
272
|
-
# :nocov:
|
|
273
249
|
end
|
|
274
250
|
else
|
|
275
251
|
otp.verify(ot_pass)
|
|
@@ -278,6 +254,7 @@ module Rodauth
|
|
|
278
254
|
|
|
279
255
|
def otp_remove
|
|
280
256
|
otp_key_ds.delete
|
|
257
|
+
@otp_key = nil
|
|
281
258
|
super if defined?(super)
|
|
282
259
|
end
|
|
283
260
|
|
|
@@ -309,7 +286,7 @@ module Rodauth
|
|
|
309
286
|
end
|
|
310
287
|
|
|
311
288
|
def otp_issuer
|
|
312
|
-
|
|
289
|
+
domain
|
|
313
290
|
end
|
|
314
291
|
|
|
315
292
|
def otp_provisioning_name
|
|
@@ -332,8 +309,37 @@ module Rodauth
|
|
|
332
309
|
!!hmac_secret
|
|
333
310
|
end
|
|
334
311
|
|
|
312
|
+
def possible_authentication_methods
|
|
313
|
+
methods = super
|
|
314
|
+
methods << 'totp' if otp_exists? && !@otp_tmp_key
|
|
315
|
+
methods
|
|
316
|
+
end
|
|
317
|
+
|
|
335
318
|
private
|
|
336
319
|
|
|
320
|
+
def _two_factor_auth_links
|
|
321
|
+
links = super
|
|
322
|
+
links << [20, otp_auth_path, otp_auth_link_text] if otp_exists? && !otp_locked_out?
|
|
323
|
+
links
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def _two_factor_setup_links
|
|
327
|
+
links = super
|
|
328
|
+
links << [20, otp_setup_path, otp_setup_link_text] unless otp_exists?
|
|
329
|
+
links
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def _two_factor_remove_links
|
|
333
|
+
links = super
|
|
334
|
+
links << [20, otp_disable_path, otp_disable_link_text] if otp_exists?
|
|
335
|
+
links
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def _two_factor_remove_all_from_session
|
|
339
|
+
two_factor_remove_session('totp')
|
|
340
|
+
super
|
|
341
|
+
end
|
|
342
|
+
|
|
337
343
|
def clear_cached_otp
|
|
338
344
|
remove_instance_variable(:@otp) if defined?(@otp)
|
|
339
345
|
end
|
|
@@ -357,32 +363,24 @@ module Rodauth
|
|
|
357
363
|
end
|
|
358
364
|
|
|
359
365
|
if ROTP::Base32.respond_to?(:random_base32)
|
|
360
|
-
# :nocov:
|
|
361
366
|
def otp_new_secret
|
|
362
|
-
ROTP::Base32.random_base32
|
|
367
|
+
ROTP::Base32.random_base32.downcase
|
|
363
368
|
end
|
|
364
|
-
# :nocov:
|
|
365
369
|
else
|
|
370
|
+
# :nocov:
|
|
366
371
|
def otp_new_secret
|
|
367
372
|
ROTP::Base32.random.downcase
|
|
368
373
|
end
|
|
374
|
+
# :nocov:
|
|
369
375
|
end
|
|
370
376
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
chars = 'abcdefghijklmnopqrstuvwxyz234567'
|
|
375
|
-
length.times.map{|i|chars[data[i] % 32].chr}.join
|
|
376
|
-
end
|
|
377
|
-
# :nocov:
|
|
378
|
-
else
|
|
379
|
-
def base32_encode(data, length)
|
|
380
|
-
chars = 'abcdefghijklmnopqrstuvwxyz234567'
|
|
381
|
-
length.times.map{|i|chars[data[i].ord % 32]}.join
|
|
382
|
-
end
|
|
377
|
+
def base32_encode(data, length)
|
|
378
|
+
chars = 'abcdefghijklmnopqrstuvwxyz234567'
|
|
379
|
+
length.times.map{|i|chars[data[i].ord % 32]}.join
|
|
383
380
|
end
|
|
384
381
|
|
|
385
382
|
def _otp_tmp_key(secret)
|
|
383
|
+
@otp_tmp_key = true
|
|
386
384
|
@otp_user_key = nil
|
|
387
385
|
@otp_key = secret
|
|
388
386
|
end
|