lato 3.22.2 → 3.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 97c06009567898ffba7a3b34ace5ab8de7d896f19708d371839f5194589df395
4
- data.tar.gz: 21d2297e40a0d97e46f343413114fdbebebc0bfa40b53d567cb63229a3bc5055
3
+ metadata.gz: 926e453e02a52408f23980d6ed7e0e74c40de3f4e5841a911420ea1b3df25128
4
+ data.tar.gz: 179a8b9e1959c8c734e92a17eec7c30e640da7f65df7b266e35f8f257d75094b
5
5
  SHA512:
6
- metadata.gz: 4cc47fd5c9ec4cc561a31348e637c93ccf3f6a63203d236fa88c309008412f336e450b539428e2ea1c2d4e57b6c28f6c685bc39d77b90fac14bf04d9b125f2cf
7
- data.tar.gz: ca60c0153ea46ea522403fab3d49e64b89297d3af97c157ccfc94cd0b94e2ab93d6343ab643387d2321f9048d8f9e3770e5fdb647f262ecaf9bba414b7443ea5
6
+ metadata.gz: ed3a9f66a07bac72a2bba82dcfb2db645574b0c26e5860bccb72f21da54a14fc273554935408226365bcbc3b17b64b559298c7e32c4a3a0eae815271c0086c37
7
+ data.tar.gz: 02b9de09b5551dec982080c649f0e767faf3cb75c8c735fae962a30249095e3129ec7d2aa76cbdac8417931b1f1acea88f2a84691a492604aa81368610b9d5b8
@@ -47,7 +47,13 @@ module Lato
47
47
  end
48
48
 
49
49
  def session_create(user_id)
50
- cookies.encrypted[:lato_session] = { value: Lato::Session.generate_session_per_user(user_id), expires: Lato.config.session_lifetime.from_now }
50
+ # httponly keeps the cookie out of reach of injected scripts; secure avoids leaking it over plain HTTP.
51
+ cookies.encrypted[:lato_session] = {
52
+ value: Lato::Session.generate_session_per_user(user_id),
53
+ expires: Lato.config.session_lifetime.from_now,
54
+ httponly: true,
55
+ secure: request.ssl?
56
+ }
51
57
  @session = Lato::Session.new(cookies.encrypted[:lato_session])
52
58
 
53
59
  true
@@ -10,7 +10,6 @@ module Lato
10
10
 
11
11
  around_action :catch_exceptions
12
12
 
13
- before_action :override_request_remote_ip
14
13
  before_action :set_default_locale
15
14
 
16
15
  def index
@@ -39,13 +38,6 @@ module Lato
39
38
 
40
39
  protected
41
40
 
42
- # This method override the request remote ip with the X-Forwarded-For header if exists.
43
- # This method is used to get the real ip of the user when the application is behind a proxy.
44
- # For example if the application is behind a nginx proxy the request.remote_ip will be the ip of the proxy and not the ip of the user.
45
- def override_request_remote_ip
46
- request.remote_ip = request.headers['X-Forwarded-For'].split(',').first if request.headers['X-Forwarded-For']
47
- end
48
-
49
41
  # This method set the default locale for the application.
50
42
  # The default locale is the locale of the user if exists, otherwise is the default locale of the application.
51
43
  def set_default_locale
@@ -1,10 +1,11 @@
1
1
  module Lato
2
2
  class AuthenticationController < ApplicationController
3
- before_action :limit_requests, only: %i[signin_action signup_action accept_invitation_action recover_password_action update_password_action]
3
+ before_action :limit_requests, only: %i[signin_action signup_action accept_invitation_action recover_password_action update_password_action verify_email_action authenticator_action webauthn_action]
4
4
  before_action :not_authenticate_session, only: %i[signin signin_action signup signup_action accept_invitation accept_invitation_action]
5
5
  before_action :authenticate_session, only: %i[signout signout_action]
6
6
 
7
- before_action :find_user, only: %i[verify_email verify_email_action update_password update_password_action]
7
+ before_action :find_user, only: %i[verify_email verify_email_action]
8
+ before_action :find_recover_password_user, only: %i[update_password update_password_action]
8
9
  before_action :find_invitation, only: %i[accept_invitation accept_invitation_action]
9
10
  before_action :find_authentication_user, only: %i[authentication_method authentication_method_action webauthn webauthn_action]
10
11
 
@@ -33,8 +34,9 @@ module Lato
33
34
  ))
34
35
  redirect_path = determine_authentication_redirect(@user)
35
36
  if redirect_path
37
+ # Second factor still pending: expose only where to continue, not user data.
36
38
  format.html { redirect_to redirect_path }
37
- format.json { render json: @user }
39
+ format.json { render json: { redirect: redirect_path } }
38
40
  else
