rodauth 1.22.0 → 2.3.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.
Files changed (198) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +190 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +210 -80
  5. data/doc/account_expiration.rdoc +12 -26
  6. data/doc/active_sessions.rdoc +49 -0
  7. data/doc/audit_logging.rdoc +44 -0
  8. data/doc/base.rdoc +75 -128
  9. data/doc/change_login.rdoc +7 -14
  10. data/doc/change_password.rdoc +9 -13
  11. data/doc/change_password_notify.rdoc +2 -2
  12. data/doc/close_account.rdoc +9 -16
  13. data/doc/confirm_password.rdoc +12 -5
  14. data/doc/create_account.rdoc +11 -22
  15. data/doc/disallow_password_reuse.rdoc +6 -13
  16. data/doc/email_auth.rdoc +15 -14
  17. data/doc/email_base.rdoc +6 -15
  18. data/doc/guides/admin_activation.rdoc +46 -0
  19. data/doc/guides/already_authenticated.rdoc +10 -0
  20. data/doc/guides/alternative_login.rdoc +46 -0
  21. data/doc/guides/create_account_programmatically.rdoc +38 -0
  22. data/doc/guides/delay_password.rdoc +25 -0
  23. data/doc/guides/email_only.rdoc +16 -0
  24. data/doc/guides/i18n.rdoc +26 -0
  25. data/doc/{internals.rdoc → guides/internals.rdoc} +0 -0
  26. data/doc/guides/links.rdoc +12 -0
  27. data/doc/guides/login_return.rdoc +37 -0
  28. data/doc/guides/password_column.rdoc +25 -0
  29. data/doc/guides/password_confirmation.rdoc +37 -0
  30. data/doc/guides/password_requirements.rdoc +30 -0
  31. data/doc/guides/paths.rdoc +36 -0
  32. data/doc/guides/query_params.rdoc +9 -0
  33. data/doc/guides/redirects.rdoc +17 -0
  34. data/doc/guides/registration_field.rdoc +68 -0
  35. data/doc/guides/require_mfa.rdoc +30 -0
  36. data/doc/guides/reset_password_autologin.rdoc +21 -0
  37. data/doc/guides/status_column.rdoc +28 -0
  38. data/doc/guides/totp_or_recovery.rdoc +16 -0
  39. data/doc/http_basic_auth.rdoc +10 -1
  40. data/doc/jwt.rdoc +22 -22
  41. data/doc/jwt_cors.rdoc +2 -3
  42. data/doc/jwt_refresh.rdoc +23 -8
  43. data/doc/lockout.rdoc +17 -15
  44. data/doc/login.rdoc +17 -2
  45. data/doc/login_password_requirements_base.rdoc +18 -37
  46. data/doc/logout.rdoc +2 -2
  47. data/doc/otp.rdoc +25 -19
  48. data/doc/password_complexity.rdoc +10 -26
  49. data/doc/password_expiration.rdoc +11 -25
  50. data/doc/password_grace_period.rdoc +16 -2
  51. data/doc/recovery_codes.rdoc +18 -12
  52. data/doc/release_notes/1.23.0.txt +32 -0
  53. data/doc/release_notes/2.0.0.txt +361 -0
  54. data/doc/release_notes/2.1.0.txt +31 -0
  55. data/doc/release_notes/2.2.0.txt +39 -0
  56. data/doc/release_notes/2.3.0.txt +37 -0
  57. data/doc/remember.rdoc +40 -64
  58. data/doc/reset_password.rdoc +12 -9
  59. data/doc/session_expiration.rdoc +1 -0
  60. data/doc/single_session.rdoc +16 -25
  61. data/doc/sms_codes.rdoc +24 -14
  62. data/doc/two_factor_base.rdoc +60 -22
  63. data/doc/verify_account.rdoc +14 -12
  64. data/doc/verify_account_grace_period.rdoc +6 -2
  65. data/doc/verify_login_change.rdoc +9 -8
  66. data/doc/webauthn.rdoc +115 -0
  67. data/doc/webauthn_login.rdoc +15 -0
  68. data/doc/webauthn_verify_account.rdoc +9 -0
  69. data/javascript/webauthn_auth.js +45 -0
  70. data/javascript/webauthn_setup.js +35 -0
  71. data/lib/roda/plugins/rodauth.rb +1 -1
  72. data/lib/rodauth.rb +36 -28
  73. data/lib/rodauth/features/account_expiration.rb +5 -5
  74. data/lib/rodauth/features/active_sessions.rb +158 -0
  75. data/lib/rodauth/features/audit_logging.rb +98 -0
  76. data/lib/rodauth/features/base.rb +144 -43
  77. data/lib/rodauth/features/change_password_notify.rb +2 -2
  78. data/lib/rodauth/features/close_account.rb +8 -6
  79. data/lib/rodauth/features/confirm_password.rb +40 -2
  80. data/lib/rodauth/features/create_account.rb +8 -13
  81. data/lib/rodauth/features/disallow_common_passwords.rb +1 -1
  82. data/lib/rodauth/features/disallow_password_reuse.rb +1 -1
  83. data/lib/rodauth/features/email_auth.rb +31 -30
  84. data/lib/rodauth/features/email_base.rb +9 -4
  85. data/lib/rodauth/features/http_basic_auth.rb +55 -35
  86. data/lib/rodauth/features/jwt.rb +63 -16
  87. data/lib/rodauth/features/jwt_cors.rb +15 -15
  88. data/lib/rodauth/features/jwt_refresh.rb +42 -13
  89. data/lib/rodauth/features/lockout.rb +12 -14
  90. data/lib/rodauth/features/login.rb +64 -15
  91. data/lib/rodauth/features/login_password_requirements_base.rb +13 -8
  92. data/lib/rodauth/features/otp.rb +77 -80
  93. data/lib/rodauth/features/password_complexity.rb +8 -13
  94. data/lib/rodauth/features/password_expiration.rb +2 -2
  95. data/lib/rodauth/features/password_grace_period.rb +17 -10
  96. data/lib/rodauth/features/recovery_codes.rb +49 -53
  97. data/lib/rodauth/features/remember.rb +11 -27
  98. data/lib/rodauth/features/reset_password.rb +26 -26
  99. data/lib/rodauth/features/session_expiration.rb +7 -10
  100. data/lib/rodauth/features/single_session.rb +8 -6
  101. data/lib/rodauth/features/sms_codes.rb +62 -72
  102. data/lib/rodauth/features/two_factor_base.rb +134 -30
  103. data/lib/rodauth/features/verify_account.rb +29 -21
  104. data/lib/rodauth/features/verify_account_grace_period.rb +18 -9
  105. data/lib/rodauth/features/verify_login_change.rb +12 -11
  106. data/lib/rodauth/features/webauthn.rb +505 -0
  107. data/lib/rodauth/features/webauthn_login.rb +70 -0
  108. data/lib/rodauth/features/webauthn_verify_account.rb +46 -0
  109. data/lib/rodauth/migrations.rb +16 -5
  110. data/lib/rodauth/version.rb +2 -2
  111. data/templates/button.str +1 -3
  112. data/templates/change-login.str +1 -2
  113. data/templates/change-password.str +3 -5
  114. data/templates/close-account.str +2 -2
  115. data/templates/confirm-password.str +1 -1
  116. data/templates/create-account.str +1 -1
  117. data/templates/email-auth-request-form.str +2 -3
  118. data/templates/email-auth.str +1 -1
  119. data/templates/global-logout-field.str +6 -0
  120. data/templates/login-confirm-field.str +2 -4
  121. data/templates/login-display.str +3 -2
  122. data/templates/login-field.str +2 -4
  123. data/templates/login-form-footer.str +6 -0
  124. data/templates/login-form.str +7 -0
  125. data/templates/login.str +1 -9
  126. data/templates/logout.str +1 -1
  127. data/templates/multi-phase-login.str +3 -0
  128. data/templates/otp-auth-code-field.str +5 -3
  129. data/templates/otp-auth.str +1 -1
  130. data/templates/otp-disable.str +1 -1
  131. data/templates/otp-setup.str +3 -3
  132. data/templates/password-confirm-field.str +2 -4
  133. data/templates/password-field.str +2 -4
  134. data/templates/recovery-auth.str +3 -6
  135. data/templates/recovery-codes.str +1 -1
  136. data/templates/remember.str +15 -20
  137. data/templates/reset-password-request.str +3 -3
  138. data/templates/reset-password.str +1 -2
  139. data/templates/sms-auth.str +1 -1
  140. data/templates/sms-code-field.str +5 -3
  141. data/templates/sms-confirm.str +1 -2
  142. data/templates/sms-disable.str +1 -2
  143. data/templates/sms-request.str +1 -1
  144. data/templates/sms-setup.str +6 -4
  145. data/templates/two-factor-auth.str +5 -0
  146. data/templates/two-factor-disable.str +6 -0
  147. data/templates/two-factor-manage.str +16 -0
  148. data/templates/unlock-account-request.str +4 -4
  149. data/templates/unlock-account.str +1 -1
  150. data/templates/verify-account-resend.str +3 -3
  151. data/templates/verify-account.str +1 -2
  152. data/templates/verify-login-change.str +1 -1
  153. data/templates/webauthn-auth.str +11 -0
  154. data/templates/webauthn-remove.str +14 -0
  155. data/templates/webauthn-setup.str +12 -0
  156. metadata +94 -54
  157. data/Rakefile +0 -179
  158. data/doc/verify_change_login.rdoc +0 -11
  159. data/lib/rodauth/features/verify_change_login.rb +0 -20
  160. data/spec/account_expiration_spec.rb +0 -225
  161. data/spec/all.rb +0 -1
  162. data/spec/change_login_spec.rb +0 -156
  163. data/spec/change_password_notify_spec.rb +0 -33
  164. data/spec/change_password_spec.rb +0 -202
  165. data/spec/close_account_spec.rb +0 -162
  166. data/spec/confirm_password_spec.rb +0 -70
  167. data/spec/create_account_spec.rb +0 -127
  168. data/spec/disallow_common_passwords_spec.rb +0 -93
  169. data/spec/disallow_password_reuse_spec.rb +0 -179
  170. data/spec/email_auth_spec.rb +0 -285
  171. data/spec/http_basic_auth_spec.rb +0 -143
  172. data/spec/jwt_cors_spec.rb +0 -57
  173. data/spec/jwt_refresh_spec.rb +0 -256
  174. data/spec/jwt_spec.rb +0 -235
  175. data/spec/lockout_spec.rb +0 -250
  176. data/spec/login_spec.rb +0 -328
  177. data/spec/migrate/001_tables.rb +0 -184
  178. data/spec/migrate/002_account_password_hash_column.rb +0 -11
  179. data/spec/migrate_password/001_tables.rb +0 -73
  180. data/spec/migrate_travis/001_tables.rb +0 -141
  181. data/spec/password_complexity_spec.rb +0 -109
  182. data/spec/password_expiration_spec.rb +0 -244
  183. data/spec/password_grace_period_spec.rb +0 -93
  184. data/spec/remember_spec.rb +0 -451
  185. data/spec/reset_password_spec.rb +0 -229
  186. data/spec/rodauth_spec.rb +0 -343
  187. data/spec/session_expiration_spec.rb +0 -58
  188. data/spec/single_session_spec.rb +0 -127
  189. data/spec/spec_helper.rb +0 -327
  190. data/spec/two_factor_spec.rb +0 -1462
  191. data/spec/update_password_hash_spec.rb +0 -40
  192. data/spec/verify_account_grace_period_spec.rb +0 -171
  193. data/spec/verify_account_spec.rb +0 -240
  194. data/spec/verify_change_login_spec.rb +0 -46
  195. data/spec/verify_login_change_spec.rb +0 -232
  196. data/spec/views/layout-other.str +0 -11
  197. data/spec/views/layout.str +0 -11
  198. data/spec/views/login.str +0 -21
