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