rodauth 1.22.0 → 2.3.0

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