@@ -4,25 +4,25 @@ require 'jwt'
4
4
 
5
5
  module Rodauth
6
6
  Feature.define(:jwt, :Jwt) do
7
- auth_value_method :invalid_jwt_format_error_message, "invalid JWT format or claim in Authorization header"
8
- auth_value_method :json_non_post_error_message, 'non-POST method used in JSON API'
9
- auth_value_method :json_not_accepted_error_message, 'Unsupported Accept header. Must accept "application/json" or compatible content type'
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?, false
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, nil
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?, false
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
- auth_value_method :non_json_request_error_message, 'Only JSON format requests are allowed'
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(request.send(:convert_to_json, json_response))
53
+ response.write(_json_response_body(json_response))
54
54
  request.halt
55
55
  end
56
56
 
@@ -138,15 +138,25 @@ module Rodauth
138
138
  !!(jwt_token && jwt_payload)
139
139
  end
140
140
 
141
+ def view(page, title)
142
+ return super unless use_jwt?
143
+ return_json_response
144
+ end
145
+
141
146
  private
142
147
 
148
+ def check_csrf?
149
+ return false if use_jwt?
150
+ super
151
+ end
152
+
143
153
  def before_rodauth
144
154
  if json_request?
145
155
  if jwt_check_accept? && (accept = request.env['HTTP_ACCEPT']) && accept !~ json_accept_regexp
