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,63 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Rodauth
|
|
4
|
+
EmailBase = Feature.define(:email_base) do
|
|
5
|
+
auth_value_method :email_subject_prefix, nil
|
|
6
|
+
auth_value_method :require_mail?, true
|
|
7
|
+
auth_value_method :token_separator, "_"
|
|
8
|
+
|
|
9
|
+
auth_value_methods(
|
|
10
|
+
:email_from
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
auth_methods(
|
|
14
|
+
:create_email,
|
|
15
|
+
:email_to
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
def post_configure
|
|
19
|
+
super
|
|
20
|
+
require 'mail' if require_mail?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def create_email(subject, body)
|
|
26
|
+
m = Mail.new
|
|
27
|
+
m.from = email_from
|
|
28
|
+
m.to = email_to
|
|
29
|
+
m.subject = "#{email_subject_prefix}#{subject}"
|
|
30
|
+
m.body = body
|
|
31
|
+
m
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def email_from
|
|
35
|
+
"webmaster@#{request.host}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def email_to
|
|
39
|
+
account[login_column]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def split_token(token)
|
|
43
|
+
token.split(token_separator, 2)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def token_link(route, param, key)
|
|
47
|
+
"#{request.base_url}#{prefix}/#{route}?#{param}=#{account_id}#{token_separator}#{key}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def account_from_key(token, status_id=nil)
|
|
51
|
+
id, key = split_token(token)
|
|
52
|
+
return unless id && key
|
|
53
|
+
|
|
54
|
+
return unless actual = yield(id)
|
|
55
|
+
|
|
56
|
+
return unless timing_safe_eql?(key, actual)
|
|
57
|
+
|
|
58
|
+
ds = account_ds(id)
|
|
59
|
+
ds = ds.where(account_status_column=>status_id) if status_id && !skip_status_checks?
|
|
60
|
+
ds.first
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
require 'jwt'
|
|
4
|
+
|
|
5
|
+
module Rodauth
|
|
6
|
+
Jwt = Feature.define(:jwt) do
|
|
7
|
+
auth_value_method :json_non_post_error_message, 'non-POST method used in JSON API'
|
|
8
|
+
auth_value_method :json_response_error_status, 400
|
|
9
|
+
auth_value_method :json_response_error_key, "error"
|
|
10
|
+
auth_value_method :json_response_field_error_key, "field-error"
|
|
11
|
+
auth_value_method :json_response_success_key, nil
|
|
12
|
+
auth_value_method :jwt_algorithm, "HS256"
|
|
13
|
+
auth_value_method :non_json_request_error_message, 'Only JSON format requests are allowed'
|
|
14
|
+
auth_value_method :only_json?, true
|
|
15
|
+
|
|
16
|
+
auth_value_methods :jwt_secret
|
|
17
|
+
|
|
18
|
+
auth_methods(
|
|
19
|
+
:json_request?,
|
|
20
|
+
:jwt_token,
|
|
21
|
+
:set_jwt_token
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def session
|
|
25
|
+
return super unless json_request?
|
|
26
|
+
return @session if defined?(@session)
|
|
27
|
+
@session = if token = jwt_token
|
|
28
|
+
s = {}
|
|
29
|
+
payload, header = JWT.decode(token, jwt_secret, true, :algorithm=>jwt_algorithm)
|
|
30
|
+
payload.each do |k,v|
|
|
31
|
+
s[k.to_sym] = v
|
|
32
|
+
end
|
|
33
|
+
s
|
|
34
|
+
else
|
|
35
|
+
{}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def clear_session
|
|
40
|
+
super
|
|
41
|
+
set_jwt if json_request?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def set_field_error(field, message)
|
|
45
|
+
return super unless json_request?
|
|
46
|
+
json_response[json_response_field_error_key] = [field, message]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def set_error_flash(message)
|
|
50
|
+
return super unless json_request?
|
|
51
|
+
json_response[json_response_error_key] = message
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def set_redirect_error_flash(message)
|
|
55
|
+
return super unless json_request?
|
|
56
|
+
json_response[json_response_error_key] = message
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def set_notice_flash(message)
|
|
60
|
+
return super unless json_request?
|
|
61
|
+
json_response[json_response_success_key] = message if include_success_messages?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def set_notice_now_flash(message)
|
|
65
|
+
return super unless json_request?
|
|
66
|
+
json_response[json_response_success_key] = message if include_success_messages?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def jwt_token
|
|
70
|
+
if v = request.env['HTTP_AUTHORIZATION']
|
|
71
|
+
v.sub(/\ABearer:?\s+/, '')
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def set_jwt_token(token)
|
|
76
|
+
response.headers['Authorization'] = token
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def before_rodauth
|
|
82
|
+
if only_json? && !json_request?
|
|
83
|
+
response.status = json_response_error_status
|
|
84
|
+
response.write non_json_request_error_message
|
|
85
|
+
request.halt
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if json_request? && !request.post?
|
|
89
|
+
response.status = 405
|
|
90
|
+
response.headers['Allow'] = 'POST'
|
|
91
|
+
json_response[json_response_error_key] = json_non_post_error_message
|
|
92
|
+
return_json_response
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
super
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def before_view_recovery_codes
|
|
99
|
+
super if defined?(super)
|
|
100
|
+
if json_request?
|
|
101
|
+
json_response[:codes] = recovery_codes
|
|
102
|
+
json_response[json_response_success_key] ||= "" if include_success_messages?
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def jwt_secret
|
|
107
|
+
raise ArgumentError, "jwt_secret not set"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def redirect(_)
|
|
111
|
+
return super unless json_request?
|
|
112
|
+
return_json_response
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def include_success_messages?
|
|
116
|
+
!json_response_success_key.nil?
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def set_session_value(key, value)
|
|
120
|
+
super
|
|
121
|
+
set_jwt if json_request?
|
|
122
|
+
value
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def json_response
|
|
126
|
+
@json_response ||= {}
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def _view(meth, page)
|
|
130
|
+
return super unless json_request?
|
|
131
|
+
return super if meth == :render
|
|
132
|
+
return_json_response
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def return_json_response
|
|
136
|
+
response.status ||= json_response_error_status if json_response[json_response_error_key]
|
|
137
|
+
set_jwt
|
|
138
|
+
response.write(request.send(:convert_to_json, json_response))
|
|
139
|
+
request.halt
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def set_jwt
|
|
143
|
+
set_jwt_token(JWT.encode(session, jwt_secret, jwt_algorithm))
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def json_request?
|
|
147
|
+
return @json_request if defined?(@json_request)
|
|
148
|
+
@json_request = request.content_type =~ /application\/json/
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Rodauth
|
|
4
|
+
Lockout = Feature.define(:lockout) do
|
|
5
|
+
depends :login, :email_base
|
|
6
|
+
|
|
7
|
+
view 'unlock-account-request', 'Request Account Unlock', 'unlock_account_request'
|
|
8
|
+
view 'unlock-account', 'Unlock Account', 'unlock_account'
|
|
9
|
+
before 'unlock_account'
|
|
10
|
+
before 'unlock_account_request'
|
|
11
|
+
after 'unlock_account'
|
|
12
|
+
after 'unlock_account_request'
|
|
13
|
+
additional_form_tags 'unlock_account'
|
|
14
|
+
additional_form_tags 'unlock_account_request'
|
|
15
|
+
button 'Unlock Account', 'unlock_account'
|
|
16
|
+
button 'Request Account Unlock', 'unlock_account_request'
|
|
17
|
+
error_flash "There was an error unlocking your account", 'unlock_account'
|
|
18
|
+
error_flash "This account is currently locked out and cannot be logged in to.", "login_lockout"
|
|
19
|
+
notice_flash "Your account has been unlocked", 'unlock_account'
|
|
20
|
+
notice_flash "An email has been sent to you with a link to unlock your account", 'unlock_account_request'
|
|
21
|
+
redirect :unlock_account
|
|
22
|
+
redirect :unlock_account_request
|
|
23
|
+
|
|
24
|
+
auth_value_method :unlock_account_autologin?, true
|
|
25
|
+
auth_value_method :max_invalid_logins, 100
|
|
26
|
+
auth_value_method :account_login_failures_table, :account_login_failures
|
|
27
|
+
auth_value_method :account_login_failures_id_column, :id
|
|
28
|
+
auth_value_method :account_login_failures_number_column, :number
|
|
29
|
+
auth_value_method :account_lockouts_table, :account_lockouts
|
|
30
|
+
auth_value_method :account_lockouts_id_column, :id
|
|
31
|
+
auth_value_method :account_lockouts_key_column, :key
|
|
32
|
+
auth_value_method :account_lockouts_deadline_column, :deadline
|
|
33
|
+
auth_value_method :account_lockouts_deadline_interval, {:days=>1}
|
|
34
|
+
auth_value_method :no_matching_unlock_account_key_message, 'No matching unlock account key'
|
|
35
|
+
auth_value_method :unlock_account_email_subject, 'Unlock Account'
|
|
36
|
+
auth_value_method :unlock_account_key_param, 'key'
|
|
37
|
+
auth_value_method :unlock_account_requires_password?, false
|
|
38
|
+
|
|
39
|
+
auth_value_methods(
|
|
40
|
+
:unlock_account_redirect,
|
|
41
|
+
:unlock_account_request_redirect
|
|
42
|
+
)
|
|
43
|
+
auth_methods(
|
|
44
|
+
:clear_invalid_login_attempts,
|
|
45
|
+
:create_unlock_account_email,
|
|
46
|
+
:generate_unlock_account_key,
|
|
47
|
+
:get_unlock_account_key,
|
|
48
|
+
:invalid_login_attempted,
|
|
49
|
+
:locked_out?,
|
|
50
|
+
:send_unlock_account_email,
|
|
51
|
+
:unlock_account_email_body,
|
|
52
|
+
:unlock_account_email_link,
|
|
53
|
+
:unlock_account,
|
|
54
|
+
:unlock_account_key
|
|
55
|
+
)
|
|
56
|
+
auth_private_methods :account_from_unlock_key
|
|
57
|
+
|
|
58
|
+
route(:unlock_account_request) do |r|
|
|
59
|
+
check_already_logged_in
|
|
60
|
+
before_unlock_account_request_route
|
|
61
|
+
|
|
62
|
+
r.post do
|
|
63
|
+
if account_from_login(param(login_param)) && get_unlock_account_key
|
|
64
|
+
transaction do
|
|
65
|
+
before_unlock_account_request
|
|
66
|
+
send_unlock_account_email
|
|
67
|
+
after_unlock_account_request
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
set_notice_flash unlock_account_request_notice_flash
|
|
71
|
+
else
|
|
72
|
+
set_redirect_error_flash no_matching_login_message
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
redirect unlock_account_request_redirect
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
route(:unlock_account) do |r|
|
|
80
|
+
check_already_logged_in
|
|
81
|
+
before_unlock_account_route
|
|
82
|
+
|
|
83
|
+
r.get do
|
|
84
|
+
if account_from_unlock_key(param(unlock_account_key_param))
|
|
85
|
+
unlock_account_view
|
|
86
|
+
else
|
|
87
|
+
set_redirect_error_flash no_matching_unlock_account_key_message
|
|
88
|
+
redirect require_login_redirect
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
r.post do
|
|
93
|
+
key = param(unlock_account_key_param)
|
|
94
|
+
unless account_from_unlock_key(key)
|
|
95
|
+
set_redirect_error_flash no_matching_unlock_account_key_message
|
|
96
|
+
redirect unlock_account_request_redirect
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
if !unlock_account_requires_password? || password_match?(param(password_param))
|
|
100
|
+
transaction do
|
|
101
|
+
before_unlock_account
|
|
102
|
+
unlock_account
|
|
103
|
+
after_unlock_account
|
|
104
|
+
if unlock_account_autologin?
|
|
105
|
+
update_session
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
set_notice_flash unlock_account_notice_flash
|
|
110
|
+
redirect unlock_account_redirect
|
|
111
|
+
else
|
|
112
|
+
set_field_error(password_param, invalid_password_message)
|
|
113
|
+
set_error_flash unlock_account_error_flash
|
|
114
|
+
unlock_account_view
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def locked_out?
|
|
120
|
+
if t = convert_timestamp(account_lockouts_ds.get(account_lockouts_deadline_column))
|
|
121
|
+
if Time.now < t
|
|
122
|
+
true
|
|
123
|
+
else
|
|
124
|
+
unlock_account
|
|
125
|
+
false
|
|
126
|
+
end
|
|
127
|
+
else
|
|
128
|
+
false
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def unlock_account
|
|
133
|
+
transaction do
|
|
134
|
+
remove_lockout_metadata
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def clear_invalid_login_attempts
|
|
139
|
+
unlock_account
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def invalid_login_attempted
|
|
143
|
+
ds = account_login_failures_ds.
|
|
144
|
+
where(account_login_failures_id_column=>account_id)
|
|
145
|
+
|
|
146
|
+
number = if db.database_type == :postgres
|
|
147
|
+
ds.returning(account_login_failures_number_column).
|
|
148
|
+
with_sql(:update_sql, account_login_failures_number_column=>Sequel.expr(account_login_failures_number_column)+1).
|
|
149
|
+
single_value
|
|
150
|
+
else
|
|
151
|
+
# :nocov:
|
|
152
|
+
if ds.update(account_login_failures_number_column=>Sequel.expr(account_login_failures_number_column)+1) > 0
|
|
153
|
+
ds.get(account_login_failures_number_column)
|
|
154
|
+
end
|
|
155
|
+
# :nocov:
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
unless number
|
|
159
|
+
# Ignoring the violation is safe here. It may allow slightly more than max_invalid_logins invalid logins before
|
|
160
|
+
# lockout, but allowing a few extra is OK if the race is lost.
|
|
161
|
+
ignore_uniqueness_violation{account_login_failures_ds.insert(account_login_failures_id_column=>account_id)}
|
|
162
|
+
number = 1
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
if number >= max_invalid_logins
|
|
166
|
+
@unlock_account_key_value = generate_unlock_account_key
|
|
167
|
+
hash = {account_lockouts_id_column=>account_id, account_lockouts_key_column=>unlock_account_key_value}
|
|
168
|
+
set_deadline_value(hash, account_lockouts_deadline_column, account_lockouts_deadline_interval)
|
|
169
|
+
|
|
170
|
+
if e = raised_uniqueness_violation{account_lockouts_ds.insert(hash)}
|
|
171
|
+
# If inserting into the lockout table raises a violation, we should just be able to pull the already inserted
|
|
172
|
+
# key out of it. If that doesn't return a valid key, we should reraise the error.
|
|
173
|
+
raise e unless @unlock_account_key_value = account_lockouts_ds.get(account_lockouts_key_column)
|
|
174
|
+
|
|
175
|
+
show_lockout_page
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def get_unlock_account_key
|
|
181
|
+
account_lockouts_ds.get(account_lockouts_key_column)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def account_from_unlock_key(key)
|
|
185
|
+
@account = _account_from_unlock_key(key)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def send_unlock_account_email
|
|
189
|
+
@unlock_account_key_value = get_unlock_account_key
|
|
190
|
+
create_unlock_account_email.deliver!
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def unlock_account_email_link
|
|
194
|
+
token_link(unlock_account_route, unlock_account_key_param, unlock_account_key_value)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
private
|
|
198
|
+
|
|
199
|
+
attr_reader :unlock_account_key_value
|
|
200
|
+
|
|
201
|
+
def before_login_attempt
|
|
202
|
+
if locked_out?
|
|
203
|
+
show_lockout_page
|
|
204
|
+
end
|
|
205
|
+
super
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def after_login
|
|
209
|
+
clear_invalid_login_attempts
|
|
210
|
+
super
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def after_login_failure
|
|
214
|
+
invalid_login_attempted
|
|
215
|
+
super
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def after_close_account
|
|
219
|
+
remove_lockout_metadata
|
|
220
|
+
super if defined?(super)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def generate_unlock_account_key
|
|
224
|
+
random_key
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def remove_lockout_metadata
|
|
228
|
+
account_login_failures_ds.delete
|
|
229
|
+
account_lockouts_ds.delete
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def show_lockout_page
|
|
233
|
+
set_error_flash login_lockout_error_flash
|
|
234
|
+
response.write unlock_account_request_view
|
|
235
|
+
request.halt
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def create_unlock_account_email
|
|
239
|
+
create_email(unlock_account_email_subject, unlock_account_email_body)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def unlock_account_email_body
|
|
243
|
+
render('unlock-account-email')
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def use_date_arithmetic?
|
|
247
|
+
db.database_type == :mysql
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def account_login_failures_ds
|
|
251
|
+
db[account_login_failures_table].where(account_login_failures_id_column=>account_id)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def account_lockouts_ds(id=account_id)
|
|
255
|
+
db[account_lockouts_table].where(account_lockouts_id_column=>id)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def _account_from_unlock_key(token)
|
|
259
|
+
account_from_key(token){|id| account_lockouts_ds(id).get(account_lockouts_key_column)}
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|