39
41
  session_create(@user.id)
40
42
  format.html { redirect_to lato.root_path }
@@ -145,11 +147,14 @@ module Lato
145
147
 
146
148
  def recover_password_action
147
149
  @user = Lato::User.new
150
+ return unless verify_hcaptcha(:recover_password)
148
151
 
149
152
  respond_to do |format|
150
153
  if @user.request_recover_password(params.require(:user).permit(:email))
151
- format.html { redirect_to lato.authentication_update_password_path(id: @user.id) }
152
- format.json { render json: @user }
154
+ # Same response whether the email is registered or not: the user is tracked in session, not in the URL.
155
+ session[:recover_password_email] = @user.email
156
+ format.html { redirect_to lato.authentication_update_password_path, notice: I18n.t('lato.authentication_controller.recover_password_action_notice') }
157
+ format.json { render json: {} }
153
158
  else
154
159
  format.html { render :recover_password, status: :unprocessable_entity }
155
160
  format.json { render json: @user.errors, status: :unprocessable_entity }
@@ -164,10 +169,12 @@ module Lato
164
169
 
165
170
  def update_password_action
166
171
  respond_to do |format|
167
- if @user.update_password(params.require(:user).permit(:code, :password, :password_confirmation))
172
+ if @recover_password_user.update_password(params.require(:user).permit(:code, :password, :password_confirmation))
173
+ session[:recover_password_email] = nil
168
174
  format.html { redirect_to lato.authentication_signin_path, notice: I18n.t('lato.authentication_controller.update_password_action_notice') }
169
- format.json { render json: @user }
175
+ format.json { render json: {} }
170
176
  else
177
+ @user.errors.merge!(@recover_password_user.errors)
171
178
  format.html { render :update_password, status: :unprocessable_entity }
172
179
  format.json { render json: @user.errors, status: :unprocessable_entity }
173
180
  end
@@ -283,6 +290,16 @@ module Lato
283
290
  respond_to_with_not_found unless @user
284
291
  end
285
292
 
293
+ # Unknown emails get a blank user: the code check fails like for a wrong code, without revealing anything.
294
+ # Views always get a blank @user: a persisted record would change the rendered form (id, method) and leak existence.
295
+ def find_recover_password_user
296
+ email = session[:recover_password_email]
297
+ return redirect_to lato.authentication_recover_password_path if email.blank?
298
+
299
+ @recover_password_user = Lato::User.find_by(email: email) || Lato::User.new
300
+ @user = Lato::User.new
301
+ end
302
+
286
303
  def find_invitation
287
304
  @invitation = Lato::Invitation.find_by(id: params[:id], accepted_code: params[:accepted_code])
288
305
  respond_to_with_not_found unless @invitation
@@ -65,6 +65,18 @@ module Lato
65
65
  webauthn_id.present? && webauthn_public_key.present?
66
66
  end
67
67
 
68
+ # Serialization
69
+ ##
70
+
71
+ # Credentials and second factor secrets must never leave the server (e.g. through `render json: user`).
72
+ SENSITIVE_ATTRIBUTES = %w[password_digest authenticator_secret webauthn_id webauthn_public_key].freeze
73
+
74
+ def serializable_hash(options = nil)
75
+ options = (options || {}).dup
76
+ options[:except] = Array(options[:except]).map(&:to_s) | SENSITIVE_ATTRIBUTES
77
+ super(options)
78
+ end
79
+
68
80
  # Helpers
69
81
  ##
70
82
 
@@ -117,14 +129,11 @@ module Lato
117
129
  def signin(params)
118
130
  self.email = params[:email]
119
131
 
120
- user = Lato::User.find_by(email: params[:email])
121
- unless user
122
- errors.add(:email, :not_correct)
123
- return
124
- end
125
-
126
- unless user.authenticate(params[:password])
127
- errors.add(:password, :not_correct)
132
+ # Same error and similar timing for unknown email and wrong password, so signin can not be used to enumerate users.
133
+ user = Lato::User.find_by(email: normalize_email(params[:email]))
134
+ authenticated = user ? user.authenticate(params[:password].to_s) : dummy_password_check(params[:password])
135
+ unless authenticated
136
+ errors.add(:base, :credentials_not_correct)
128
137
  return
129
138
  end
130
139
 
@@ -211,7 +220,7 @@ module Lato
211
220
  return
212
221
  end
213
222
 
214
- unless email_verification_code == params[:code]
223
+ unless ActiveSupport::SecurityUtils.secure_compare(email_verification_code, params[:code].to_s)
215
224
  errors.add(:base, :email_verification_code_invalid)
216
225
  return
217
226
  end
@@ -223,42 +232,41 @@ module Lato
223
232
  true
224
233
  end