146
156
  response.status = 406
147
157
  json_response[json_response_error_key] = json_not_accepted_error_message
148
158
  response['Content-Type'] ||= json_response_content_type
149
- response.write(request.send(:convert_to_json, json_response))
159
+ response.write(_json_response_body(json_response))
150
160
  request.halt
151
161
  end
152
162
 
@@ -173,6 +183,43 @@ module Rodauth
173
183
  end
174
184
  end
175
185
 
186
+ def before_webauthn_setup_route
187
+ super if defined?(super)
188
+ if use_jwt? && !param_or_nil(webauthn_setup_param)
189
+ cred = new_webauthn_credential
190
+ json_response[webauthn_setup_param] = cred.as_json
191
+ json_response[webauthn_setup_challenge_param] = cred.challenge
192
+ json_response[webauthn_setup_challenge_hmac_param] = compute_hmac(cred.challenge)
193
+ end
194
+ end
195
+
196
+ def before_webauthn_auth_route
197
+ super if defined?(super)
198
+ if use_jwt? && !param_or_nil(webauthn_auth_param)
199
+ cred = webauth_credential_options_for_get
200
+ json_response[webauthn_auth_param] = cred.as_json
201
+ json_response[webauthn_auth_challenge_param] = cred.challenge
202
+ json_response[webauthn_auth_challenge_hmac_param] = compute_hmac(cred.challenge)
203
+ end
204
+ end
205
+
206
+ def before_webauthn_login_route
207
+ super if defined?(super)
208
+ if use_jwt? && !param_or_nil(webauthn_auth_param) && account_from_login(param(login_param))
209
+ cred = webauth_credential_options_for_get
210
+ json_response[webauthn_auth_param] = cred.as_json
211
+ json_response[webauthn_auth_challenge_param] = cred.challenge
212
+ json_response[webauthn_auth_challenge_hmac_param] = compute_hmac(cred.challenge)
213
+ end
214
+ end
215
+
216
+ def before_webauthn_remove_route
217
+ super if defined?(super)
218
+ if use_jwt? && !param_or_nil(webauthn_remove_param)
219
+ json_response[webauthn_remove_param] = account_webauthn_usage
220
+ end
221
+ end
222
+
176
223
  def before_otp_setup_route
