rodauth 1.20.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (180) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +170 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +211 -79
  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/http_basic_auth.rdoc +10 -1
  19. data/doc/internals.rdoc +1 -1
  20. data/doc/jwt.rdoc +22 -22
  21. data/doc/jwt_cors.rdoc +22 -0
  22. data/doc/jwt_refresh.rdoc +12 -8
  23. data/doc/lockout.rdoc +17 -15
  24. data/doc/login.rdoc +10 -2
  25. data/doc/login_password_requirements_base.rdoc +15 -37
  26. data/doc/logout.rdoc +2 -2
  27. data/doc/otp.rdoc +24 -19
  28. data/doc/password_complexity.rdoc +10 -26
  29. data/doc/password_expiration.rdoc +11 -25
  30. data/doc/password_grace_period.rdoc +16 -2
  31. data/doc/recovery_codes.rdoc +18 -12
  32. data/doc/release_notes/1.21.0.txt +12 -0
  33. data/doc/release_notes/1.22.0.txt +11 -0
  34. data/doc/release_notes/1.23.0.txt +32 -0
  35. data/doc/release_notes/2.0.0.txt +361 -0
  36. data/doc/release_notes/2.1.0.txt +31 -0
  37. data/doc/remember.rdoc +40 -64
  38. data/doc/reset_password.rdoc +12 -9
  39. data/doc/session_expiration.rdoc +1 -0
  40. data/doc/single_session.rdoc +16 -25
  41. data/doc/sms_codes.rdoc +24 -14
  42. data/doc/two_factor_base.rdoc +60 -22
  43. data/doc/verify_account.rdoc +14 -12
  44. data/doc/verify_account_grace_period.rdoc +6 -2
  45. data/doc/verify_login_change.rdoc +9 -8
  46. data/doc/webauthn.rdoc +115 -0
  47. data/doc/webauthn_login.rdoc +15 -0
  48. data/doc/webauthn_verify_account.rdoc +9 -0
  49. data/javascript/webauthn_auth.js +45 -0
  50. data/javascript/webauthn_setup.js +35 -0
  51. data/lib/roda/plugins/rodauth.rb +1 -1
  52. data/lib/rodauth.rb +32 -24
  53. data/lib/rodauth/features/account_expiration.rb +5 -5
  54. data/lib/rodauth/features/active_sessions.rb +160 -0
  55. data/lib/rodauth/features/audit_logging.rb +96 -0
  56. data/lib/rodauth/features/base.rb +144 -43
  57. data/lib/rodauth/features/change_password_notify.rb +2 -2
  58. data/lib/rodauth/features/confirm_password.rb +40 -2
  59. data/lib/rodauth/features/create_account.rb +8 -13
  60. data/lib/rodauth/features/disallow_common_passwords.rb +1 -1
  61. data/lib/rodauth/features/disallow_password_reuse.rb +1 -1
  62. data/lib/rodauth/features/email_auth.rb +30 -29
  63. data/lib/rodauth/features/email_base.rb +9 -4
  64. data/lib/rodauth/features/http_basic_auth.rb +55 -35
  65. data/lib/rodauth/features/jwt.rb +58 -10
  66. data/lib/rodauth/features/jwt_cors.rb +53 -0
  67. data/lib/rodauth/features/jwt_refresh.rb +3 -3
  68. data/lib/rodauth/features/lockout.rb +12 -14
  69. data/lib/rodauth/features/login.rb +54 -10
  70. data/lib/rodauth/features/login_password_requirements_base.rb +4 -4
  71. data/lib/rodauth/features/otp.rb +72 -74
  72. data/lib/rodauth/features/password_complexity.rb +4 -11
  73. data/lib/rodauth/features/password_expiration.rb +2 -2
  74. data/lib/rodauth/features/password_grace_period.rb +17 -10
  75. data/lib/rodauth/features/recovery_codes.rb +49 -53
  76. data/lib/rodauth/features/remember.rb +11 -27
  77. data/lib/rodauth/features/reset_password.rb +26 -26
  78. data/lib/rodauth/features/session_expiration.rb +6 -4
  79. data/lib/rodauth/features/single_session.rb +7 -5
  80. data/lib/rodauth/features/sms_codes.rb +62 -71
  81. data/lib/rodauth/features/two_factor_base.rb +132 -28
  82. data/lib/rodauth/features/verify_account.rb +25 -21
  83. data/lib/rodauth/features/verify_account_grace_period.rb +20 -9
  84. data/lib/rodauth/features/verify_login_change.rb +12 -11
  85. data/lib/rodauth/features/webauthn.rb +507 -0
  86. data/lib/rodauth/features/webauthn_login.rb +70 -0
  87. data/lib/rodauth/features/webauthn_verify_account.rb +46 -0
  88. data/lib/rodauth/version.rb +2 -2
  89. data/templates/button.str +1 -3
  90. data/templates/change-login.str +1 -2
  91. data/templates/change-password.str +3 -5
  92. data/templates/close-account.str +2 -2
  93. data/templates/confirm-password.str +1 -1
  94. data/templates/create-account.str +1 -1
  95. data/templates/email-auth-email.str +1 -1
  96. data/templates/email-auth-request-form.str +2 -3
  97. data/templates/email-auth.str +1 -1
  98. data/templates/global-logout-field.str +6 -0
  99. data/templates/login-confirm-field.str +2 -4
  100. data/templates/login-display.str +3 -2
  101. data/templates/login-field.str +2 -4
  102. data/templates/login-form-footer.str +6 -0
  103. data/templates/login-form.str +7 -0
  104. data/templates/login.str +1 -9
  105. data/templates/logout.str +1 -1
  106. data/templates/multi-phase-login.str +3 -0
  107. data/templates/otp-auth-code-field.str +5 -3
  108. data/templates/otp-auth.str +1 -1
  109. data/templates/otp-disable.str +1 -1
  110. data/templates/otp-setup.str +3 -3
  111. data/templates/password-confirm-field.str +2 -4
  112. data/templates/password-field.str +2 -4
  113. data/templates/recovery-auth.str +3 -6
  114. data/templates/recovery-codes.str +1 -1
  115. data/templates/remember.str +15 -20
  116. data/templates/reset-password-email.str +1 -1
  117. data/templates/reset-password-request.str +3 -3
  118. data/templates/reset-password.str +1 -2
  119. data/templates/sms-auth.str +1 -1
  120. data/templates/sms-code-field.str +5 -3
  121. data/templates/sms-confirm.str +1 -2
  122. data/templates/sms-disable.str +1 -2
  123. data/templates/sms-request.str +1 -1
  124. data/templates/sms-setup.str +6 -4
  125. data/templates/two-factor-auth.str +5 -0
  126. data/templates/two-factor-disable.str +6 -0
  127. data/templates/two-factor-manage.str +16 -0
  128. data/templates/unlock-account-email.str +1 -1
  129. data/templates/unlock-account-request.str +4 -4
  130. data/templates/unlock-account.str +1 -1
  131. data/templates/verify-account-email.str +1 -1
  132. data/templates/verify-account-resend.str +3 -3
  133. data/templates/verify-account.str +1 -2
  134. data/templates/verify-login-change-email.str +2 -1
  135. data/templates/verify-login-change.str +1 -1
  136. data/templates/webauthn-auth.str +11 -0
  137. data/templates/webauthn-remove.str +14 -0
  138. data/templates/webauthn-setup.str +12 -0
  139. metadata +89 -50
  140. data/Rakefile +0 -179
  141. data/doc/verify_change_login.rdoc +0 -11
  142. data/lib/rodauth/features/verify_change_login.rb +0 -20
  143. data/spec/account_expiration_spec.rb +0 -225
  144. data/spec/all.rb +0 -1
  145. data/spec/change_login_spec.rb +0 -156
  146. data/spec/change_password_notify_spec.rb +0 -33
  147. data/spec/change_password_spec.rb +0 -202
  148. data/spec/close_account_spec.rb +0 -162
  149. data/spec/confirm_password_spec.rb +0 -70
  150. data/spec/create_account_spec.rb +0 -127
  151. data/spec/disallow_common_passwords_spec.rb +0 -93
  152. data/spec/disallow_password_reuse_spec.rb +0 -179
  153. data/spec/email_auth_spec.rb +0 -285
  154. data/spec/http_basic_auth_spec.rb +0 -143
  155. data/spec/jwt_refresh_spec.rb +0 -256
  156. data/spec/jwt_spec.rb +0 -235
  157. data/spec/lockout_spec.rb +0 -250
  158. data/spec/login_spec.rb +0 -328
  159. data/spec/migrate/001_tables.rb +0 -184
  160. data/spec/migrate/002_account_password_hash_column.rb +0 -11
  161. data/spec/migrate_password/001_tables.rb +0 -73
  162. data/spec/migrate_travis/001_tables.rb +0 -141
  163. data/spec/password_complexity_spec.rb +0 -109
  164. data/spec/password_expiration_spec.rb +0 -244
  165. data/spec/password_grace_period_spec.rb +0 -93
  166. data/spec/remember_spec.rb +0 -451
  167. data/spec/reset_password_spec.rb +0 -229
  168. data/spec/rodauth_spec.rb +0 -343
  169. data/spec/session_expiration_spec.rb +0 -58
  170. data/spec/single_session_spec.rb +0 -127
  171. data/spec/spec_helper.rb +0 -327
  172. data/spec/two_factor_spec.rb +0 -1423
  173. data/spec/update_password_hash_spec.rb +0 -40
  174. data/spec/verify_account_grace_period_spec.rb +0 -171
  175. data/spec/verify_account_spec.rb +0 -240
  176. data/spec/verify_change_login_spec.rb +0 -46
  177. data/spec/verify_login_change_spec.rb +0 -232
  178. data/spec/views/layout-other.str +0 -11
  179. data/spec/views/layout.str +0 -11
  180. 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
 
