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 +4 -4
- data/app/controllers/concerns/lato/sessionable.rb +7 -1
- data/app/controllers/lato/application_controller.rb +0 -8
- data/app/controllers/lato/authentication_controller.rb +24 -7
- data/app/models/lato/user.rb +62 -28
- data/app/views/lato/authentication/_form-recover-password.html.erb +2 -0
- data/app/views/lato/authentication/_form-update-password.html.erb +1 -1
- data/config/locales/en.yml +3 -1
- data/config/locales/fr.yml +3 -1
- data/config/locales/it.yml +3 -1
- data/config/locales/ro.yml +3 -1
- data/lib/lato/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 926e453e02a52408f23980d6ed7e0e74c40de3f4e5841a911420ea1b3df25128
|
|
4
|
+
data.tar.gz: 179a8b9e1959c8c734e92a17eec7c30e640da7f65df7b266e35f8f257d75094b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
152
|
-
|
|
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 @
|
|
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:
|
|
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
|
data/app/models/lato/user.rb
CHANGED
|
@@ -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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
errors.add(:email, :
|
|
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
|
-
|
|
234
|
-
|
|
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
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
252
|
-
|
|
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
|
|
@@ -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
|
|
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">
|
data/config/locales/en.yml
CHANGED
|
@@ -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
|
data/config/locales/fr.yml
CHANGED
|
@@ -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
|
data/config/locales/it.yml
CHANGED
|
@@ -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
|
data/config/locales/ro.yml
CHANGED
|
@@ -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