177
224
  super if defined?(super)
178
225
  if use_jwt? && otp_keys_use_hmac? && !param_or_nil(otp_setup_raw_param)
@@ -204,14 +251,14 @@ module Rodauth
204
251
  value
205
252
  end
206
253
 
207
- def json_response
208
- @json_response ||= {}
254
+ def remove_session_value(key)
255
+ value = super
256
+ set_jwt if use_jwt?
257
+ value
209
258
  end
210
259
 
211
- def _view(meth, page)
212
- return super unless use_jwt?
213
- return super if meth == :render
214
- return_json_response
260
+ def json_response
261
+ @json_response ||= {}
215
262
  end
216
263
 
217
264
  def _json_response_body(hash)
@@ -13,27 +13,27 @@ module Rodauth
13
13
  auth_methods(:jwt_cors_allow?)
14
14
 
15
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
16
+ return false unless origin = request.env['HTTP_ORIGIN']
17
+
18
+ case allowed = jwt_cors_allow_origin
19
+ when String
20
+ timing_safe_eql?(origin, allowed)
21
+ when Array
22
+ allowed.any?{|s| timing_safe_eql?(origin, s)}
23
+ when Regexp
24
+ allowed =~ origin
25
+ when true
26
+ true
27
+ else
28
+ false
29
29
  end