@@ -140,13 +140,18 @@ module Rodauth
140
140
 
141
141
  private
142
142
 
143
+ def check_csrf?
144
+ return false if use_jwt?
145
+ super
146
+ end
147
+
143
148
  def before_rodauth
144
149
  if json_request?
145
150
  if jwt_check_accept? && (accept = request.env['HTTP_ACCEPT']) && accept !~ json_accept_regexp
146
151
  response.status = 406
147
152
  json_response[json_response_error_key] = json_not_accepted_error_message
148
153
  response['Content-Type'] ||= json_response_content_type
149
- response.write(request.send(:convert_to_json, json_response))
154
+ response.write(_json_response_body(json_response))
150
155
  request.halt
151
156
  end
152
157
 
@@ -173,6 +178,43 @@ module Rodauth
173
178
  end
174
179
  end
175
180
 
181
+ def before_webauthn_setup_route
182
+ super if defined?(super)
183
+ if use_jwt? && !param_or_nil(webauthn_setup_param)
184
+ cred = new_webauthn_credential
185
+ json_response[webauthn_setup_param] = cred.as_json
186
+ json_response[webauthn_setup_challenge_param] = cred.challenge
187
+ json_response[webauthn_setup_challenge_hmac_param] = compute_hmac(cred.challenge)
188
+ end
189
+ end
190
+
191
+ def before_webauthn_auth_route
192
+ super if defined?(super)
193
+ if use_jwt? && !param_or_nil(webauthn_auth_param)
194
+ cred = webauth_credential_options_for_get
195
+ json_response[webauthn_auth_param] = cred.as_json
196
+ json_response[webauthn_auth_challenge_param] = cred.challenge
197
+ json_response[webauthn_auth_challenge_hmac_param] = compute_hmac(cred.challenge)
198
+ end
199
+ end
200
+
201
+ def before_webauthn_login_route
202
+ super if defined?(super)
203
+ if use_jwt? && !param_or_nil(webauthn_auth_param) && account_from_login(param(login_param))
204
+ cred = webauth_credential_options_for_get
205
+ json_response[webauthn_auth_param] = cred.as_json
206
+ json_response[webauthn_auth_challenge_param] = cred.challenge
207
+ json_response[webauthn_auth_challenge_hmac_param] = compute_hmac(cred.challenge)
208
+ end
209
+ end
210
+
211
+ def before_webauthn_remove_route
212
+ super if defined?(super)
213
+ if use_jwt? && !param_or_nil(webauthn_remove_param)
214
+ json_response[webauthn_remove_param] = account_webauthn_usage
215
+ end
216
+ end
217
+
176
218
  def before_otp_setup_route
