rodauth 1.23.0 → 2.4.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 (160) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +184 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +221 -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 +76 -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 +5 -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/password_pepper.rdoc +44 -0
  52. data/doc/recovery_codes.rdoc +18 -12
  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/release_notes/2.4.0.txt +22 -0
  58. data/doc/remember.rdoc +40 -64
  59. data/doc/reset_password.rdoc +12 -9
  60. data/doc/session_expiration.rdoc +1 -0
  61. data/doc/single_session.rdoc +16 -25
  62. data/doc/sms_codes.rdoc +24 -14
  63. data/doc/two_factor_base.rdoc +60 -22
  64. data/doc/verify_account.rdoc +14 -12
  65. data/doc/verify_account_grace_period.rdoc +6 -2
  66. data/doc/verify_login_change.rdoc +9 -8
  67. data/doc/webauthn.rdoc +115 -0
  68. data/doc/webauthn_login.rdoc +15 -0
  69. data/doc/webauthn_verify_account.rdoc +9 -0
  70. data/javascript/webauthn_auth.js +45 -0
  71. data/javascript/webauthn_setup.js +35 -0
  72. data/lib/roda/plugins/rodauth.rb +1 -1
  73. data/lib/rodauth.rb +33 -28
  74. data/lib/rodauth/features/account_expiration.rb +5 -5
  75. data/lib/rodauth/features/active_sessions.rb +158 -0
  76. data/lib/rodauth/features/audit_logging.rb +98 -0
  77. data/lib/rodauth/features/base.rb +152 -49
  78. data/lib/rodauth/features/change_password_notify.rb +1 -1
  79. data/lib/rodauth/features/close_account.rb +8 -6
  80. data/lib/rodauth/features/confirm_password.rb +40 -2
  81. data/lib/rodauth/features/create_account.rb +8 -13
  82. data/lib/rodauth/features/disallow_common_passwords.rb +1 -1
  83. data/lib/rodauth/features/disallow_password_reuse.rb +5 -3
  84. data/lib/rodauth/features/email_auth.rb +30 -28
  85. data/lib/rodauth/features/email_base.rb +3 -3
  86. data/lib/rodauth/features/http_basic_auth.rb +55 -35
  87. data/lib/rodauth/features/jwt.rb +63 -16
  88. data/lib/rodauth/features/jwt_cors.rb +15 -15
  89. data/lib/rodauth/features/jwt_refresh.rb +42 -13
  90. data/lib/rodauth/features/lockout.rb +11 -13
  91. data/lib/rodauth/features/login.rb +58 -13
  92. data/lib/rodauth/features/login_password_requirements_base.rb +13 -8
  93. data/lib/rodauth/features/otp.rb +76 -82
  94. data/lib/rodauth/features/password_complexity.rb +8 -13
  95. data/lib/rodauth/features/password_expiration.rb +1 -1
  96. data/lib/rodauth/features/password_grace_period.rb +17 -10
  97. data/lib/rodauth/features/password_pepper.rb +45 -0
  98. data/lib/rodauth/features/recovery_codes.rb +47 -51
  99. data/lib/rodauth/features/remember.rb +13 -27
  100. data/lib/rodauth/features/reset_password.rb +25 -25
  101. data/lib/rodauth/features/session_expiration.rb +7 -10
  102. data/lib/rodauth/features/single_session.rb +8 -6
  103. data/lib/rodauth/features/sms_codes.rb +58 -68
  104. data/lib/rodauth/features/two_factor_base.rb +134 -30
  105. data/lib/rodauth/features/verify_account.rb +28 -20
  106. data/lib/rodauth/features/verify_account_grace_period.rb +18 -9
  107. data/lib/rodauth/features/verify_login_change.rb +11 -10
  108. data/lib/rodauth/features/webauthn.rb +505 -0
  109. data/lib/rodauth/features/webauthn_login.rb +70 -0
  110. data/lib/rodauth/features/webauthn_verify_account.rb +46 -0
  111. data/lib/rodauth/migrations.rb +16 -5
  112. data/lib/rodauth/version.rb +2 -2
  113. data/templates/button.str +1 -3
  114. data/templates/change-login.str +1 -2
  115. data/templates/change-password.str +3 -5
  116. data/templates/close-account.str +2 -2
  117. data/templates/confirm-password.str +1 -1
  118. data/templates/create-account.str +1 -1
  119. data/templates/email-auth-request-form.str +1 -2
  120. data/templates/email-auth.str +1 -1
  121. data/templates/global-logout-field.str +6 -0
  122. data/templates/login-confirm-field.str +2 -4
  123. data/templates/login-display.str +3 -2
  124. data/templates/login-field.str +2 -4
  125. data/templates/login-form-footer.str +6 -0
  126. data/templates/login-form.str +7 -0
  127. data/templates/login.str +1 -9
  128. data/templates/logout.str +1 -1
  129. data/templates/multi-phase-login.str +3 -0
  130. data/templates/otp-auth-code-field.str +5 -3
  131. data/templates/otp-auth.str +1 -1
  132. data/templates/otp-disable.str +1 -1
  133. data/templates/otp-setup.str +3 -3
  134. data/templates/password-confirm-field.str +2 -4
  135. data/templates/password-field.str +2 -4
  136. data/templates/recovery-auth.str +3 -6
  137. data/templates/recovery-codes.str +1 -1
  138. data/templates/remember.str +15 -20
  139. data/templates/reset-password-request.str +2 -2
  140. data/templates/reset-password.str +1 -2
  141. data/templates/sms-auth.str +1 -1
  142. data/templates/sms-code-field.str +5 -3
  143. data/templates/sms-confirm.str +1 -2
  144. data/templates/sms-disable.str +1 -2
  145. data/templates/sms-request.str +1 -1
  146. data/templates/sms-setup.str +6 -4
  147. data/templates/two-factor-auth.str +5 -0
  148. data/templates/two-factor-disable.str +6 -0
  149. data/templates/two-factor-manage.str +16 -0
  150. data/templates/unlock-account-request.str +2 -2
  151. data/templates/unlock-account.str +1 -1
  152. data/templates/verify-account-resend.str +1 -1
  153. data/templates/verify-account.str +1 -2
  154. data/templates/verify-login-change.str +1 -1
  155. data/templates/webauthn-auth.str +11 -0
  156. data/templates/webauthn-remove.str +14 -0
  157. data/templates/webauthn-setup.str +12 -0
  158. metadata +96 -13
  159. data/doc/verify_change_login.rdoc +0 -11
  160. data/lib/rodauth/features/verify_change_login.rb +0 -20