30
30
  end
31
31
 
32
32
  private
33
33
 
34
34
  def before_rodauth
35
- if (origin = request.env['HTTP_ORIGIN']) && jwt_cors_allow?
36
- response['Access-Control-Allow-Origin'] = origin
35
+ if jwt_cors_allow?
36
+ response['Access-Control-Allow-Origin'] = request.env['HTTP_ORIGIN']
37
37
 
38
38
  # Handle CORS preflight request
39
39
  if request.request_method == 'OPTIONS'
@@ -1,7 +1,7 @@
1
1
  # frozen-string-literal: true
2
2
 
3
3
  module Rodauth
4
- JwtRefresh = Feature.define(:jwt_refresh) do
4
+ Feature.define(:jwt_refresh, :JwtRefresh) do
5
5
  depends :jwt
6
6
 
7
7
  after 'refresh_token'
@@ -10,32 +10,38 @@ 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
- auth_value_method :jwt_refresh_invalid_token_message, 'invalid JWT refresh token'
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
20
20
  auth_value_method :jwt_refresh_token_key_param, 'refresh_token'
21
21
  auth_value_method :jwt_refresh_token_table, :account_jwt_refresh_keys
22
+ translatable_method :jwt_refresh_without_access_token_message, 'no JWT access token provided during refresh'
23
+ auth_value_method :jwt_refresh_without_access_token_status, 401
22
24
 
23
25
  auth_private_methods(
24
26
  :account_from_refresh_token
25
27
  )
26
28
 
27
29
  route do |r|
30
+ before_jwt_refresh_route
31
+
28
32
  r.post do
29
- if (refresh_token = param_or_nil(jwt_refresh_token_key_param)) && account_from_refresh_token(refresh_token)
30
- formatted_token = nil
33
+ if !session_value
34
+ response.status ||= jwt_refresh_without_access_token_status
35
+ json_response[json_response_error_key] = jwt_refresh_without_access_token_message
36
+ elsif (refresh_token = param_or_nil(jwt_refresh_token_key_param)) && account_from_refresh_token(refresh_token)
31
37
  transaction do
32
38
  before_refresh_token
33
39
  formatted_token = generate_refresh_token
34
40
  remove_jwt_refresh_token_key(refresh_token)
41
+ json_response[jwt_refresh_token_key] = formatted_token
42
+ json_response[jwt_access_token_key] = session_jwt
35
43
  after_refresh_token
36
44
  end
37
- json_response[jwt_refresh_token_key] = formatted_token
38
- json_response[jwt_access_token_key] = session_jwt
39
45
  else
40
46
  json_response[json_response_error_key] = jwt_refresh_invalid_token_message
41
47
  response.status ||= json_response_error_status
@@ -80,14 +86,10 @@ module Rodauth
80
86
  private
81
87
 
82
88
  def _account_from_refresh_token(token)
83
- id, token = split_token(token)
84
- return unless id && token
85
-
86
- token_id, key = split_token(token)
87
- return unless token_id && key
89
+ id, token_id, key = _account_refresh_token_split(token)
88
90
 
91
+ return unless key
89
92
  return unless actual = get_active_refresh_token(id, token_id)
90
-
91
93
  return unless timing_safe_eql?(key, convert_token_key(actual))
92
94
 
93
95
  ds = account_ds(id)
@@ -95,6 +97,16 @@ module Rodauth
95
97
  ds.first
96
98
  end
97
99
 
100
+ def _account_refresh_token_split(token)
101
+ id, token = split_token(token)
102
+ return unless id && token
103
+
104
+ token_id, key = split_token(token)
105
+ return unless token_id && key
106
+
107
+ [id, token_id, key]
108
+ end
109
+
98
110
  def get_active_refresh_token(account_id, token_id)
99
111
  jwt_refresh_token_account_ds(account_id).
100
112
  where(Sequel::CURRENT_TIMESTAMP > jwt_refresh_token_deadline_column).
@@ -134,6 +146,23 @@ module Rodauth
134
146
  hash
135
147
  end
136
148
 
