rodauth 0.10.0 → 1.0.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 +146 -0
- data/README.rdoc +644 -220
- data/Rakefile +99 -11
- data/doc/account_expiration.rdoc +55 -0
- data/doc/base.rdoc +104 -0
- data/doc/change_login.rdoc +29 -0
- data/doc/change_password.rdoc +26 -0
- data/doc/close_account.rdoc +31 -0
- data/doc/confirm_password.rdoc +22 -0
- data/doc/create_account.rdoc +34 -0
- data/doc/disallow_password_reuse.rdoc +37 -0
- data/doc/email_base.rdoc +19 -0
- data/doc/jwt.rdoc +35 -0
- data/doc/lockout.rdoc +83 -0
- data/doc/login.rdoc +27 -0
- data/doc/login_password_requirements_base.rdoc +50 -0
- data/doc/logout.rdoc +21 -0
- data/doc/otp.rdoc +100 -0
- data/doc/password_complexity.rdoc +50 -0
- data/doc/password_expiration.rdoc +52 -0
- data/doc/password_grace_period.rdoc +10 -0
- data/doc/recovery_codes.rdoc +60 -0
- data/doc/release_notes/1.0.0.txt +443 -0
- data/doc/remember.rdoc +82 -0
- data/doc/reset_password.rdoc +70 -0
- data/doc/session_expiration.rdoc +27 -0
- data/doc/single_session.rdoc +43 -0
- data/doc/sms_codes.rdoc +119 -0
- data/doc/two_factor_base.rdoc +27 -0
- data/doc/verify_account.rdoc +70 -0
- data/doc/verify_account_grace_period.rdoc +15 -0
- data/doc/verify_change_login.rdoc +9 -0
- data/lib/roda/plugins/rodauth.rb +3 -262
- data/lib/rodauth.rb +260 -0
- data/lib/rodauth/features/account_expiration.rb +108 -0
- data/lib/rodauth/features/base.rb +479 -0
- data/lib/rodauth/features/change_login.rb +77 -0
- data/lib/rodauth/features/change_password.rb +66 -0
- data/lib/rodauth/features/close_account.rb +82 -0
- data/lib/rodauth/features/confirm_password.rb +51 -0
- data/lib/rodauth/features/create_account.rb +128 -0
- data/lib/rodauth/features/disallow_password_reuse.rb +82 -0
- data/lib/rodauth/features/email_base.rb +63 -0
- data/lib/rodauth/features/jwt.rb +151 -0
- data/lib/rodauth/features/lockout.rb +262 -0
- data/lib/rodauth/features/login.rb +61 -0
- data/lib/rodauth/features/login_password_requirements_base.rb +123 -0
- data/lib/rodauth/features/logout.rb +37 -0
- data/lib/rodauth/features/otp.rb +338 -0
- data/lib/rodauth/features/password_complexity.rb +89 -0
- data/lib/rodauth/features/password_expiration.rb +111 -0
- data/lib/rodauth/features/password_grace_period.rb +46 -0
- data/lib/rodauth/features/recovery_codes.rb +240 -0
- data/lib/rodauth/features/remember.rb +200 -0
- data/lib/rodauth/features/reset_password.rb +207 -0
- data/lib/rodauth/features/session_expiration.rb +55 -0
- data/lib/rodauth/features/single_session.rb +87 -0
- data/lib/rodauth/features/sms_codes.rb +498 -0
- data/lib/rodauth/features/two_factor_base.rb +135 -0
- data/lib/rodauth/features/verify_account.rb +232 -0
- data/lib/rodauth/features/verify_account_grace_period.rb +76 -0
- data/lib/rodauth/features/verify_change_login.rb +20 -0
- data/lib/rodauth/migrations.rb +130 -0
- data/lib/rodauth/version.rb +9 -0
- data/spec/account_expiration_spec.rb +90 -0
- data/spec/all.rb +1 -0
- data/spec/change_login_spec.rb +149 -0
- data/spec/change_password_spec.rb +177 -0
- data/spec/close_account_spec.rb +162 -0
- data/spec/confirm_password_spec.rb +70 -0
- data/spec/create_account_spec.rb +127 -0
- data/spec/disallow_password_reuse_spec.rb +84 -0
- data/spec/lockout_spec.rb +228 -0
- data/spec/login_spec.rb +188 -0
- data/spec/migrate/001_tables.rb +103 -16
- data/spec/migrate/002_account_password_hash_column.rb +11 -0
- data/spec/migrate_password/001_tables.rb +60 -42
- data/spec/migrate_travis/001_tables.rb +116 -0
- data/spec/password_complexity_spec.rb +108 -0
- data/spec/password_expiration_spec.rb +243 -0
- data/spec/password_grace_period_spec.rb +93 -0
- data/spec/remember_spec.rb +424 -0
- data/spec/reset_password_spec.rb +185 -0
- data/spec/rodauth_spec.rb +57 -980
- data/spec/session_expiration_spec.rb +58 -0
- data/spec/single_session_spec.rb +107 -0
- data/spec/spec_helper.rb +202 -0
- data/spec/two_factor_spec.rb +1310 -0
- data/spec/verify_account_grace_period_spec.rb +135 -0
- data/spec/verify_account_spec.rb +142 -0
- data/spec/verify_change_login_spec.rb +46 -0
- data/spec/views/login.str +2 -2
- data/templates/add-recovery-codes.str +2 -0
- data/templates/button.str +5 -0
- data/templates/change-login.str +5 -18
- data/templates/change-password.str +6 -14
- data/templates/close-account.str +3 -6
- data/templates/confirm-password.str +4 -14
- data/templates/create-account.str +6 -30
- data/templates/login-confirm-field.str +6 -0
- data/templates/login-field.str +6 -0
- data/templates/login.str +5 -19
- data/templates/logout.str +2 -6
- data/templates/otp-auth-code-field.str +6 -0
- data/templates/otp-auth.str +8 -0
- data/templates/otp-disable.str +6 -0
- data/templates/otp-setup.str +21 -0
- data/templates/password-confirm-field.str +6 -0
- data/templates/password-field.str +6 -0
- data/templates/recovery-auth.str +12 -0
- data/templates/recovery-codes.str +6 -0
- data/templates/remember.str +8 -12
- data/templates/reset-password-request.str +2 -2
- data/templates/reset-password.str +4 -18
- data/templates/sms-auth.str +6 -0
- data/templates/sms-code-field.str +6 -0
- data/templates/sms-confirm.str +7 -0
- data/templates/sms-disable.str +7 -0
- data/templates/sms-request.str +5 -0
- data/templates/sms-setup.str +12 -0
- data/templates/unlock-account-request.str +3 -7
- data/templates/unlock-account.str +4 -7
- data/templates/verify-account-resend.str +2 -2
- data/templates/verify-account.str +2 -6
- metadata +191 -29
- data/lib/roda/plugins/rodauth/base.rb +0 -428
- data/lib/roda/plugins/rodauth/change_login.rb +0 -48
- data/lib/roda/plugins/rodauth/change_password.rb +0 -42
- data/lib/roda/plugins/rodauth/close_account.rb +0 -42
- data/lib/roda/plugins/rodauth/create_account.rb +0 -92
- data/lib/roda/plugins/rodauth/lockout.rb +0 -292
- data/lib/roda/plugins/rodauth/login.rb +0 -81
- data/lib/roda/plugins/rodauth/logout.rb +0 -36
- data/lib/roda/plugins/rodauth/remember.rb +0 -226
- data/lib/roda/plugins/rodauth/reset_password.rb +0 -205
- data/lib/roda/plugins/rodauth/verify_account.rb +0 -228
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Rodauth
|
|
4
|
+
ChangeLogin = Feature.define(:change_login) do
|
|
5
|
+
depends :login_password_requirements_base
|
|
6
|
+
|
|
7
|
+
notice_flash 'Your login has been changed'
|
|
8
|
+
error_flash 'There was an error changing your login'
|
|
9
|
+
view 'change-login', 'Change Login'
|
|
10
|
+
after
|
|
11
|
+
before
|
|
12
|
+
additional_form_tags
|
|
13
|
+
button 'Change Login'
|
|
14
|
+
redirect
|
|
15
|
+
|
|
16
|
+
auth_value_methods :change_login_requires_password?
|
|
17
|
+
|
|
18
|
+
auth_methods :change_login
|
|
19
|
+
|
|
20
|
+
route do |r|
|
|
21
|
+
require_account
|
|
22
|
+
before_change_login_route
|
|
23
|
+
|
|
24
|
+
r.get do
|
|
25
|
+
change_login_view
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
r.post do
|
|
29
|
+
catch_error do
|
|
30
|
+
if change_login_requires_password? && !password_match?(param(password_param))
|
|
31
|
+
throw_error(password_param, invalid_password_message)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
login = param(login_param)
|
|
35
|
+
unless login_meets_requirements?(login)
|
|
36
|
+
throw_error(login_param, login_does_not_meet_requirements_message)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
if require_login_confirmation? && login != param(login_confirm_param)
|
|
40
|
+
throw_error(login_param, logins_do_not_match_message)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
transaction do
|
|
44
|
+
before_change_login
|
|
45
|
+
unless change_login(login)
|
|
46
|
+
throw_error(login_param, login_does_not_meet_requirements_message)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
after_change_login
|
|
50
|
+
set_notice_flash change_login_notice_flash
|
|
51
|
+
redirect change_login_redirect
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
set_error_flash change_login_error_flash
|
|
56
|
+
change_login_view
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def change_login_requires_password?
|
|
61
|
+
modifications_require_password?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def change_login(login)
|
|
65
|
+
updated = nil
|
|
66
|
+
if account_ds.get(login_column).downcase == login.downcase
|
|
67
|
+
@login_requirement_message = 'same as current login'
|
|
68
|
+
return false
|
|
69
|
+
end
|
|
70
|
+
raised = raises_uniqueness_violation?{updated = update_account({login_column=>login}, account_ds.exclude(login_column=>login)) == 1}
|
|
71
|
+
if raised
|
|
72
|
+
@login_requirement_message = 'already an account with this login'
|
|
73
|
+
end
|
|
74
|
+
updated && !raised
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Rodauth
|
|
4
|
+
ChangePassword = Feature.define(:change_password) do
|
|
5
|
+
depends :login_password_requirements_base
|
|
6
|
+
|
|
7
|
+
notice_flash 'Your password has been changed'
|
|
8
|
+
error_flash 'There was an error changing your password'
|
|
9
|
+
view 'change-password', 'Change Password'
|
|
10
|
+
after
|
|
11
|
+
before
|
|
12
|
+
additional_form_tags
|
|
13
|
+
button 'Change Password'
|
|
14
|
+
redirect
|
|
15
|
+
|
|
16
|
+
auth_value_method :new_password_label, 'New Password'
|
|
17
|
+
auth_value_method :new_password_param, 'new-password'
|
|
18
|
+
|
|
19
|
+
auth_value_methods :change_password_requires_password?
|
|
20
|
+
|
|
21
|
+
route do |r|
|
|
22
|
+
require_account
|
|
23
|
+
before_change_password_route
|
|
24
|
+
|
|
25
|
+
r.get do
|
|
26
|
+
change_password_view
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
r.post do
|
|
30
|
+
catch_error do
|
|
31
|
+
if change_password_requires_password? && !password_match?(param(password_param))
|
|
32
|
+
throw_error(password_param, invalid_password_message)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
password = param(new_password_param)
|
|
36
|
+
if require_password_confirmation? && password != param(password_confirm_param)
|
|
37
|
+
throw_error(new_password_param, passwords_do_not_match_message)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if password_match?(password)
|
|
41
|
+
throw_error(new_password_param, same_as_existing_password_message)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
unless password_meets_requirements?(password)
|
|
45
|
+
throw_error(new_password_param, password_does_not_meet_requirements_message)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
transaction do
|
|
49
|
+
before_change_password
|
|
50
|
+
set_password(password)
|
|
51
|
+
after_change_password
|
|
52
|
+
end
|
|
53
|
+
set_notice_flash change_password_notice_flash
|
|
54
|
+
redirect change_password_redirect
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
set_error_flash change_password_error_flash
|
|
58
|
+
change_password_view
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def change_password_requires_password?
|
|
63
|
+
modifications_require_password?
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Rodauth
|
|
4
|
+
CloseAccount = Feature.define(:close_account) do
|
|
5
|
+
notice_flash 'Your account has been closed'
|
|
6
|
+
error_flash 'There was an error closing your account'
|
|
7
|
+
view 'close-account', 'Close Account'
|
|
8
|
+
additional_form_tags
|
|
9
|
+
button 'Close Account'
|
|
10
|
+
after
|
|
11
|
+
before
|
|
12
|
+
redirect
|
|
13
|
+
|
|
14
|
+
auth_value_method :account_closed_status_value, 3
|
|
15
|
+
|
|
16
|
+
auth_value_methods(
|
|
17
|
+
:close_account_requires_password?,
|
|
18
|
+
:delete_account_on_close?
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
auth_methods(
|
|
22
|
+
:close_account,
|
|
23
|
+
:delete_account
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
route do |r|
|
|
27
|
+
require_account
|
|
28
|
+
before_close_account_route
|
|
29
|
+
|
|
30
|
+
r.get do
|
|
31
|
+
close_account_view
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
r.post do
|
|
35
|
+
if !close_account_requires_password? || password_match?(param(password_param))
|
|
36
|
+
transaction do
|
|
37
|
+
before_close_account
|
|
38
|
+
close_account
|
|
39
|
+
after_close_account
|
|
40
|
+
if delete_account_on_close?
|
|
41
|
+
delete_account
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
clear_session
|
|
45
|
+
|
|
46
|
+
set_notice_flash close_account_notice_flash
|
|
47
|
+
redirect close_account_redirect
|
|
48
|
+
else
|
|
49
|
+
set_field_error(password_param, invalid_password_message)
|
|
50
|
+
set_error_flash close_account_error_flash
|
|
51
|
+
close_account_view
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def close_account_requires_password?
|
|
57
|
+
modifications_require_password?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def close_account
|
|
61
|
+
unless skip_status_checks?
|
|
62
|
+
update_account(account_status_column=>account_closed_status_value)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
unless account_password_hash_column
|
|
66
|
+
password_hash_ds.delete
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def delete_account
|
|
71
|
+
account_ds.delete
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def delete_account_on_close?
|
|
75
|
+
skip_status_checks?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def skip_status_checks?
|
|
79
|
+
false
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Rodauth
|
|
4
|
+
ConfirmPassword = Feature.define(:confirm_password) do
|
|
5
|
+
notice_flash "Your password has been confirmed"
|
|
6
|
+
error_flash "There was an error confirming your password"
|
|
7
|
+
view 'confirm-password', 'Confirm Password'
|
|
8
|
+
additional_form_tags
|
|
9
|
+
button 'Confirm Password'
|
|
10
|
+
before
|
|
11
|
+
after
|
|
12
|
+
|
|
13
|
+
auth_value_methods :confirm_password_redirect
|
|
14
|
+
|
|
15
|
+
auth_methods :confirm_password
|
|
16
|
+
|
|
17
|
+
route do
|
|
18
|
+
require_account
|
|
19
|
+
before_confirm_password_route
|
|
20
|
+
|
|
21
|
+
request.get do
|
|
22
|
+
confirm_password_view
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
request.post do
|
|
26
|
+
if password_match?(param(password_param))
|
|
27
|
+
transaction do
|
|
28
|
+
before_confirm_password
|
|
29
|
+
confirm_password
|
|
30
|
+
after_confirm_password
|
|
31
|
+
end
|
|
32
|
+
set_notice_flash confirm_password_notice_flash
|
|
33
|
+
redirect confirm_password_redirect
|
|
34
|
+
else
|
|
35
|
+
set_field_error(password_param, invalid_password_message)
|
|
36
|
+
set_error_flash confirm_password_error_flash
|
|
37
|
+
confirm_password_view
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def confirm_password
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def confirm_password_redirect
|
|
47
|
+
session.delete(:confirm_password_redirect) || default_redirect
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Rodauth
|
|
4
|
+
CreateAccount = Feature.define(:create_account) do
|
|
5
|
+
depends :login_password_requirements_base
|
|
6
|
+
|
|
7
|
+
depends :login
|
|
8
|
+
notice_flash 'Your account has been created'
|
|
9
|
+
error_flash "There was an error creating your account"
|
|
10
|
+
view 'create-account', 'Create Account'
|
|
11
|
+
after
|
|
12
|
+
before
|
|
13
|
+
button 'Create Account'
|
|
14
|
+
additional_form_tags
|
|
15
|
+
redirect
|
|
16
|
+
|
|
17
|
+
auth_value_method :create_account_autologin?, true
|
|
18
|
+
|
|
19
|
+
auth_value_methods :create_account_link
|
|
20
|
+
|
|
21
|
+
auth_methods(
|
|
22
|
+
:save_account,
|
|
23
|
+
:set_new_account_password
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
auth_private_methods(
|
|
27
|
+
:new_account
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
route do |r|
|
|
31
|
+
check_already_logged_in
|
|
32
|
+
before_create_account_route
|
|
33
|
+
|
|
34
|
+
r.get do
|
|
35
|
+
create_account_view
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
r.post do
|
|
39
|
+
login = param(login_param)
|
|
40
|
+
password = param(password_param)
|
|
41
|
+
new_account(login)
|
|
42
|
+
|
|
43
|
+
if account_password_hash_column
|
|
44
|
+
set_new_account_password(param(password_param))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
catch_error do
|
|
48
|
+
if require_login_confirmation? && login != param(login_confirm_param)
|
|
49
|
+
throw_error(login_param, logins_do_not_match_message)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
unless login_meets_requirements?(login)
|
|
53
|
+
throw_error(login_param, login_does_not_meet_requirements_message)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if require_password_confirmation? && password != param(password_confirm_param)
|
|
57
|
+
throw_error(password_param, passwords_do_not_match_message)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
unless password_meets_requirements?(password)
|
|
61
|
+
throw_error(password_param, password_does_not_meet_requirements_message)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
transaction do
|
|
65
|
+
before_create_account
|
|
66
|
+
unless save_account
|
|
67
|
+
throw_error(login_param, login_does_not_meet_requirements_message)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
unless account_password_hash_column
|
|
71
|
+
set_password(password)
|
|
72
|
+
end
|
|
73
|
+
after_create_account
|
|
74
|
+
if create_account_autologin?
|
|
75
|
+
update_session
|
|
76
|
+
end
|
|
77
|
+
set_notice_flash create_account_notice_flash
|
|
78
|
+
redirect create_account_redirect
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
set_error_flash create_account_error_flash
|
|
83
|
+
create_account_view
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def create_account_link
|
|
88
|
+
"<p><a href=\"#{prefix}/#{create_account_route}\">Create a New Account</a></p>"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def login_form_footer
|
|
92
|
+
super + create_account_link
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def set_new_account_password(password)
|
|
96
|
+
account[account_password_hash_column] = password_hash(password)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def new_account(login)
|
|
100
|
+
@account = _new_account(login)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def save_account
|
|
104
|
+
id = nil
|
|
105
|
+
raised = raises_uniqueness_violation?{id = db[accounts_table].insert(account)}
|
|
106
|
+
|
|
107
|
+
if raised
|
|
108
|
+
@login_requirement_message = 'already an account with this login'
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
if id
|
|
112
|
+
account[account_id_column] = id
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
id && !raised
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def _new_account(login)
|
|
121
|
+
acc = {login_column=>login}
|
|
122
|
+
unless skip_status_checks?
|
|
123
|
+
acc[account_status_column] = account_initial_status_value
|
|
124
|
+
end
|
|
125
|
+
acc
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Rodauth
|
|
4
|
+
DisallowPasswordReuse = Feature.define(:disallow_password_reuse) do
|
|
5
|
+
depends :login_password_requirements_base
|
|
6
|
+
|
|
7
|
+
auth_value_method :password_same_as_previous_password_message, "same as previous password"
|
|
8
|
+
auth_value_method :previous_password_account_id_column, :account_id
|
|
9
|
+
auth_value_method :previous_password_hash_column, :password_hash
|
|
10
|
+
auth_value_method :previous_password_hash_table, :account_previous_password_hashes
|
|
11
|
+
auth_value_method :previous_password_id_column, :id
|
|
12
|
+
auth_value_method :previous_passwords_to_check, 6
|
|
13
|
+
|
|
14
|
+
auth_methods(
|
|
15
|
+
:add_previous_password_hash,
|
|
16
|
+
:password_doesnt_match_previous_password?
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def set_password(password)
|
|
20
|
+
hash = super
|
|
21
|
+
add_previous_password_hash(hash)
|
|
22
|
+
hash
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def add_previous_password_hash(hash)
|
|
26
|
+
ds = previous_password_ds
|
|
27
|
+
keep_before = ds.reverse(previous_password_id_column).
|
|
28
|
+
limit(nil, previous_passwords_to_check).
|
|
29
|
+
get(previous_password_id_column)
|
|
30
|
+
|
|
31
|
+
ds.where(Sequel.expr(previous_password_id_column) <= keep_before).
|
|
32
|
+
delete
|
|
33
|
+
|
|
34
|
+
# This should never raise uniqueness violations, as it uses a serial primary key
|
|
35
|
+
ds.insert(previous_password_account_id_column=>account_id, previous_password_hash_column=>hash)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def password_meets_requirements?(password)
|
|
39
|
+
super &&
|
|
40
|
+
password_doesnt_match_previous_password?(password)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def password_doesnt_match_previous_password?(password)
|
|
46
|
+
id = account_id
|
|
47
|
+
match = if use_database_authentication_functions?
|
|
48
|
+
salts = previous_password_ds.
|
|
49
|
+
select_map([previous_password_id_column, Sequel.function(function_name(:rodauth_get_previous_salt), previous_password_id_column).as(:salt)])
|
|
50
|
+
return true if salts.empty?
|
|
51
|
+
|
|
52
|
+
salts.any? do |hash_id, salt|
|
|
53
|
+
db.get(Sequel.function(function_name(:rodauth_previous_password_hash_match), hash_id, BCrypt::Engine.hash_secret(password, salt)))
|
|
54
|
+
end
|
|
55
|
+
else
|
|
56
|
+
# :nocov:
|
|
57
|
+
previous_password_ds.select_map(previous_password_hash_column).any?{|hash| BCrypt::Password.new(hash) == password}
|
|
58
|
+
# :nocov:
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
return true unless match
|
|
62
|
+
@password_requirement_message = password_same_as_previous_password_message
|
|
63
|
+
false
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def after_close_account
|
|
67
|
+
super if defined?(super)
|
|
68
|
+
previous_password_ds.delete
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def after_create_account
|
|
72
|
+
if account_password_hash_column
|
|
73
|
+
add_previous_password_hash(password_hash(request[password_param]))
|
|
74
|
+
end
|
|
75
|
+
super if defined?(super)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def previous_password_ds
|
|
79
|
+
db[previous_password_hash_table].where(previous_password_account_id_column=>account_id)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|