225
234
 
235
+ # NOTE: Always succeeds for a well formed email, registered or not, so the response can not be used to enumerate users.
226
236
  def request_recover_password(params)
227
- user = Lato::User.find_by(email: params[:email])
228
- unless user
229
- errors.add(:email, :not_registered)
237
+ self.email = normalize_email(params[:email])
238
+ if email.blank?
239
+ errors.add(:email, :blank)
230
240
  return
231
241
  end
232
242
 
233
- code = SecureRandom.hex.upcase
234
- delivery = Lato::UserMailer.password_update_mail(user.id, code).deliver_now
235
- unless delivery
236
- errors.add(:base, :email_sending_error)
237
- return
238
- end
243
+ user = Lato::User.find_by(email: email)
244
+ return true unless user
239
245
 
240
- self.id = user.id
241
- reload
246
+ # Per-user throttle: avoid mail bombing a victim, the previous code stays valid.
247
+ return true if user.c_password_update_semaphore
242
248
 
243
- c_password_update_code(code)
249
+ code = SecureRandom.hex.upcase
250
+ user.c_password_update_code(code)
251
+ user.c_password_update_semaphore(true)
252
+
253
+ # Async delivery keeps response time independent from email existence.
254
+ Lato::UserMailer.password_update_mail(user.id, code).deliver_later
244
255
 
245
256
  true
246
257
  end
247
258
 
248
259
  def update_password(params)
249
- password_update_code = c_password_update_code
260
+ password_update_code = new_record? ? nil : c_password_update_code
250
261
 
251
- if password_update_code.blank?
252
- errors.add(:base, :password_update_code_expired)
253
- return
254
- end
255
-
256
- unless password_update_code == params[:code]
262
+ # Single error for missing, expired or wrong code: distinct messages would reveal which emails are registered.
263
+ if password_update_code.blank? || !ActiveSupport::SecurityUtils.secure_compare(password_update_code, params[:code].to_s)
257
264
  errors.add(:base, :password_update_code_invalid)
258
265
  return
259
266
  end
260
267
 
261
268
  c_password_update_code('')
269
+ c_password_update_semaphore(false)
262
270
 