@@ -4,7 +4,7 @@ module Rodauth
4
4
  Feature.define(:change_password_notify, :ChangePasswordNotify) do
5
5
  depends :change_password, :email_base
6
6
 
7
- auth_value_method :password_changed_email_subject, 'Password Changed'
7
+ translatable_method :password_changed_email_subject, 'Password Changed'
8
8
 
9
9
  auth_value_methods(
10
10
  :password_changed_email_body
@@ -33,7 +33,11 @@ module Rodauth
33
33
  end
34
34
 
35
35
  r.post do
36
- if !close_account_requires_password? || password_match?(param(password_param))
36
+ catch_error do
37
+ if close_account_requires_password? && !password_match?(param(password_param))
38
+ throw_error_status(invalid_password_error_status, password_param, invalid_password_message)
39
+ end
40
+
37
41
  transaction do
38
42
  before_close_account
39
43
  close_account
@@ -46,12 +50,10 @@ module Rodauth
46
50
 
47
51
  set_notice_flash close_account_notice_flash
48
52
  redirect close_account_redirect
49
- else
50
- set_response_error_status(invalid_password_error_status)
51
- set_field_error(password_param, invalid_password_message)
52
- set_error_flash close_account_error_flash
53
- close_account_view
54
53
  end
54
+
55
+ set_error_flash close_account_error_flash
56
+ close_account_view
55
57
  end
56
58
  end
57
59
 
@@ -4,20 +4,26 @@ module Rodauth
4
4
  Feature.define(:confirm_password, :ConfirmPassword) do
5
5
  notice_flash "Your password has been confirmed"
6
6
  error_flash "There was an error confirming your password"
7
+ error_flash "You need to confirm your password before continuing", 'password_authentication_required'
7
8
  loaded_templates %w'confirm-password password-field'
8
9
  view 'confirm-password', 'Confirm Password'
9
10
  additional_form_tags
10
11
  button 'Confirm Password'
11
12
  before
12
13
  after
14
+ redirect(:password_authentication_required){confirm_password_path}
13
15
 
14
16
  session_key :confirm_password_redirect_session_key, :confirm_password_redirect
17
+ translatable_method :confirm_password_link_text, "Enter Password"
18
+ auth_value_method :password_authentication_required_error_status, 401
19
+
15
20
  auth_value_methods :confirm_password_redirect
16
21
 
17
22
  auth_methods :confirm_password
18
23
 
19
24
  route do |r|
20
- require_account
25
+ require_login
26
+ require_account_session
21
27
  before_confirm_password_route
22
28
 
23
29
  request.get do
@@ -42,12 +48,44 @@ module Rodauth
42
48
  end
43
49
  end
44
50
 
51
+ def require_password_authentication
52
+ require_login
53
+
54
+ if require_password_authentication? && has_password?
55
+ set_redirect_error_status(password_authentication_required_error_status)
56
+ set_redirect_error_flash password_authentication_required_error_flash
57
+ set_session_value(confirm_password_redirect_session_key, request.fullpath)
58
+ redirect password_authentication_required_redirect
59
+ end
60
+ end
61
+
45
62
  def confirm_password
63
+ authenticated_by.delete('autologin')
64
+ authenticated_by.delete('remember')
65
+ authenticated_by.delete('email_auth')
66
+ authenticated_by.delete('password')
67
+ authenticated_by.unshift("password")
68
+ remove_session_value(autologin_type_session_key)
46
69
  nil
47
70
  end
48
71
 
49
72
  def confirm_password_redirect
50
- session.delete(confirm_password_redirect_session_key) || default_redirect
73
+ remove_session_value(confirm_password_redirect_session_key) || default_redirect
74
+ end
75
+
76
+ private
77
+
78
+ def _two_factor_auth_links
79
+ links = (super if defined?(super)) || []
80
+ if authenticated_by.length == 1 && !authenticated_by.include?('password') && has_password?
81
+ links << [5, confirm_password_path, confirm_password_link_text]
82
+ end
83
+ links
84
+ end
85
+
86
+ def require_password_authentication?
87
+ return true if defined?(super) && super
88
+ !authenticated_by.include?('password')
51
89
  end
52
90
  end
53
91
  end
@@ -2,9 +2,8 @@
2
2
 
3
3
  module Rodauth
4
4
  Feature.define(:create_account, :CreateAccount) do
5
- depends :login_password_requirements_base
5
+ depends :login, :login_password_requirements_base
6
6
 
7
- depends :login
8
7
  notice_flash 'Your account has been created'
9
8
  error_flash "There was an error creating your account"
10
9
  loaded_templates %w'create-account login-field login-confirm-field password-field password-confirm-field'
@@ -16,10 +15,9 @@ module Rodauth
16
15
  redirect
17
16
 
18
17
  auth_value_method :create_account_autologin?, true
18
+ translatable_method :create_account_link_text, "Create a New Account"
19
19
  auth_value_method :create_account_set_password?, true
20
20
 
21
- auth_value_methods :create_account_link
22
-
23
21
  auth_methods(
24
22
  :save_account,
25
23
  :set_new_account_password
@@ -32,6 +30,7 @@ module Rodauth
32
30
  route do |r|
33
31
  check_already_logged_in
34
32
  before_create_account_route
33
+ @password_field_autocomplete_value = 'new-password'
35
34
 
36
35
  r.get do
37
36
  create_account_view
@@ -76,7 +75,7 @@ module Rodauth
76
75
  end
77
76
  after_create_account
78
77
  if create_account_autologin?
79
- update_session
78
+ autologin_session('create_account')
80
79
  end
81
80
  set_notice_flash create_account_notice_flash
82
81
  redirect create_account_redirect
@@ -88,14 +87,6 @@ module Rodauth
88
87
  end
89
88
  end
90
89
 
91
- def create_account_link
92
- "<p><a href=\"#{create_account_path}\">Create a New Account</a></p>"
93
- end
94
-
95
- def login_form_footer
96
- super + create_account_link
97
- end
98
-
99
90
  def set_new_account_password(password)
100
91
  account[account_password_hash_column] = password_hash(password)
101
92
  end
@@ -121,6 +112,10 @@ module Rodauth
121
112
 
122
113
  private
123
114
 
115
+ def _login_form_footer_links
116
+ super << [10, create_account_path, create_account_link_text]
117
+ end
118
+
124
119
  def _new_account(login)
125
120
  acc = {login_column=>login}
126
121
  unless skip_status_checks?
@@ -5,7 +5,7 @@ module Rodauth
5
5
  depends :login_password_requirements_base
6
6
 
7
7
  auth_value_method :most_common_passwords_file, File.expand_path('../../../../dict/top-10_000-passwords.txt', __FILE__)
8
- auth_value_method :password_is_one_of_the_most_common_message, "is one of the most common passwords"
8
+ translatable_method :password_is_one_of_the_most_common_message, "is one of the most common passwords"
9
9
  auth_value_method :most_common_passwords, nil
10
10
 
11
11
  auth_methods :password_one_of_most_common?
@@ -4,7 +4,7 @@ module Rodauth
4
4
  Feature.define(:disallow_password_reuse, :DisallowPasswordReuse) do
5
5
  depends :login_password_requirements_base
6
6
 
7
- auth_value_method :password_same_as_previous_password_message, "same as previous password"
7
+ translatable_method :password_same_as_previous_password_message, "same as previous password"
8
8
  auth_value_method :previous_password_account_id_column, :account_id
9
9
  auth_value_method :previous_password_hash_column, :password_hash
10
10
  auth_value_method :previous_password_hash_table, :account_previous_password_hashes
@@ -51,11 +51,13 @@ module Rodauth
51
51
  return true if salts.empty?
52
52
 
53
53
  salts.any? do |hash_id, salt|
54
- db.get(Sequel.function(function_name(:rodauth_previous_password_hash_match), hash_id, BCrypt::Engine.hash_secret(password, salt)))
54
+ database_function_password_match?(:rodauth_previous_password_hash_match, hash_id, password, salt)
55
55
  end
56
56
  else
57
57
  # :nocov:
58
- previous_password_ds.select_map(previous_password_hash_column).any?{|hash| BCrypt::Password.new(hash) == password}
58
+ previous_password_ds.select_map(previous_password_hash_column).any? do |hash|
59
+ password_hash_match?(hash, password)
60
+ end
59
61
  # :nocov:
60
62
  end
61
63
 
@@ -4,8 +4,6 @@ module Rodauth
4
4
  Feature.define(:email_auth, :EmailAuth) do
5
5
  depends :login, :email_base
6
6
 
7
- def_deprecated_alias :no_matching_email_auth_key_error_flash, :no_matching_email_auth_key_message
8
-
9
7
  notice_flash "An email has been sent to you with a link to login to your account", 'email_auth_email_sent'
10
8
  error_flash "There was an error logging you in"
11
9
  error_flash "There was an error requesting an email link to authenticate", 'email_auth_request'
@@ -23,17 +21,16 @@ module Rodauth
23
21
  redirect(:email_auth_email_recently_sent){default_post_email_redirect}
24
22
 
25
23
  auth_value_method :email_auth_deadline_column, :deadline
26
- auth_value_method :email_auth_deadline_interval, {:days=>1}
27
- auth_value_method :email_auth_email_subject, 'Login Link'
24
+ auth_value_method :email_auth_deadline_interval, {:days=>1}.freeze
25
+ translatable_method :email_auth_email_subject, 'Login Link'
28
26
  auth_value_method :email_auth_id_column, :id
29
27
  auth_value_method :email_auth_key_column, :key
30
28
  auth_value_method :email_auth_key_param, 'key'
31
29
  auth_value_method :email_auth_email_last_sent_column, :email_last_sent
32
30
  auth_value_method :email_auth_skip_resend_email_within, 300
33
31
  auth_value_method :email_auth_table, :account_email_auth_keys
32
+ auth_value_method :force_email_auth?, false
34
33
  session_key :email_auth_session_key, :email_auth_key
35
-
36
- auth_value_methods :force_email_auth?
37
34
 
38
35
  auth_methods(
39
36
  :create_email_auth_email,
@@ -74,7 +71,7 @@ module Rodauth
74
71
 
75
72
  r.get do
76
73
  if key = param_or_nil(email_auth_key_param)
77
- session[email_auth_session_key] = key
74
+ set_session_value(email_auth_session_key, key)
78
75
  redirect(r.path)
79
76
  end
80
77
 
@@ -82,7 +79,7 @@ module Rodauth
82
79
  if account_from_email_auth_key(key)
83
80
  email_auth_view
84
81
  else
85
- session[email_auth_session_key] = nil
82
+ remove_session_value(email_auth_session_key)
86
83
  set_redirect_error_flash no_matching_email_auth_key_error_flash
87
84
  redirect require_login_redirect
88
85
  end
@@ -97,7 +94,7 @@ module Rodauth
97
94
  redirect email_auth_email_sent_redirect
98
95
  end
99
96
 
100
- _login
97
+ login('email_auth')
101
98
  end
102
99
  end
103
100
 
@@ -148,43 +145,44 @@ module Rodauth
148
145
  ds.get(email_auth_key_column)
149
146
  end
150
147
 
151
- def login_form_footer
152
- footer = super
153
- footer += email_auth_request_form if valid_login_entered?
154
- footer
155
- end
156
-
157
148
  def email_auth_request_form
158
149
  render('email-auth-request-form')
159
150
  end
160
151
 
161
152
  def after_login_entered_during_multi_phase_login
162
- if force_email_auth?
163
- # If the account does not have a password hash, just send the
164
- # email link.
165
- _email_auth_request
166
- redirect email_auth_email_sent_redirect
167
- else
168
- # If the account has a password hash, allow password login, but
169
- # we will show form below to also login via email link.
170
- super
171
- end
153
+ # If forcing email auth, just send the email link.
154
+ _email_auth_request_and_redirect if force_email_auth?
155
+
156
+ super
172
157
  end
173
158
 
174
159
  def use_multi_phase_login?
175
160
  true
176
161
  end
177
162
 
178
- def force_email_auth?
179
- get_password_hash.nil?
163
+ def possible_authentication_methods
164
+ methods = super
165
+ methods << 'email_auth' if !methods.include?('password') && allow_email_auth?
166
+ methods
180
167
  end
181
168
 
182
169
  private
183
170
 
171
+ def _multi_phase_login_forms
172
+ forms = super
173
+ forms << [30, email_auth_request_form, :_email_auth_request_and_redirect] if valid_login_entered? && allow_email_auth?
174
+ forms
175
+ end
176
+
184
177
  def email_auth_email_recently_sent?
185
178
  (email_last_sent = get_email_auth_email_last_sent) && (Time.now - email_last_sent < email_auth_skip_resend_email_within)
186
179
  end
187
180
 
181
+ def _email_auth_request_and_redirect
182
+ _email_auth_request
183
+ redirect email_auth_email_sent_redirect
184
+ end
185
+
188
186
  def _email_auth_request
189
187
  if email_auth_email_recently_sent?
190
188
  set_redirect_error_flash email_auth_email_recently_sent_error_flash
@@ -204,6 +202,10 @@ module Rodauth
204
202
 
205
203
  attr_reader :email_auth_key_value
206
204
 
205
+ def allow_email_auth?
206
+ defined?(super) ? super : true
207
+ end
208
+
207
209
  def after_login
208
210
  # Remove the email auth key after any login, even if
209
211
  # it is a password login. This is done to invalidate
@@ -213,7 +215,7 @@ module Rodauth
213
215
  # that allows login access to the account becomes a
214
216
  # security liability, and it is best to remove it.
215
217
  remove_email_auth_key
216
- super if defined?(super)
218
+ super
217
219
  end
218
220
 
219
221
  def after_close_account
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Rodauth
4
4
  Feature.define(:email_base, :EmailBase) do
5
- auth_value_method :email_subject_prefix, nil
5
+ translatable_method :email_subject_prefix, ''
6
6
  auth_value_method :require_mail?, true
7
7
  auth_value_method :allow_raw_email_token?, false
8
8
 
@@ -43,7 +43,7 @@ module Rodauth
43
43
  end
44
44
 
45
45
  def email_from
46
- "webmaster@#{request.host}"
46
+ "webmaster@#{domain}"
47
47
  end
48
48
 
49
49
  def email_to
@@ -51,7 +51,7 @@ module Rodauth
51
51
  end
52
52
 
53
53
  def token_link(route, param, key)
54
- "#{route_url(route)}?#{param}=#{account_id}#{token_separator}#{convert_email_token_key(key)}"
54
+ route_url(route, param => "#{account_id}#{token_separator}#{convert_email_token_key(key)}")
55
55
  end
56
56
 
57
57
  def convert_email_token_key(key)
@@ -3,54 +3,74 @@
3
3
  module Rodauth
4
4
  Feature.define(:http_basic_auth, :HttpBasicAuth) do
5
5
  auth_value_method :http_basic_auth_realm, "protected"
6
- auth_value_method :require_http_basic_auth, false
6
+ auth_value_method :require_http_basic_auth?, false
7
7
 
8
- def session
9
- return @session if defined?(@session)
10
- sess = super
11
- return sess if sess[session_key]
12
- return sess unless token = ((v = request.env['HTTP_AUTHORIZATION']) && v[/\A *Basic (.*)\Z/, 1])
13
- username, password = token.unpack("m*").first.split(/:/, 2)
8
+ def logged_in?
9
+ ret = super
14
10
 
15
- if username && password
16
- catch_error do
17
- unless account_from_login(username)
18
- throw_basic_auth_error(login_param, no_matching_login_message)
19
- end
20
-
21
- before_login_attempt
22
-
23
- unless open_account?
24
- throw_basic_auth_error(login_param, no_matching_login_message)
25
- end
26
-
27
- unless password_match?(password)
28
- after_login_failure
29
- throw_basic_auth_error(password_param, invalid_password_message)
30
- end
31
-
32
- transaction do
33
- before_login
34
- sess[session_key] = account_session_value
35
- after_login
36
- end
37
- end
11
+ if !ret && !defined?(@checked_http_basic_auth)
12
+ http_basic_auth
13
+ ret = super
38
14
  end
39
15
 
40
- sess
16
+ ret
41
17
  end
42
18
 
43
- private
44
-
45
19
  def require_login
46
- if !logged_in? && require_http_basic_auth
20
+ if require_http_basic_auth?
21
+ require_http_basic_auth
22
+ end
23
+
24
+ super
25
+ end
26
+
27
+ def require_http_basic_auth
28
+ unless http_basic_auth
47
29
  set_http_basic_auth_error_response
48
30
  request.halt
49
31
  end
32
+ end
50
33
 
51
- super
34
+ def http_basic_auth
35
+ return @checked_http_basic_auth if defined?(@checked_http_basic_auth)
36
+
37
+ @checked_http_basic_auth = nil
38
+ return unless token = ((v = request.env['HTTP_AUTHORIZATION']) && v[/\A *Basic (.*)\Z/, 1])
39
+
40
+ username, password = token.unpack("m*").first.split(/:/, 2)
41
+ return unless username && password
42
+
43
+ catch_error do
44
+ unless account_from_login(username)
45
+ throw_basic_auth_error(login_param, no_matching_login_message)
46
+ end
47
+
48
+ before_login_attempt
49
+
50
+ unless open_account?
51
+ throw_basic_auth_error(login_param, no_matching_login_message)
52
+ end
53
+
54
+ unless password_match?(password)
55
+ after_login_failure
56
+ throw_basic_auth_error(password_param, invalid_password_message)
57
+ end
58
+
59
+ transaction do
60
+ before_login
61
+ login_session('password')
62
+ after_login
63
+ end
64
+
65
+ @checked_http_basic_auth = true
66
+ return true
67
+ end
68
+
69
+ nil
52
70
  end
53
71
 
72
+ private
73
+
54
74
  def set_http_basic_auth_error_response
55
75
  response.status = 401
56
76
  response.headers["WWW-Authenticate"] = "Basic realm=\"#{http_basic_auth_realm}\""