rodauth 2.0.0 → 2.5.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 +58 -0
- data/README.rdoc +14 -0
- data/doc/base.rdoc +2 -0
- 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/jwt_refresh.rdoc +11 -0
- data/doc/login.rdoc +8 -0
- data/doc/login_password_requirements_base.rdoc +3 -0
- data/doc/otp.rdoc +1 -0
- data/doc/password_pepper.rdoc +44 -0
- data/doc/release_notes/2.1.0.txt +31 -0
- data/doc/release_notes/2.2.0.txt +39 -0
- data/doc/release_notes/2.3.0.txt +37 -0
- data/doc/release_notes/2.4.0.txt +22 -0
- data/doc/release_notes/2.5.0.txt +20 -0
- data/doc/verify_login_change.rdoc +1 -0
- data/lib/rodauth.rb +5 -5
- data/lib/rodauth/features/active_sessions.rb +5 -7
- data/lib/rodauth/features/audit_logging.rb +2 -0
- data/lib/rodauth/features/base.rb +21 -2
- data/lib/rodauth/features/change_password.rb +1 -1
- data/lib/rodauth/features/close_account.rb +8 -6
- data/lib/rodauth/features/create_account.rb +1 -0
- data/lib/rodauth/features/disallow_password_reuse.rb +4 -2
- data/lib/rodauth/features/email_auth.rb +2 -2
- data/lib/rodauth/features/http_basic_auth.rb +15 -2
- data/lib/rodauth/features/jwt.rb +12 -8
- data/lib/rodauth/features/jwt_cors.rb +15 -15
- data/lib/rodauth/features/jwt_refresh.rb +39 -10
- data/lib/rodauth/features/login.rb +23 -12
- data/lib/rodauth/features/login_password_requirements_base.rb +9 -4
- data/lib/rodauth/features/otp.rb +5 -1
- data/lib/rodauth/features/password_complexity.rb +4 -2
- data/lib/rodauth/features/password_pepper.rb +45 -0
- data/lib/rodauth/features/remember.rb +2 -0
- data/lib/rodauth/features/session_expiration.rb +1 -6
- data/lib/rodauth/features/single_session.rb +1 -1
- data/lib/rodauth/features/sms_codes.rb +0 -1
- data/lib/rodauth/features/two_factor_base.rb +4 -4
- data/lib/rodauth/features/verify_account.rb +5 -0
- data/lib/rodauth/features/verify_account_grace_period.rb +3 -5
- data/lib/rodauth/features/verify_login_change.rb +2 -1
- data/lib/rodauth/features/webauthn.rb +1 -3
- data/lib/rodauth/features/webauthn_login.rb +1 -1
- data/lib/rodauth/migrations.rb +16 -5
- data/lib/rodauth/version.rb +1 -1
- data/templates/password-field.str +1 -1
- metadata +37 -5
@@ -23,6 +23,8 @@ module Rodauth
|
|
23
23
|
auth_cached_method :login_form_footer_links
|
24
24
|
auth_cached_method :login_form_footer
|
25
25
|
|
26
|
+
auth_value_methods :login_return_to_requested_location_path
|
27
|
+
|
26
28
|
route do |r|
|
27
29
|
check_already_logged_in
|
28
30
|
before_login_route
|
@@ -62,7 +64,7 @@ module Rodauth
|
|
62
64
|
throw_error_status(login_error_status, password_param, invalid_password_message)
|
63
65
|
end
|
64
66
|
|
65
|
-
|
67
|
+
login('password')
|
66
68
|
end
|
67
69
|
|
68
70
|
set_error_flash login_error_flash unless skip_error_flash
|
@@ -72,13 +74,29 @@ module Rodauth
|
|
72
74
|
|
73
75
|
attr_reader :login_form_header
|
74
76
|
|
77
|
+
def login(auth_type)
|
78
|
+
saved_login_redirect = remove_session_value(login_redirect_session_key)
|
79
|
+
transaction do
|
80
|
+
before_login
|
81
|
+
login_session(auth_type)
|
82
|
+
yield if block_given?
|
83
|
+
after_login
|
84
|
+
end
|
85
|
+
set_notice_flash login_notice_flash
|
86
|
+
redirect(saved_login_redirect || login_redirect)
|
87
|
+
end
|
88
|
+
|
75
89
|
def login_required
|
76
|
-
if login_return_to_requested_location?
|
77
|
-
set_session_value(login_redirect_session_key,
|
90
|
+
if login_return_to_requested_location? && (path = login_return_to_requested_location_path)
|
91
|
+
set_session_value(login_redirect_session_key, path)
|
78
92
|
end
|
79
93
|
super
|
80
94
|
end
|
81
95
|
|
96
|
+
def login_return_to_requested_location_path
|
97
|
+
request.fullpath if request.get?
|
98
|
+
end
|
99
|
+
|
82
100
|
def after_login_entered_during_multi_phase_login
|
83
101
|
set_notice_now_flash need_password_notice_flash
|
84
102
|
if multi_phase_login_forms.length == 1 && (meth = multi_phase_login_forms[0][2])
|
@@ -126,15 +144,8 @@ module Rodauth
|
|
126
144
|
end
|
127
145
|
|
128
146
|
def _login(auth_type)
|
129
|
-
|
130
|
-
|
131
|
-
before_login
|
132
|
-
login_session(auth_type)
|
133
|
-
yield if block_given?
|
134
|
-
after_login
|
135
|
-
end
|
136
|
-
set_notice_flash login_notice_flash
|
137
|
-
redirect(saved_login_redirect || login_redirect)
|
147
|
+
warn("Deprecated #_login method called, use #login instead.")
|
148
|
+
login(auth_type)
|
138
149
|
end
|
139
150
|
end
|
140
151
|
end
|
@@ -4,8 +4,10 @@ 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
|
+
auth_value_method :login_email_regexp, /\A[^,;@ \r\n]+@[^,@; \r\n]+\.[^,@; \r\n]+\z/
|
7
8
|
auth_value_method :login_minimum_length, 3
|
8
9
|
auth_value_method :login_maximum_length, 255
|
10
|
+
translatable_method :login_not_valid_email_message, 'not a valid email address'
|
9
11
|
translatable_method :logins_do_not_match_message, 'logins do not match'
|
10
12
|
auth_value_method :password_confirm_param, 'password-confirm'
|
11
13
|
auth_value_method :password_minimum_length, 6
|
@@ -28,6 +30,7 @@ module Rodauth
|
|
28
30
|
|
29
31
|
auth_methods(
|
30
32
|
:login_meets_requirements?,
|
33
|
+
:login_valid_email?,
|
31
34
|
:password_hash,
|
32
35
|
:password_meets_requirements?,
|
33
36
|
:set_password
|
@@ -104,13 +107,15 @@ module Rodauth
|
|
104
107
|
|
105
108
|
def login_meets_email_requirements?(login)
|
106
109
|
return true unless require_email_address_logins?
|
107
|
-
if login
|
108
|
-
|
109
|
-
end
|
110
|
-
@login_requirement_message = 'not a valid email address'
|
110
|
+
return true if login_valid_email?(login)
|
111
|
+
@login_requirement_message = login_not_valid_email_message
|
111
112
|
return false
|
112
113
|
end
|
113
114
|
|
115
|
+
def login_valid_email?(login)
|
116
|
+
login =~ login_email_regexp
|
117
|
+
end
|
118
|
+
|
114
119
|
def password_meets_length_requirements?(password)
|
115
120
|
return true if password_minimum_length <= password.length
|
116
121
|
@password_requirement_message = password_too_short_message
|
data/lib/rodauth/features/otp.rb
CHANGED
@@ -79,6 +79,7 @@ module Rodauth
|
|
79
79
|
:otp,
|
80
80
|
:otp_exists?,
|
81
81
|
:otp_key,
|
82
|
+
:otp_last_use,
|
82
83
|
:otp_locked_out?,
|
83
84
|
:otp_new_secret,
|
84
85
|
:otp_provisioning_name,
|
@@ -255,7 +256,6 @@ module Rodauth
|
|
255
256
|
def otp_remove
|
256
257
|
otp_key_ds.delete
|
257
258
|
@otp_key = nil
|
258
|
-
super if defined?(super)
|
259
259
|
end
|
260
260
|
|
261
261
|
def otp_add_key
|
@@ -269,6 +269,10 @@ module Rodauth
|
|
269
269
|
update(otp_keys_last_use_column=>Sequel::CURRENT_TIMESTAMP) == 1
|
270
270
|
end
|
271
271
|
|
272
|
+
def otp_last_use
|
273
|
+
convert_timestamp(otp_key_ds.get(otp_keys_last_use_column))
|
274
|
+
end
|
275
|
+
|
272
276
|
def otp_record_authentication_failure
|
273
277
|
otp_key_ds.update(otp_keys_failures_column=>Sequel.identifier(otp_keys_failures_column) + 1)
|
274
278
|
end
|
@@ -26,14 +26,16 @@ module Rodauth
|
|
26
26
|
|
27
27
|
def post_configure
|
28
28
|
super
|
29
|
-
return if
|
29
|
+
return if method(:password_dictionary).owner != Rodauth::PasswordComplexity
|
30
30
|
|
31
31
|
case password_dictionary_file
|
32
32
|
when false
|
33
|
-
|
33
|
+
# nothing
|
34
34
|
when nil
|
35
35
|
default_dictionary_file = '/usr/share/dict/words'
|
36
|
+
# :nocov:
|
36
37
|
if File.file?(default_dictionary_file)
|
38
|
+
# :nocov:
|
37
39
|
words = File.read(default_dictionary_file)
|
38
40
|
end
|
39
41
|
else
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Rodauth
|
4
|
+
Feature.define(:password_pepper, :PasswordPepper) do
|
5
|
+
depends :login_password_requirements_base
|
6
|
+
|
7
|
+
auth_value_method :password_pepper, nil
|
8
|
+
auth_value_method :previous_password_peppers, [""]
|
9
|
+
auth_value_method :password_pepper_update?, true
|
10
|
+
|
11
|
+
def password_match?(password)
|
12
|
+
if (result = super) && @previous_pepper_matched && password_pepper_update?
|
13
|
+
set_password(password)
|
14
|
+
end
|
15
|
+
|
16
|
+
result
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def password_hash(password)
|
22
|
+
super(password + password_pepper.to_s)
|
23
|
+
end
|
24
|
+
|
25
|
+
def password_hash_match?(hash, password)
|
26
|
+
return super if password_pepper.nil?
|
27
|
+
|
28
|
+
return true if super(hash, password + password_pepper)
|
29
|
+
|
30
|
+
@previous_pepper_matched = previous_password_peppers.any? do |pepper|
|
31
|
+
super(hash, password + pepper)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def database_function_password_match?(name, hash_id, password, salt)
|
36
|
+
return super if password_pepper.nil?
|
37
|
+
|
38
|
+
return true if super(name, hash_id, password + password_pepper, salt)
|
39
|
+
|
40
|
+
@previous_pepper_matched = previous_password_peppers.any? do |pepper|
|
41
|
+
super(name, hash_id, password + pepper, salt)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -58,7 +58,9 @@ module Rodauth
|
|
58
58
|
if [remember_remember_param_value, remember_forget_param_value, remember_disable_param_value].include?(remember)
|
59
59
|
transaction do
|
60
60
|
before_remember
|
61
|
+
# :nocov:
|
61
62
|
case remember
|
63
|
+
# :nocov:
|
62
64
|
when remember_remember_param_value
|
63
65
|
remember_login
|
64
66
|
when remember_forget_param_value
|
@@ -3,6 +3,7 @@
|
|
3
3
|
module Rodauth
|
4
4
|
Feature.define(:session_expiration, :SessionExpiration) do
|
5
5
|
error_flash "This session has expired, please login again"
|
6
|
+
redirect{require_login_redirect}
|
6
7
|
|
7
8
|
auth_value_method :max_session_lifetime, 86400
|
8
9
|
session_key :session_created_session_key, :session_created_at
|
@@ -11,8 +12,6 @@ module Rodauth
|
|
11
12
|
auth_value_method :session_inactivity_timeout, 1800
|
12
13
|
session_key :session_last_activity_session_key, :last_session_activity_at
|
13
14
|
|
14
|
-
auth_value_methods :session_expiration_redirect
|
15
|
-
|
16
15
|
def check_session_expiration
|
17
16
|
return unless logged_in?
|
18
17
|
|
@@ -43,10 +42,6 @@ module Rodauth
|
|
43
42
|
redirect session_expiration_redirect
|
44
43
|
end
|
45
44
|
|
46
|
-
def session_expiration_redirect
|
47
|
-
require_login_redirect
|
48
|
-
end
|
49
|
-
|
50
45
|
def update_session
|
51
46
|
super
|
52
47
|
t = Time.now.to_i
|
@@ -125,7 +125,7 @@ module Rodauth
|
|
125
125
|
return true if two_factor_authenticated?
|
126
126
|
|
127
127
|
# True if authenticated via single factor and 2nd factor not setup
|
128
|
-
!
|
128
|
+
!uses_two_factor_authentication?
|
129
129
|
end
|
130
130
|
|
131
131
|
def require_authentication
|
@@ -134,7 +134,7 @@ module Rodauth
|
|
134
134
|
# Avoid database query if already authenticated via 2nd factor
|
135
135
|
return if two_factor_authenticated?
|
136
136
|
|
137
|
-
require_two_factor_authenticated if
|
137
|
+
require_two_factor_authenticated if uses_two_factor_authentication?
|
138
138
|
end
|
139
139
|
|
140
140
|
def require_two_factor_setup
|
@@ -208,11 +208,11 @@ module Rodauth
|
|
208
208
|
end
|
209
209
|
|
210
210
|
def _two_factor_setup_links
|
211
|
-
|
211
|
+
[]
|
212
212
|
end
|
213
213
|
|
214
214
|
def _two_factor_remove_links
|
215
|
-
|
215
|
+
[]
|
216
216
|
end
|
217
217
|
|
218
218
|
def _two_factor_remove_all_from_session
|
@@ -70,6 +70,7 @@ module Rodauth
|
|
70
70
|
end
|
71
71
|
|
72
72
|
r.post do
|
73
|
+
verified = false
|
73
74
|
if account_from_login(param(login_param)) && allow_resending_verify_account_email?
|
74
75
|
if verify_account_email_recently_sent?
|
75
76
|
set_redirect_error_flash verify_account_email_recently_sent_error_flash
|
@@ -79,8 +80,11 @@ module Rodauth
|
|
79
80
|
before_verify_account_email_resend
|
80
81
|
if verify_account_email_resend
|
81
82
|
after_verify_account_email_resend
|
83
|
+
verified = true
|
82
84
|
end
|
85
|
+
end
|
83
86
|
|
87
|
+
if verified
|
84
88
|
set_notice_flash verify_account_email_sent_notice_flash
|
85
89
|
else
|
86
90
|
set_redirect_error_status(no_matching_login_error_status)
|
@@ -94,6 +98,7 @@ module Rodauth
|
|
94
98
|
route do |r|
|
95
99
|
verify_account_check_already_logged_in
|
96
100
|
before_verify_account_route
|
101
|
+
@password_field_autocomplete_value = 'new-password'
|
97
102
|
|
98
103
|
r.get do
|
99
104
|
if key = param_or_nil(verify_account_key_param)
|
@@ -23,7 +23,7 @@ module Rodauth
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def open_account?
|
26
|
-
super || account_in_unverified_grace_period?
|
26
|
+
super || (account_in_unverified_grace_period? && has_password?)
|
27
27
|
end
|
28
28
|
|
29
29
|
def verify_account_set_password?
|
@@ -53,10 +53,7 @@ module Rodauth
|
|
53
53
|
end
|
54
54
|
|
55
55
|
def allow_email_auth?
|
56
|
-
|
57
|
-
return false unless super
|
58
|
-
end
|
59
|
-
!account_in_unverified_grace_period?
|
56
|
+
(defined?(super) ? super : true) && !account_in_unverified_grace_period?
|
60
57
|
end
|
61
58
|
|
62
59
|
def verify_account_check_already_logged_in
|
@@ -75,6 +72,7 @@ module Rodauth
|
|
75
72
|
end
|
76
73
|
|
77
74
|
def account_in_unverified_grace_period?
|
75
|
+
account || account_from_session
|
78
76
|
account[account_status_column] == account_unverified_status_value &&
|
79
77
|
verify_account_grace_period &&
|
80
78
|
!verify_account_ds.where(Sequel.date_add(verification_requested_at_column, :seconds=>verify_account_grace_period) > Sequel::CURRENT_TIMESTAMP).empty?
|
@@ -8,6 +8,7 @@ module Rodauth
|
|
8
8
|
error_flash "Unable to change login as there is already an account with the new login", 'verify_login_change_duplicate_account'
|
9
9
|
error_flash "There was an error verifying your login change: invalid verify login change key", 'no_matching_verify_login_change_key'
|
10
10
|
notice_flash "Your login change has been verified"
|
11
|
+
notice_flash "An email has been sent to you with a link to verify your login change", 'change_login_needs_verification'
|
11
12
|
loaded_templates %w'verify-login-change verify-login-change-email'
|
12
13
|
view 'verify-login-change', 'Verify Login Change'
|
13
14
|
additional_form_tags
|
@@ -131,7 +132,7 @@ module Rodauth
|
|
131
132
|
end
|
132
133
|
|
133
134
|
def change_login_notice_flash
|
134
|
-
|
135
|
+
change_login_needs_verification_notice_flash
|
135
136
|
end
|
136
137
|
|
137
138
|
def verify_login_change_old_login
|
@@ -377,9 +377,7 @@ module Rodauth
|
|
377
377
|
end
|
378
378
|
|
379
379
|
def remove_webauthn_key(webauthn_id)
|
380
|
-
|
381
|
-
super if defined?(super)
|
382
|
-
ret
|
380
|
+
webauthn_keys_ds.where(webauthn_keys_webauthn_id_column=>webauthn_id).delete == 1
|
383
381
|
end
|
384
382
|
|
385
383
|
def remove_all_webauthn_keys_and_user_ids
|
data/lib/rodauth/migrations.rb
CHANGED
@@ -9,9 +9,14 @@ module Rodauth
|
|
9
9
|
case db.database_type
|
10
10
|
when :postgres
|
11
11
|
search_path = opts[:search_path] || 'public, pg_temp'
|
12
|
+
primary_key_type =
|
13
|
+
case db.schema(table_name).find { |row| row.first == :id }[1][:db_type]
|
14
|
+
when 'uuid' then :uuid
|
15
|
+
else :int8
|
16
|
+
end
|
12
17
|
|
13
18
|
db.run <<END
|
14
|
-
CREATE OR REPLACE FUNCTION #{get_salt_name}(acct_id
|
19
|
+
CREATE OR REPLACE FUNCTION #{get_salt_name}(acct_id #{primary_key_type}) RETURNS text AS $$
|
15
20
|
DECLARE salt text;
|
16
21
|
BEGIN
|
17
22
|
SELECT substr(password_hash, 0, 30) INTO salt
|
@@ -25,7 +30,7 @@ SET search_path = #{search_path};
|
|
25
30
|
END
|
26
31
|
|
27
32
|
db.run <<END
|
28
|
-
CREATE OR REPLACE FUNCTION #{valid_hash_name}(acct_id
|
33
|
+
CREATE OR REPLACE FUNCTION #{valid_hash_name}(acct_id #{primary_key_type}, hash text) RETURNS boolean AS $$
|
29
34
|
DECLARE valid boolean;
|
30
35
|
BEGIN
|
31
36
|
SELECT password_hash = hash INTO valid
|
@@ -100,13 +105,19 @@ END
|
|
100
105
|
end
|
101
106
|
|
102
107
|
def self.drop_database_authentication_functions(db, opts={})
|
108
|
+
table_name = opts[:table_name] || :account_password_hashes
|
103
109
|
get_salt_name = opts[:get_salt_name] || :rodauth_get_salt
|
104
110
|
valid_hash_name = opts[:valid_hash_name] || :rodauth_valid_password_hash
|
105
111
|
|
106
112
|
case db.database_type
|
107
113
|
when :postgres
|
108
|
-
|
109
|
-
|
114
|
+
primary_key_type =
|
115
|
+
case db.schema(table_name).find { |row| row.first == :id }[1][:db_type]
|
116
|
+
when 'uuid' then :uuid
|
117
|
+
else :int8
|
118
|
+
end
|
119
|
+
db.run "DROP FUNCTION #{get_salt_name}(#{primary_key_type})"
|
120
|
+
db.run "DROP FUNCTION #{valid_hash_name}(#{primary_key_type}, text)"
|
110
121
|
when :mysql, :mssql
|
111
122
|
db.run "DROP FUNCTION #{get_salt_name}"
|
112
123
|
db.run "DROP FUNCTION #{valid_hash_name}"
|
@@ -118,6 +129,6 @@ END
|
|
118
129
|
end
|
119
130
|
|
120
131
|
def self.drop_database_previous_password_check_functions(db, opts={})
|
121
|
-
drop_database_authentication_functions(db, {:get_salt_name=>:rodauth_get_previous_salt, :valid_hash_name=>:rodauth_previous_password_hash_match}.merge(opts))
|
132
|
+
drop_database_authentication_functions(db, {:table_name=>:account_previous_password_hashes, :get_salt_name=>:rodauth_get_previous_salt, :valid_hash_name=>:rodauth_previous_password_hash_match}.merge(opts))
|
122
133
|
end
|
123
134
|
end
|
data/lib/rodauth/version.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
1
|
<div class="form-group">
|
2
2
|
<label for="password">#{rodauth.password_label}#{rodauth.input_field_label_suffix}</label>
|
3
|
-
#{rodauth.input_field_string(rodauth.password_param, 'password', :type => 'password', :autocomplete=>
|
3
|
+
#{rodauth.input_field_string(rodauth.password_param, 'password', :type => 'password', :autocomplete=>rodauth.password_field_autocomplete_value)}
|
4
4
|
</div>
|