263
271
  update(params.permit(:password, :password_confirmation).merge(
264
272
  authenticator_secret: nil, # Reset authenticator secret when password is updated
@@ -467,8 +475,34 @@ module Lato
467
475
  value
468
476
  end
469
477
 
478
+ def c_password_update_semaphore(value = nil)
479
+ cache_key = "Lato::User/c_password_update_semaphore/#{id}"
480
+ return Rails.cache.read(cache_key) if value.nil?
481
+
482
+ Rails.cache.write(cache_key, value, expires_in: 2.minutes)
483
+ value
484
+ end
485
+
470
486
  private
471
487
 
488
+ def normalize_email(value)
489
+ value.to_s.downcase.strip
490
+ end
491
+
492
+ # Spend the same bcrypt cost as a real check when the user does not exist.
493
+ def dummy_password_check(password)
494
+ BCrypt::Password.new(self.class.dummy_password_digest).is_password?(password.to_s)
495
+ false
496
+ end
497
+
498
+ # Same cost used by has_secure_password, so the dummy check takes as long as a real one.
499
+ def self.dummy_password_digest
500
+ @dummy_password_digest ||= BCrypt::Password.create(
501
+ SecureRandom.hex,
502
+ cost: ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
503
+ )
504
+ end
505
+
472
506
  def webauthn_user_handle
473
507
  Digest::SHA256.digest("lato-user-#{id}")
474
508
  end
@@ -13,6 +13,8 @@ user ||= Lato::User.new
13
13
  <%= lato_form_item_input_email form, :email, required: true %>
14
14
  </div>
15
15
 
16
+ <%= render 'lato/authentication/hcaptcha' %>
17
+
16
18
  <div>
17
19
  <%= lato_form_submit form, I18n.t('lato.next'), class: %w[d-block w-100] %>
18
20
  </div>
@@ -5,7 +5,7 @@ user ||= Lato::User.new
5
5
  %>
6
6
 
7
7
  <%= turbo_frame_tag 'authentication_form-update-password' do %>
8
- <%= form_with model: user, url: lato.authentication_update_password_action_path(id: user.id), data: { turbo_frame: '_self', controller: 'lato-form' } do |form| %>
8
+ <%= form_with model: user, url: lato.authentication_update_password_action_path, method: :patch, data: { turbo_frame: '_self', controller: 'lato-form' } do |form| %>
9
9
  <%= lato_form_errors user, class: %w[mb-3] %>
10
10
 
11
11
  <div class="mb-3">
@@ -116,6 +116,7 @@ en:
116
116
  authentication_controller:
117
117
  verify_email_action_notice: Successfully verified email address
118
118
  update_password_action_notice: Your password has been successfully updated
119
+ recover_password_action_notice: If the email is registered you will receive a verification code shortly
119
120
 
120
121
  activerecord:
121
122
  attributes:
@@ -138,7 +139,8 @@ en:
138
139
  email_verification_code_expired: Email verification code has expired
139
140
  email_verification_code_invalid: Email verification code is invalid
140
141
  password_update_code_expired: Verification code has expired
141
- password_update_code_invalid: Verification code is invalid
142
+ password_update_code_invalid: Verification code is invalid or expired
143
+ credentials_not_correct: Email or password not correct
142
144
  privacy_policy_invalid: To accept the privacy policy you must select the confirmation checkbox
143
145
  terms_and_conditions_invalid: To accept the terms and conditions you must select the confirmation checkbox
144
146
  web3_address_invalid: The address you send is not corretly signed
@@ -118,6 +118,7 @@ fr:
118
118
  authentication_controller:
119
119
  verify_email_action_notice: Adresse e-mail vérifiée avec succès
120
120
  update_password_action_notice: Votre mot de passe a été mis à jour avec succès
121
+ recover_password_action_notice: Si l'adresse e-mail est enregistrée, vous recevrez bientôt un code de vérification
121
122
 
122
123
  activerecord:
123
124
  attributes:
@@ -145,7 +146,8 @@ fr:
145
146
  email_verification_code_expired: Le code de vérification de l'e-mail a expiré
146
147
  email_verification_code_invalid: Le code de vérification de l'e-mail n'est pas valide
147
148
  password_update_code_expired: Le code de vérification a expiré
148
- password_update_code_invalid: Le code de vérification n'est pas valide
149
+ password_update_code_invalid: Le code de vérification n'est pas valide ou a expiré
150
+ credentials_not_correct: E-mail ou mot de passe incorrect
149
151
  privacy_policy_invalid: Pour accepter la politique de confidentialité, vous devez cocher la case de confirmation
150
152
  terms_and_conditions_invalid: Pour accepter les conditions générales, vous devez cocher la case de confirmation
151
153
  invitation_invalid: Invitation non valide
@@ -118,6 +118,7 @@ it:
118
118
  authentication_controller:
119
119
  verify_email_action_notice: Indirizzo email verificato correttamente
120
120
  update_password_action_notice: La tua password è stata aggiornata correttamente
121
+ recover_password_action_notice: Se l'email è registrata riceverai a breve un codice di verifica
121
122
 
122
123
  activerecord:
123
124
  attributes:
@@ -145,7 +146,8 @@ it:
145
146
  email_verification_code_expired: Il codice di verifica email risulta scaduto
146
147
  email_verification_code_invalid: Il codice di verifica email non risulta valido
147
148
  password_update_code_expired: Il codice di verifica risulta scaduto
148
- password_update_code_invalid: Il codice di verifica non risulta valido
149
+ password_update_code_invalid: Il codice di verifica non risulta valido o è scaduto
150
+ credentials_not_correct: Email o password non corretti
149
151
  privacy_policy_invalid: Per accettare la privacy policy devi selezionare la checkbox di conferma
150
152
  terms_and_conditions_invalid: Per accettare i termini e condizioni devi selezionare la checkbox di conferma
151
153
  invitation_invalid: Invito non valido
@@ -118,6 +118,7 @@ ro:
118
118
  authentication_controller:
119
119
  verify_email_action_notice: Adresa de email a fost verificată cu succes
120
120
  update_password_action_notice: Parola ta a fost actualizată cu succes
121
+ recover_password_action_notice: Dacă adresa de email este înregistrată, vei primi în curând un cod de verificare
121
122
 
122
123
  activerecord:
123
124
  attributes:
@@ -145,7 +146,8 @@ ro:
145
146
  email_verification_code_expired: Codul de verificare email a expirat
146
147
  email_verification_code_invalid: Codul de verificare email nu este valid
147
148
  password_update_code_expired: Codul de verificare a expirat
148
- password_update_code_invalid: Codul de verificare nu este valid
149
+ password_update_code_invalid: Codul de verificare nu este valid sau a expirat
150
+ credentials_not_correct: Email sau parolă incorectă
149
151
  privacy_policy_invalid: Pentru a accepta politica de confidențialitate trebuie să bifezi căsuța de confirmare
150
152
  terms_and_conditions_invalid: Pentru a accepta termenii și condițiile trebuie să bifezi căsuța de confirmare
151
153
  invitation_invalid: Invitație nevalidă
data/lib/lato/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Lato
2
- VERSION = "3.22.2"
2
+ VERSION = "3.23.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lato
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.22.2
4
+ version: 3.23.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gregorio Galante