177
219
  super if defined?(super)
178
220
  if use_jwt? && otp_keys_use_hmac? && !param_or_nil(otp_setup_raw_param)
@@ -204,6 +246,12 @@ module Rodauth
204
246
  value
205
247
  end
206
248
 
249
+ def remove_session_value(key)
250
+ value = super
251
+ set_jwt if use_jwt?
252
+ value
253
+ end
254
+
207
255
  def json_response
208
256
  @json_response ||= {}
209
257
  end
@@ -0,0 +1,53 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ Feature.define(:jwt_cors, :JwtCors) do
5
+ depends :jwt
6
+
7
+ auth_value_method :jwt_cors_allow_origin, false
8
+ auth_value_method :jwt_cors_allow_methods, 'POST'
9
+ auth_value_method :jwt_cors_allow_headers, 'Content-Type, Authorization, Accept'
10
+ auth_value_method :jwt_cors_expose_headers, 'Authorization'
11
+ auth_value_method :jwt_cors_max_age, 86400
12
+
13
+ auth_methods(:jwt_cors_allow?)
14
+
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
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def before_rodauth
35
+ if (origin = request.env['HTTP_ORIGIN']) && jwt_cors_allow?
36
+ response['Access-Control-Allow-Origin'] = origin
37
+
38
+ # Handle CORS preflight request
39
+ if request.request_method == 'OPTIONS'
40
+ response['Access-Control-Allow-Methods'] = jwt_cors_allow_methods
41
+ response['Access-Control-Allow-Headers'] = jwt_cors_allow_headers
42
+ response['Access-Control-Max-Age'] = jwt_cors_max_age.to_s
43
+ response.status = 204
44
+ request.halt(response.finish)
45
+ end
46
+
47
+ response['Access-Control-Expose-Headers'] = jwt_cors_expose_headers
48
+ end
49
+
50
+ super
51
+ end
52
+ end
53
+ end
@@ -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,10 +10,10 @@ 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
@@ -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,79 @@ 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_required
76
+ if login_return_to_requested_location?
77
+ set_session_value(login_redirect_session_key, request.fullpath)
78
+ end
79
+ super
80
+ end
81
+
66
82
  def after_login_entered_during_multi_phase_login
