rodauth 1.20.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|