149
+ def before_logout
150
+ if token = param_or_nil(jwt_refresh_token_key_param)
151
+ if token == 'all'
152
+ jwt_refresh_token_account_ds(session_value).delete
153
+ else
154
+ id, token_id, key = _account_refresh_token_split(token)
155
+
156
+ if id && token_id && key && (actual = get_active_refresh_token(session_value, token_id)) && timing_safe_eql?(key, convert_token_key(actual))
157
+ jwt_refresh_token_account_ds(id).
158
+ where(jwt_refresh_token_id_column=>token_id).
159
+ delete
160
+ end
161
+ end
162
+ end
163
+ super if defined?(super)
164
+ end
165
+
137
166
  def after_close_account
138
167
  jwt_refresh_token_account_ds(account_id).delete
139
168
  super if defined?(super)
@@ -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.", "login_lockout"
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, nil
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
- auth_value_method :unlock_account_email_subject, 'Unlock Account'
43
- auth_value_method :unlock_account_explanatory_text, '<p>This account is currently locked out. You can unlock the account:</p>'
44
- auth_value_method :unlock_account_request_explanatory_text, '<p>This account is currently locked out. You can request that the account be unlocked:</p>'
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
- session[unlock_account_session_key] = key
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
- session[unlock_account_session_key] = nil
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
- update_session
129
+ autologin_session('unlock_account')
131
130
  end
132
131
  end
133
132
 
134
- session[unlock_account_session_key] = nil
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
- @unlock_account_key_value = get_unlock_account_key
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
- auth_value_method :login_form_footer, ''
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,84 @@ 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
- login_view
69
+ send(view)
61
70
  end
62
71
  end
63
72
 
64
73
  attr_reader :login_form_header
65
74
 
75
+ def login(auth_type)
76
+ saved_login_redirect = remove_session_value(login_redirect_session_key)
77
+ transaction do
78
+ before_login
79
+ login_session(auth_type)
80
+ yield if block_given?
81
+ after_login
82
+ end
83
+ set_notice_flash login_notice_flash
84
+ redirect(saved_login_redirect || login_redirect)
85
+ end
86
+
87
+ def login_required
88
+ if login_return_to_requested_location?
89
+ set_session_value(login_redirect_session_key, request.fullpath)
90
+ end
91
+ super
92
+ end
93
+
66
94
  def after_login_entered_during_multi_phase_login
67
95
  set_notice_now_flash need_password_notice_flash
96
+ if multi_phase_login_forms.length == 1 && (meth = multi_phase_login_forms[0][2])
97
+ send(meth)
98
+ end
68
99
  end
69
100
 
70
101
  def skip_login_field_on_login?
71
102
  return false unless use_multi_phase_login?
72
- @valid_login_entered
103
+ valid_login_entered?
73
104
  end
74
105
 
75
106
  def skip_password_field_on_login?
76
107
  return false unless use_multi_phase_login?
77
- @valid_login_entered != true
108
+ !valid_login_entered?
109
+ end
110
+
111
+ def valid_login_entered?
112
+ @valid_login_entered
78
113
  end
79
114
 
80
115
  def login_hidden_field
81
116
  "<input type='hidden' name=\"#{login_param}\" value=\"#{scope.h param(login_param)}\" />"
82
117
  end
83
118
 
119
+ def render_multi_phase_login_forms
120
+ multi_phase_login_forms.sort.map{|_, form, _| form}.join("\n")
121
+ end
122
+
84
123
  private
85
124
 
86
- def _login
87
- transaction do
88
- before_login
89
- update_session
90
- after_login
91
- end
92
- set_notice_flash login_notice_flash
93
- redirect login_redirect
125
+ def _login_form_footer_links
126
+ []
127
+ end
128
+
129
+ def _multi_phase_login_forms
130
+ forms = []
131
+ forms << [10, render("login-form"), nil] if has_password?
132
+ forms
133
+ end
134
+
135
+ def _login_form_footer
136
+ return '' if _login_form_footer_links.empty?
137
+ render('login-form-footer')
138
+ end
139
+
140
+ def _login(auth_type)
141
+ warn("Deprecated #_login method called, use #login instead.")
142
+ login(auth_type)
94
143
  end
95
144
  end
96
145
  end