67
83
  set_notice_now_flash need_password_notice_flash
84
+ if multi_phase_login_forms.length == 1 && (meth = multi_phase_login_forms[0][2])
85
+ send(meth)
86
+ end
68
87
  end
69
88
 
70
89
  def skip_login_field_on_login?
71
90
  return false unless use_multi_phase_login?
72
- @valid_login_entered
91
+ valid_login_entered?
73
92
  end
74
93
 
75
94
  def skip_password_field_on_login?
76
95
  return false unless use_multi_phase_login?
77
- @valid_login_entered != true
96
+ !valid_login_entered?
97
+ end
98
+
99
+ def valid_login_entered?
100
+ @valid_login_entered
78
101
  end
79
102
 
80
103
  def login_hidden_field
81
104
  "<input type='hidden' name=\"#{login_param}\" value=\"#{scope.h param(login_param)}\" />"
82
105
  end
83
106
 
107
+ def render_multi_phase_login_forms
108
+ multi_phase_login_forms.sort.map{|_, form, _| form}.join("\n")
109
+ end
110
+
84
111
  private
85
112
 
86
- def _login
113
+ def _login_form_footer_links
114
+ []
115
+ end
116
+
117
+ def _multi_phase_login_forms
118
+ forms = []
119
+ forms << [10, render("login-form"), nil] if has_password?
120
+ forms
121
+ end
122
+
123
+ def _login_form_footer
124
+ return '' if _login_form_footer_links.empty?
125
+ render('login-form-footer')
126
+ end
127
+
128
+ def _login(auth_type)
129
+ saved_login_redirect = remove_session_value(login_redirect_session_key)
87
130
  transaction do
88
131
  before_login
89
- update_session
132
+ login_session(auth_type)
133
+ yield if block_given?
90
134
  after_login
91
135
  end
92
136
  set_notice_flash login_notice_flash
93
- redirect login_redirect
137
+ redirect(saved_login_redirect || login_redirect)
94
138
  end
95
139
  end
96
140
  end
@@ -2,18 +2,18 @@
2
2
 
3
3
  module Rodauth
4
4
  Feature.define(:login_password_requirements_base, :LoginPasswordRequirementsBase) do
5
- auth_value_method :already_an_account_with_this_login_message, 'already an account with this login'
5
+ translatable_method :already_an_account_with_this_login_message, 'already an account with this login'
6
6
  auth_value_method :login_confirm_param, 'login-confirm'
7
7
  auth_value_method :login_minimum_length, 3
8
8
  auth_value_method :login_maximum_length, 255
9
- auth_value_method :logins_do_not_match_message, 'logins do not match'
9
+ translatable_method :logins_do_not_match_message, 'logins do not match'
10
10
  auth_value_method :password_confirm_param, 'password-confirm'
11
11
  auth_value_method :password_minimum_length, 6
12
- auth_value_method :passwords_do_not_match_message, 'passwords do not match'
12
+ translatable_method :passwords_do_not_match_message, 'passwords do not match'
13
13
  auth_value_method :require_email_address_logins?, true
14
14
  auth_value_method :require_login_confirmation?, true
15
15
  auth_value_method :require_password_confirmation?, true
16
- auth_value_method :same_as_existing_password_message, "invalid password, same as current password"
16
+ translatable_method :same_as_existing_password_message, "invalid password, same as current password"
17
17
 
18
18
  auth_value_methods(
19
19
  :login_confirm_label,
@@ -19,62 +19,59 @@ module Rodauth
19
19
  before 'otp_setup'
20
20
  before 'otp_disable'
21
21
 
22
- configuration_module_eval do
23
- def before_otp_authentication_route(&block)
24
- warn "before_otp_authentication_route is deprecated, switch to before_otp_auth_route"
25
- before_otp_auth_route(&block)
26
- end
27
- end
28
-
29
- button 'Authenticate via 2nd Factor', 'otp_auth'
30
- button 'Disable Two Factor Authentication', 'otp_disable'
31
- button 'Setup Two Factor Authentication', 'otp_setup'
22
+ button 'Authenticate Using TOTP', 'otp_auth'
23
+ button 'Disable TOTP Authentication', 'otp_disable'
24
+ button 'Setup TOTP Authentication', 'otp_setup'
32
25
 
33
- error_flash "Error disabling up two factor authentication", 'otp_disable'
34
- error_flash "Error logging in via two factor authentication", 'otp_auth'
35
- error_flash "Error setting up two factor authentication", 'otp_setup'
36
- error_flash "You have already setup two factor authentication", :otp_already_setup
26
+ error_flash "Error disabling TOTP authentication", 'otp_disable'
27
+ error_flash "Error logging in via TOTP authentication", 'otp_auth'
28
+ error_flash "Error setting up TOTP authentication", 'otp_setup'
29
+ error_flash "You have already setup TOTP authentication", 'otp_already_setup'
30
+ error_flash "TOTP authentication code use locked out due to numerous failures", 'otp_lockout'
37
31
 
38
- notice_flash "Two factor authentication has been disabled", 'otp_disable'
39
- notice_flash "Two factor authentication is now setup", 'otp_setup'
32
+ notice_flash "TOTP authentication has been disabled", 'otp_disable'
33
+ notice_flash "TOTP authentication is now setup", 'otp_setup'
40
34
 
41
35
  redirect :otp_disable
42
36
  redirect :otp_already_setup
43
37
  redirect :otp_setup
38
+ redirect(:otp_lockout){two_factor_auth_required_redirect}
44
39
 
45
40
  loaded_templates %w'otp-disable otp-auth otp-setup otp-auth-code-field password-field'
46
- view 'otp-disable', 'Disable Two Factor Authentication', 'otp_disable'
41
+ view 'otp-disable', 'Disable TOTP Authentication', 'otp_disable'
47
42
  view 'otp-auth', 'Enter Authentication Code', 'otp_auth'
48
- view 'otp-setup', 'Setup Two Factor Authentication', 'otp_setup'
43
+ view 'otp-setup', 'Setup TOTP Authentication', 'otp_setup'
44
+
45
+ translatable_method :otp_auth_link_text, "Authenticate Using TOTP"
46
+ translatable_method :otp_setup_link_text, "Setup TOTP Authentication"
47
+ translatable_method :otp_disable_link_text, "Disable TOTP Authentication"
49
48
 
50
49
  auth_value_method :otp_auth_failures_limit, 5
51
- auth_value_method :otp_auth_label, 'Authentication Code'
50
+ translatable_method :otp_auth_label, 'Authentication Code'
52
51
  auth_value_method :otp_auth_param, 'otp'
53
52
  auth_value_method :otp_class, ROTP::TOTP
54
53
  auth_value_method :otp_digits, nil
55
- auth_value_method :otp_drift, nil
54
+ auth_value_method :otp_drift, 30
56
55
  auth_value_method :otp_interval, nil
57
- auth_value_method :otp_invalid_auth_code_message, "Invalid authentication code"
58
- auth_value_method :otp_invalid_secret_message, "invalid secret"
56
+ translatable_method :otp_invalid_auth_code_message, "Invalid authentication code"
57
+ translatable_method :otp_invalid_secret_message, "invalid secret"
59
58
  auth_value_method :otp_keys_column, :key
60
59
  auth_value_method :otp_keys_id_column, :id
61
60
  auth_value_method :otp_keys_failures_column, :num_failures
62
61
  auth_value_method :otp_keys_table, :account_otp_keys
63
62
  auth_value_method :otp_keys_last_use_column, :last_use
64
- auth_value_method :otp_provisioning_uri_label, 'Provisioning URL'
65
- auth_value_method :otp_secret_label, 'Secret'
63
+ translatable_method :otp_provisioning_uri_label, 'Provisioning URL'
64
+ translatable_method :otp_secret_label, 'Secret'
66
65
  auth_value_method :otp_setup_param, 'otp_secret'
67
66
  auth_value_method :otp_setup_raw_param, 'otp_raw_secret'
67
+ translatable_method :otp_auth_form_footer, ''
68
68
 
69
69
  auth_cached_method :otp_key
70
70
  auth_cached_method :otp
71
71
  private :otp
72
72
 
73
73
  auth_value_methods(
74
- :otp_auth_form_footer,
75
74
  :otp_issuer,
76
- :otp_lockout_error_flash,
77
- :otp_lockout_redirect,
78
75
  :otp_keys_use_hmac?
79
76
  )
80
77
 
@@ -103,7 +100,7 @@ module Rodauth
103
100
  route(:otp_auth) do |r|
104
101
  require_login
105
102
  require_account_session
106
- require_two_factor_not_authenticated
103
+ require_two_factor_not_authenticated('totp')
107
104
  require_otp_setup
108
105
 
109
106
  if otp_locked_out?
@@ -121,7 +118,7 @@ module Rodauth
121
118
  r.post do
122
119
  if otp_valid_code?(param(otp_auth_param)) && otp_update_last_use
123
120
  before_otp_authentication
124
- two_factor_authenticate(:totp)
121
+ two_factor_authenticate('totp')
125
122
  end
126
123
 
127
124
  otp_record_authentication_failure
@@ -173,7 +170,9 @@ module Rodauth
173
170
  transaction do
174
171
  before_otp_setup
175
172
  otp_add_key
176
- two_factor_update_session(:totp)
173
+ unless two_factor_authenticated?
174
+ two_factor_update_session('totp')
175
+ end
177
176
  after_otp_setup
178
177
  end
179
178
  set_notice_flash otp_setup_notice_flash
@@ -199,7 +198,9 @@ module Rodauth
199
198
  transaction do
200
199
  before_otp_disable
201
200
  otp_remove
202
- two_factor_remove_session
201
+ if two_factor_login_type_match?('totp')
202
+ two_factor_remove_session('totp')
203
+ end
203
204
  after_otp_disable
204
205
  end
205
206
  set_notice_flash otp_disable_notice_flash
@@ -213,18 +214,6 @@ module Rodauth
213
214
  end
214
215
  end
215
216
 
216
- def two_factor_authentication_setup?
217
- super || otp_exists?
218
- end
219
-
220
- def two_factor_need_setup_redirect
221
- "#{prefix}/#{otp_setup_route}"
222
- end
223
-
224
- def two_factor_auth_required_redirect
225
- "#{prefix}/#{otp_auth_route}"
226
- end
227
-
228
217
  def two_factor_remove
229
218
  super
230
219
  otp_remove
@@ -235,19 +224,6 @@ module Rodauth
235
224
  otp_remove_auth_failures
236
225
  end
237
226
 
238
- def otp_auth_form_footer
239
- super if defined?(super)
240
- end
241
-
242
- def otp_lockout_redirect
243
- return super if defined?(super)
244
- default_redirect
245
- end
246
-
247
- def otp_lockout_error_flash
248
- "Authentication code use locked out due to numerous failures.#{super if defined?(super)}"
249
- end
250
-
251
227
  def require_otp_setup
252
228
  unless otp_exists?
253
229
  set_redirect_error_status(two_factor_not_setup_error_status)
@@ -265,11 +241,11 @@ module Rodauth
265
241
  ot_pass = ot_pass.gsub(/\s+/, '')
266
242
  if drift = otp_drift
267
243
  if otp.respond_to?(:verify_with_drift)
244
+ # :nocov:
268
245
  otp.verify_with_drift(ot_pass, drift)
269
- else
270
246
  # :nocov:
247
+ else
271
248
  otp.verify(ot_pass, :drift_behind=>drift, :drift_ahead=>drift)
272
- # :nocov:
273
249
  end
274
250
  else
275
251
  otp.verify(ot_pass)
@@ -278,6 +254,7 @@ module Rodauth
278
254
 
279
255
  def otp_remove
280
256
  otp_key_ds.delete
257
+ @otp_key = nil
281
258
  super if defined?(super)
282
259
  end
283
260
 
@@ -309,7 +286,7 @@ module Rodauth
309
286
  end
310
287
 
311
288
  def otp_issuer
312
- request.host
289
+ domain
313
290
  end
314
291
 
315
292
  def otp_provisioning_name
@@ -332,8 +309,37 @@ module Rodauth
332
309
  !!hmac_secret
333
310
  end
334
311
 
312
+ def possible_authentication_methods
313
+ methods = super
314
+ methods << 'totp' if otp_exists? && !@otp_tmp_key
315
+ methods
316
+ end
317
+
335
318
  private
336
319
 
320
+ def _two_factor_auth_links
321
+ links = super
322
+ links << [20, otp_auth_path, otp_auth_link_text] if otp_exists? && !otp_locked_out?
323
+ links
324
+ end
325
+
326
+ def _two_factor_setup_links
327
+ links = super
328
+ links << [20, otp_setup_path, otp_setup_link_text] unless otp_exists?
329
+ links
330
+ end
331
+
332
+ def _two_factor_remove_links
333
+ links = super
334
+ links << [20, otp_disable_path, otp_disable_link_text] if otp_exists?
335
+ links
336
+ end
337
+
338
+ def _two_factor_remove_all_from_session
339
+ two_factor_remove_session('totp')
340
+ super
341
+ end
342
+
337
343
  def clear_cached_otp
338
344
  remove_instance_variable(:@otp) if defined?(@otp)
339
345
  end
@@ -357,32 +363,24 @@ module Rodauth
357
363
  end
358
364
 
359
365
  if ROTP::Base32.respond_to?(:random_base32)
360
- # :nocov:
361
366
  def otp_new_secret
362
- ROTP::Base32.random_base32
367
+ ROTP::Base32.random_base32.downcase
363
368
  end
364
- # :nocov:
365
369
  else
370
+ # :nocov:
366
371
  def otp_new_secret
367
372
  ROTP::Base32.random.downcase
368
373
  end
374
+ # :nocov:
369
375
  end
370
376
 
371
- if RUBY_VERSION < '1.9'
372
- # :nocov:
373
- def base32_encode(data, length)
374
- chars = 'abcdefghijklmnopqrstuvwxyz234567'
375
- length.times.map{|i|chars[data[i] % 32].chr}.join
376
- end
377
- # :nocov:
378
- else
379
- def base32_encode(data, length)
380
- chars = 'abcdefghijklmnopqrstuvwxyz234567'
381
- length.times.map{|i|chars[data[i].ord % 32]}.join
382
- end
377
+ def base32_encode(data, length)
378
+ chars = 'abcdefghijklmnopqrstuvwxyz234567'
379
+ length.times.map{|i|chars[data[i].ord % 32]}.join
383
380
  end
384
381
 
385
382
  def _otp_tmp_key(secret)
383
+ @otp_tmp_key = true
386
384
  @otp_user_key = nil
387
385
  @otp_key = secret
388
386
  end