Merge pull request #10741 from demarches-simplifiees/secu-improve-complexity-password-ldu

[sécu] Améliorer la complexité des mots de passe pour tous les users
This commit is contained in:
Lisa Durand 2024-09-18 12:40:17 +00:00 committed by GitHub
commit 3d50f9363f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 6528 additions and 5216 deletions

View file

@ -111,7 +111,7 @@ gem 'webrick', require: false
gem 'yabeda-prometheus' gem 'yabeda-prometheus'
gem 'yabeda-sidekiq' gem 'yabeda-sidekiq'
gem 'zipline' gem 'zipline'
gem 'zxcvbn-ruby', require: 'zxcvbn' gem 'zxcvbn'
group :test do group :test do
gem 'axe-core-rspec' # accessibility rspec matchers gem 'axe-core-rspec' # accessibility rspec matchers

View file

@ -880,7 +880,7 @@ GEM
actionpack (>= 6.0, < 8.0) actionpack (>= 6.0, < 8.0)
content_disposition (~> 1.0) content_disposition (~> 1.0)
zip_tricks (>= 4.2.1, < 6.0) zip_tricks (>= 4.2.1, < 6.0)
zxcvbn-ruby (1.2.0) zxcvbn (0.1.11)
PLATFORMS PLATFORMS
ruby ruby
@ -1034,7 +1034,7 @@ DEPENDENCIES
yabeda-prometheus yabeda-prometheus
yabeda-sidekiq yabeda-sidekiq
zipline zipline
zxcvbn-ruby zxcvbn
BUNDLED WITH BUNDLED WITH
2.5.9 2.5.9

View file

@ -11,13 +11,16 @@
.fr-fieldset__element .fr-fieldset__element
%p.fr-text--sm= t('utils.mandatory_champs') %p.fr-text--sm= t('utils.mandatory_champs')
.fr-fieldset__element= render Dsfr::InputComponent.new(form: f, attribute: :email, input_type: :email_field, opts: { disabled: :disabled, class: 'fr-input-group--disabled', value: t('.email_disabled') }) .fr-fieldset__element
= render Dsfr::InputComponent.new(form: f, attribute: :email, input_type: :email_field, opts: { disabled: :disabled, class: 'fr-input-group--disabled' })
.fr-fieldset__element= render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field, opts: { autocomplete: 'current-password' }) .fr-fieldset__element
= render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field,
opts: { autofocus: 'true', autocomplete: 'new-password', data: { controller: 'turbo-input', turbo_input_url_value: show_password_complexity_path }})
#password_complexity
= render PasswordComplexityComponent.new
= f.hidden_field :reset_password_token, value: params[:token] = f.hidden_field :reset_password_token, value: params[:token]
.fr-fieldset__element = f.submit t('.submit'), id: 'submit-password', disabled: :disabled, class: "fr-btn fr-btn--lg fr-mt-2w", data: { disabled: :disabled, disable_with: t('views.users.passwords.edit.submit_loading') }
.fr-btns-group--right.fr-btns-group.fr-btns-group--inline.fr-btns-group.fr-btns-group--inline
%ul
%li= f.submit t('.submit'), class: 'fr-mt-2v fr-btn fr-btn'

View file

@ -7,4 +7,4 @@ en:
strong: Congratulations! Password is strong and secure enough. strong: Congratulations! Password is strong and secure enough.
weak: Vulnerable password. weak: Vulnerable password.
weakest: Very vulnerable password. weakest: Very vulnerable password.
hint: A short sentence with punctuation can be a very secure password. hint_html: <p>A short sentence with punctuation can be a very secure password.</p>

View file

@ -7,4 +7,4 @@ fr:
strong: Félicitations ! Mot de passe suffisamment fort et sécurisé. strong: Félicitations ! Mot de passe suffisamment fort et sécurisé.
weak: Mot de passe vulnérable. weak: Mot de passe vulnérable.
weakest: Mot de passe très vulnérable. weakest: Mot de passe très vulnérable.
hint: Une courte phrase avec ponctuation peut être un mot de passe très sécurisé. hint_html: <p>Pour un mot de passe sécurisé, éviter d'utiliser des suites ou des répétitions de mêmes caractères.</p><p>Vous pouvez par exemple choisir une phrase (avec des espaces) que vous retiendrez facilement.</p>

View file

@ -3,4 +3,4 @@
%div{ class: alert_classes } %div{ class: alert_classes }
%h3.fr-alert__title= title %h3.fr-alert__title= title
- if !success? - if !success?
%p= t(".hint") = t(".hint_html")

View file

@ -2,7 +2,8 @@
class PasswordComplexityController < ApplicationController class PasswordComplexityController < ApplicationController
def show def show
@score, @words, @length = ZxcvbnService.new(password_param).complexity @length = password_param.to_s.length
@score = ZxcvbnService.complexity(password_param)
@min_length = PASSWORD_MIN_LENGTH @min_length = PASSWORD_MIN_LENGTH
@min_complexity = PASSWORD_COMPLEXITY_FOR_ADMIN @min_complexity = PASSWORD_COMPLEXITY_FOR_ADMIN
end end

View file

@ -44,10 +44,6 @@ class User < ApplicationRecord
# plug our custom validation a la devise (same options) https://github.com/heartcombo/devise/blob/main/lib/devise/models/validatable.rb#L30 # plug our custom validation a la devise (same options) https://github.com/heartcombo/devise/blob/main/lib/devise/models/validatable.rb#L30
validates :email, strict_email: true, allow_blank: true, if: :devise_will_save_change_to_email? validates :email, strict_email: true, allow_blank: true, if: :devise_will_save_change_to_email?
def validate_password_complexity?
administrateur?
end
# Override of Devise::Models::Confirmable#send_confirmation_instructions # Override of Devise::Models::Confirmable#send_confirmation_instructions
def send_confirmation_instructions def send_confirmation_instructions
unless @raw_confirmation_token unless @raw_confirmation_token

View file

@ -3,51 +3,16 @@
class ZxcvbnService class ZxcvbnService
@tester_mutex = Mutex.new @tester_mutex = Mutex.new
class << self # Returns an Zxcvbn instance cached between classes instances and between threads.
# Returns an Zxcvbn instance cached between classes instances and between threads. #
# # The tester weights ~20 Mo, and we'd like to save some memory so rather
# The tester weights ~20 Mo, and we'd like to save some memory so rather # that storing it in a per-thread accessor, we prefer to use a mutex
# that storing it in a per-thread accessor, we prefer to use a mutex # to cache it between threads.
# to cache it between threads. def self.tester
def tester @tester_mutex.synchronize do
@tester_mutex.synchronize do @tester ||= Zxcvbn::Tester.new
@tester ||= build_tester
end
end
private
# Returns a fully initializer tester from the on-disk dictionary.
#
# This is slow: loading and parsing the dictionary may take around 1s.
def build_tester
dictionaries = YAML.safe_load(Rails.root.join("config", "initializers", "zxcvbn_dictionnaries.yaml").read)
tester = Zxcvbn::Tester.new
tester.add_word_lists(dictionaries)
tester
end end
end end
def initialize(password) def self.complexity(password)= tester.test(password.to_s).score
@password = password
end
def complexity
wxcvbn = compute_zxcvbn
score = wxcvbn.score
length = @password.blank? ? 0 : @password.length
vulnerabilities = wxcvbn.match_sequence.map { |m| m.matched_word.nil? ? m.token : m.matched_word }.filter { |s| s.length > 2 }.join(', ')
[score, vulnerabilities, length]
end
def score
compute_zxcvbn.score
end
private
def compute_zxcvbn
self.class.tester.test(@password)
end
end end

View file

@ -2,7 +2,7 @@
class PasswordComplexityValidator < ActiveModel::EachValidator class PasswordComplexityValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
if value.present? && ZxcvbnService.new(value).score < PASSWORD_COMPLEXITY_FOR_ADMIN if value.present? && ZxcvbnService.complexity(value) < PASSWORD_COMPLEXITY_FOR_ADMIN
record.errors.add(attribute, :not_strong) record.errors.add(attribute, :not_strong)
end end
end end

View file

@ -23,4 +23,4 @@
#password_complexity #password_complexity
= render PasswordComplexityComponent.new = render PasswordComplexityComponent.new
= f.submit t('.continue'), id: 'submit-password', class: "fr-btn fr-btn--lg fr-mt-2w", data: { disable_with: t('views.users.passwords.edit.submit_loading') } = f.submit t('.continue'), id: 'submit-password', disabled: :disabled, class: "fr-btn fr-btn--lg fr-mt-2w", data: { disable_with: t('views.users.passwords.edit.submit_loading') }

View file

@ -1,3 +0,0 @@
.fr-messages-group{ "aria-live" => "off", id: id }
%p.fr-message= t('views.registrations.new.password_message')
%p.fr-message.fr-message--info= t('views.registrations.new.password_placeholder', min_length: PASSWORD_MIN_LENGTH)

View file

@ -18,16 +18,13 @@
.fr-fieldset__element .fr-fieldset__element
= render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field, = render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field,
opts: { autofocus: 'true', autocomplete: 'new-password', minlength: PASSWORD_MIN_LENGTH, data: { controller: populated_resource.validate_password_complexity? ? 'turbo-input' : false, turbo_input_url_value: show_password_complexity_path }}) do |c| opts: { autofocus: 'true', autocomplete: 'new-password', minlength: PASSWORD_MIN_LENGTH, data: { controller: 'turbo-input', turbo_input_url_value: show_password_complexity_path }}) do |c|
- c.with_describedby do - c.with_describedby do
- if populated_resource.validate_password_complexity? %div{ id: c.describedby_id }
%div{ id: c.describedby_id } #password_complexity
#password_complexity = render PasswordComplexityComponent.new
= render PasswordComplexityComponent.new
- else
= render partial: "devise/password_rules", locals: { id: c.describedby_id }
.fr-fieldset__element .fr-fieldset__element
= render Dsfr::InputComponent.new(form: f, attribute: :password_confirmation, input_type: :password_field, opts: { autocomplete: 'new-password' }) = render Dsfr::InputComponent.new(form: f, attribute: :password_confirmation, input_type: :password_field, opts: { autocomplete: 'new-password' })
= f.submit t('views.users.passwords.edit.submit'), id: 'submit-password', class: "fr-btn fr-btn--lg fr-mt-2w", data: { disable_with: t('views.users.passwords.edit.submit_loading') } = f.submit t('views.users.passwords.edit.submit'), id: 'submit-password', disabled: :disabled, class: "fr-btn fr-btn--lg fr-mt-2w", data: { disable_with: t('views.users.passwords.edit.submit_loading') }

View file

@ -1,20 +1,22 @@
.two-columns.avis-sign-up .fr-container.fr-my-5w
.columns-container .fr-grid-row.fr-grid-row--center
.column.left .fr-col-lg-6
%h2.fr-py-5w.text-center= @dossier.procedure.libelle
%p.dossier Dossier nº #{@dossier.id}
.column
= form_for(User.new(email: @email), url: sign_up_expert_avis_path(email: @email), method: :post, html: { class: "fr-py-5w" }) do |f| = form_for(User.new(email: @email), url: sign_up_expert_avis_path(email: @email), method: :post, html: { class: "fr-py-5w" }) do |f|
%h1.fr-h2= t('views.registrations.new.title', name: Current.application_name)
%h1.fr-h2
= t('views.registrations.new.title', name: Current.application_name)
%fieldset.fr-mb-0.fr-fieldset{ aria: { labelledby: 'create-account-legend' } } %fieldset.fr-mb-0.fr-fieldset{ aria: { labelledby: 'create-account-legend' } }
.fr-fieldset__element .fr-fieldset__element
%p.fr-text--sm= t('utils.mandatory_champs') %p.fr-text--sm= t('utils.mandatory_champs')
.fr-fieldset__element= render Dsfr::InputComponent.new(form: f, attribute: :email, input_type: :email_field, opts: { disabled: true, autocomplete: 'email' })
.fr-fieldset__element .fr-fieldset__element
= render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field, opts: { autocomplete: 'new-password', minlength: PASSWORD_MIN_LENGTH }) do |c| = render Dsfr::InputComponent.new(form: f, attribute: :email, input_type: :email_field, opts: { disabled: true, autocomplete: 'email' })
- c.with_describedby do
= render partial: "devise/password_rules", locals: { id: c.describedby_id }
%ul.fr-btns-group .fr-fieldset__element
%li= f.submit t('views.shared.account.create'), class: "fr-btn" = render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field,
opts: { autofocus: 'true', autocomplete: 'new-password', data: { controller: 'turbo-input', turbo_input_url_value: show_password_complexity_path }})
#password_complexity
= render PasswordComplexityComponent.new
= f.submit t('views.shared.account.create'), id: 'submit-password', disabled: :disabled, class: "fr-btn fr-btn--lg fr-mt-2w", data: { disable_with: t('views.users.passwords.edit.submit_loading') }

View file

@ -23,4 +23,4 @@
#password_complexity #password_complexity
= render PasswordComplexityComponent.new = render PasswordComplexityComponent.new
= f.submit t('.continue'), id: 'submit-password', class: "fr-btn fr-btn--lg fr-mt-2w", data: { disable_with: t('views.users.passwords.edit.submit_loading') } = f.submit t('.continue'), id: 'submit-password', disabled: :disabled, class: "fr-btn fr-btn--lg fr-mt-2w", data: { disable_with: t('views.users.passwords.edit.submit_loading') }

View file

@ -1,29 +0,0 @@
= devise_error_messages!
#form-login
%h2#instructeur_login Changement de mot de passe
%br
%br
#new-user
= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f|
= f.hidden_field :reset_password_token
%h4
= f.label 'Nouveau mot de passe'
.input-group
.input-group-addon
%span.fa.fa-asterisk
= f.password_field :password, autofocus: true, autocomplete: "off", class: 'form-control'
%br
%h4
= f.label 'Confirmez le nouveau mot de passe'
.input-group
.input-group-addon
%span.fa.fa-asterisk
= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control'
%br
%br
.actions
= f.submit 'Changer le mot de passe', class: 'btn btn-primary'
%br

View file

@ -1,21 +0,0 @@
= devise_error_messages!
%br
#form-login
%h2#instructeur_login Mot de passe oublié
%br
%br
#new-user
= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f|
%h4
= f.label :email
.input-group
.input-group-addon
%span.fa.fa-user
= f.email_field :email, class: 'form-control', placeholder: 'Email'
%br
%br
.actions
= f.submit 'Demander un nouveau mot de passe', class: 'button large expand primary'
%br

View file

@ -18,9 +18,10 @@
.fr-fieldset__element= render Dsfr::InputComponent.new(form: f, attribute: :email, input_type: :email_field, opts: { autocomplete: 'email', autofocus: true }) .fr-fieldset__element= render Dsfr::InputComponent.new(form: f, attribute: :email, input_type: :email_field, opts: { autocomplete: 'email', autofocus: true })
.fr-fieldset__element .fr-fieldset__element
= render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field, opts: { autocomplete: 'new-password', minlength: PASSWORD_MIN_LENGTH }) do |c| = render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field,
- c.with_describedby do opts: { autofocus: 'true', autocomplete: 'new-password', data: { controller: 'turbo-input', turbo_input_url_value: show_password_complexity_path }})
= render partial: "devise/password_rules", locals: { id: c.describedby_id }
%ul.fr-btns-group #password_complexity
%li= f.submit t('views.shared.account.create'), class: "fr-btn" = render PasswordComplexityComponent.new
= f.submit t('views.shared.account.create'), id: 'submit-password', disabled: :disabled, class: "fr-btn fr-btn--lg fr-mt-2w", data: { disable_with: t('views.users.passwords.edit.submit_loading') }

View file

@ -7,5 +7,5 @@ if !defined?(PASSWORD_MIN_LENGTH)
# PASSWORD_COMPLEXITY_FOR_INSTRUCTEUR = ENV.fetch('PASSWORD_COMPLEXITY_FOR_INSTRUCTEUR', '3').to_i # PASSWORD_COMPLEXITY_FOR_INSTRUCTEUR = ENV.fetch('PASSWORD_COMPLEXITY_FOR_INSTRUCTEUR', '3').to_i
PASSWORD_COMPLEXITY_FOR_ADMIN = ENV.fetch('PASSWORD_COMPLEXITY_FOR_ADMIN', '4').to_i PASSWORD_COMPLEXITY_FOR_ADMIN = ENV.fetch('PASSWORD_COMPLEXITY_FOR_ADMIN', '4').to_i
# password minimum length # password minimum length
PASSWORD_MIN_LENGTH = ENV.fetch('PASSWORD_MIN_LENGTH', '8').to_i PASSWORD_MIN_LENGTH = ENV.fetch('PASSWORD_MIN_LENGTH', '12').to_i
end end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
new_frequency_lists = ['words_fr', 'passwords_fr', 'surnames_fr', 'female_names_fr', 'male_names_fr'].index_with do |n|
Zxcvbn.file_enumerator(Rails.root.join("config/zxcvbn_frequency_lists/#{n}.txt"))
end
new_ranked_dictionary = new_frequency_lists.transform_values do |lst|
Zxcvbn::Matching.build_ranked_dict(lst)
end
Zxcvbn::Matching::RANKED_DICTIONARIES.merge! new_ranked_dictionary

File diff suppressed because it is too large Load diff

View file

@ -311,8 +311,6 @@ en:
subtitle: "Create an account using an email" subtitle: "Create an account using an email"
email_label: 'Email (name@site.com)' email_label: 'Email (name@site.com)'
password_label: "Password (%{min_length} characters minimum)" password_label: "Password (%{min_length} characters minimum)"
password_message: "Your password must have :"
password_placeholder: "%{min_length} characters minimum"
confirmation: confirmation:
new: new:
title: 'Confirm your email address' title: 'Confirm your email address'
@ -661,7 +659,7 @@ en:
invalid: 'is invalid. Fill in a valid email address, example: address@mail.com' invalid: 'is invalid. Fill in a valid email address, example: address@mail.com'
taken: 'already in use. Fill in another email' taken: 'already in use. Fill in another email'
password: password:
too_short: "is too short. Fill in a password with at least 8 characters" too_short: "is too short. Fill in a password with at least 12 characters"
not_strong: "not strong enough. Fill in a stronger password" not_strong: "not strong enough. Fill in a stronger password"
password_confirmation: password_confirmation:
confirmation: ": The two passwords do not match" confirmation: ": The two passwords do not match"

View file

@ -303,8 +303,6 @@ fr:
subtitle: "Se créer un compte avec une adresse email" subtitle: "Se créer un compte avec une adresse email"
email_label: 'Email' email_label: 'Email'
password_label: "Mot de passe (%{min_length} caractères minimum)" password_label: "Mot de passe (%{min_length} caractères minimum)"
password_message: "Votre mot de passe doit contenir :"
password_placeholder: "%{min_length} caractères minimum"
confirmation: confirmation:
new: new:
title: 'Confirmez votre adresse email' title: 'Confirmez votre adresse email'
@ -662,7 +660,7 @@ fr:
invalid: 'est invalide. Saisir une adresse électronique valide, exemple : adresse@mail.com' invalid: 'est invalide. Saisir une adresse électronique valide, exemple : adresse@mail.com'
taken: 'déjà utilisé. Saisir une autre adresse électronique.' taken: 'déjà utilisé. Saisir une autre adresse électronique.'
password: password:
too_short: "est trop court. Saisir un mot de passe avec au moins 8 caractères" too_short: "est trop court. Saisir un mot de passe avec au moins 12 caractères"
not_strong: "nest pas assez complexe. Saisir un mot de passe plus complexe" not_strong: "nest pas assez complexe. Saisir un mot de passe plus complexe"
password_confirmation: password_confirmation:
confirmation: ': Les deux mots de passe ne correspondent pas' confirmation: ': Les deux mots de passe ne correspondent pas'

View file

@ -0,0 +1,100 @@
Marie
Julie
Camille
Emilie
Aurélie
Léa
Manon
Elodie
Laura
Sarah
Chloé
Pauline
Anaïs
Céline
Audrey
Marine
Marion
Mélanie
Emma
Lucie
Mathilde
Charlotte
Amandine
Stéphanie
Sophie
Laetitia
Justine
Clara
Océane
Caroline
Inès
Claire
Amélie
Virginie
Morgane
Sabrina
Jessica
Fanny
Jade
Juliette
Mélissa
Jennifer
Eva
Vanessa
Cindy
Lisa
Louise
Alexandra
Clémence
Alice
Lola
Aurore
Cécile
Elise
Delphine
Noemie
Margaux
Coralie
Hélène
Célia
Maeva
Angelique
Romane
Sandra
Estelle
Adeline
Alicia
Zoé
Sandrine
Jeanne
Laure
Elisa
Christell
Anne
Léna
Nathalie
Margot
Julia
Ludivine
Ophélie
Sonia
Elsa
Agathe
Myriam
Emmanuelle
Lilou
Alexia
Charlène
Emeline
Marina
Ambre
Gaelle
Lina
Anna
Lou
Isabelle
Solène
Laurie
Nina
Maelys

View file

@ -0,0 +1,100 @@
Nicolas
Julien
Thomas
Alexandre
Maxime
Romain
Guillaume
Anthony
Kevin
Antoine
Lucas
Sébastien
Clément
Benjamin
Pierre
Mathieu
Quentin
Florian
Vincent
Alexis
David
Hugo
Jeremy
Théo
Jonathan
Damien
Adrien
Enzo
Valentin
Louis
Nathan
Paul
Baptiste
Mickael
Cedric
Raphaël
Arthur
Christophe
Loïc
Aurélien
Léo
Arnaud
Matthieu
Fabien
Tom
Mathis
Dylan
Axel
Ludovic
Jerome
Benoît
Simon
Gabriel
Frédéric
Olivier
Rémi
Samuel
Jules
Stéphane
Sylvain
Mohamed
Jean
Victor
Jordan
François
Corentin
Gregory
Cyril
Bastien
Florent
Yanis
Thibault
Maxence
Yann
Laurent
Michael
Mathéo
Martin
Gaëtan
Mehdi
Robin
William
Christopher
Ethan
Noah
Charles
Emmanuel
Xavier
Adam
Tristan
Yoann
Tony
Marc
Dimitri
Thibaut
Rémy
Evan
Steven
Dorian
Franck

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,200 @@
Martin
Bernard
Thomas
Petit
Robert
Richard
Dubois
Durand
Moreau
Laurent
Simon
Michel
Lefebvre
Leroy
David
Roux
Morel
Bertrand
Fournier
Girard
Fontaine
Lambert
Dupont
Bonnet
Rousseau
Vincent
Muller
Lefevre
Faure
Andre
Mercier
Guerin
Garcia
Boyer
Blanc
Garnier
Chevalier
Francois
Legrand
Gauthier
Perrin
Robin
Clement
Morin
Henry
Nicolas
Roussel
Gautier
Mathieu
Masson
Duval
Marchand
Denis
Lemaire
Dumont
Marie
Noel
Meyer
Dufour
Meunier
Martinez
Blanchard
Brun
Riviere
Lucas
Joly
Giraud
Brunet
Gaillard
Barbier
Gerard
Arnaud
Renard
Roche
Schmitt
Roy
Leroux
Caron
Colin
Vidal
Picard
Roger
Fabre
Aubert
Lemoine
Renaud
Dumas
Payet
Olivier
Lacroix
Philippe
Pierre
Bourgeois
Lopez
Benoit
Leclerc
Rey
Leclercq
Sanchez
Lecomte
Rolland
Guillaume
Jean
Hubert
Dupuy
Carpentier
Guillot
Berger
Perez
Dupuis
Louis
Moulin
Deschamps
Vasseur
Huet
Boucher
Fernandez
Fleury
Adam
Royer
Paris
Jacquet
Klein
Poirier
Charles
Aubry
Guyot
Carre
Renault
Menard
Maillard
Charpentier
Marty
Bertin
Baron
Da Silva
Bailly
Herve
Schneider
Le Gall
Collet
Leger
Bouvier
Julien
Prevost
Millet
Le Roux
Daniel
Perrot
Cousin
Germain
Breton
Rodriguez
Langlois
Remy
Besson
Leveque
Le Goff
Pelletier
Leblanc
Barre
Lebrun
Grondin
Perrier
Marchal
Weber
Boulanger
Mallet
Hamon
Jacob
Monnier
Michaud
Guichard
Poulain
Etienne
Gillet
Hoarau
Tessier
Chevallier
Collin
Lemaitre
Benard
Chauvin
Bouchet
Marechal
Gay
Humbert
Gonzalez
Antoine
Perret
Reynaud
Cordier
Lejeune
Barthelemy
Delaunay
Carlier
Pichon
Pasquier
Lamy
Gilbert

File diff suppressed because it is too large Load diff

View file

@ -37,7 +37,7 @@ describe Administrateurs::ActivateController, type: :controller do
end end
context 'when the password is not strong' do context 'when the password is not strong' do
let(:password) { 'another-password-ok?' } let(:password) { 'password-ok?' }
it { expect(administrateur.user.reload.valid_password?(password)).to be false } it { expect(administrateur.user.reload.valid_password?(password)).to be false }
it { expect(response).to redirect_to(admin_activate_path(token: token)) } it { expect(response).to redirect_to(admin_activate_path(token: token)) }

View file

@ -61,7 +61,7 @@ end
RSpec.describe "CSRF cleanup", type: :request do RSpec.describe "CSRF cleanup", type: :request do
describe 'csrf_cleaner hook', :allow_forgery_protection do describe 'csrf_cleaner hook', :allow_forgery_protection do
let(:user) { create(:user, password: password) } let(:user) { create(:user, password: password) }
let(:password) { 'my-very-secure-password' } let(:password) { SECURE_PASSWORD }
it 'refreshes the long-lived cookie after authentication' do it 'refreshes the long-lived cookie after authentication' do
get new_user_session_path get new_user_session_path

View file

@ -598,7 +598,7 @@ describe Experts::AvisController, type: :controller do
context 'with a random avis, procedure and user' do context 'with a random avis, procedure and user' do
let(:avis_id) { create(:avis).id } let(:avis_id) { create(:avis).id }
let(:random_user) { create(:user) } let(:random_user) { create(:user, password: '{Another-$3cure-p4ssWord}') }
let(:email) { random_user.email } let(:email) { random_user.email }
it 'doesnt change the random user password' do it 'doesnt change the random user password' do
@ -613,7 +613,7 @@ describe Experts::AvisController, type: :controller do
let(:avis) { create(:avis) } let(:avis) { create(:avis) }
let(:avis_id) { avis.id } let(:avis_id) { avis.id }
let(:procedure_id) { avis.procedure.id } let(:procedure_id) { avis.procedure.id }
let(:random_user) { create(:user) } let(:random_user) { create(:user, password: '{Another-$3cure-p4ssWord}') }
let(:email) { random_user.email } let(:email) { random_user.email }
it 'doesnt change the random user password' do it 'doesnt change the random user password' do
@ -629,7 +629,7 @@ describe Experts::AvisController, type: :controller do
it 'doesnt change the expert password' do it 'doesnt change the expert password' do
subject subject
expect(expert.user.reload.valid_password?(SECURE_PASSWORD)).to be false expect(expert.user.reload.valid_password?('{Another-$3cure-p4ssWord}')).to be false
end end
it { is_expected.to redirect_to new_user_session_url } it { is_expected.to redirect_to new_user_session_url }

View file

@ -394,7 +394,7 @@ describe FranceConnect::ParticulierController, type: :controller do
fci.update!(requested_email: email.downcase) fci.update!(requested_email: email.downcase)
end end
let!(:user) { create(:user, email:, password: 'abcdefgh') } let!(:user) { create(:user, email:, password: SECURE_PASSWORD) }
it 'merges the account, signs in, and delete the merge token' do it 'merges the account, signs in, and delete the merge token' do
subject subject
@ -408,7 +408,7 @@ describe FranceConnect::ParticulierController, type: :controller do
end end
context 'but the targeted user is an instructeur' do context 'but the targeted user is an instructeur' do
let!(:user) { create(:instructeur, email: email, password: 'abcdefgh').user } let!(:user) { create(:instructeur, email: email, password: SECURE_PASSWORD).user }
it 'redirects to the new session' do it 'redirects to the new session' do
subject subject

View file

@ -23,7 +23,7 @@ describe Gestionnaires::ActivateController, type: :controller do
describe '#create' do describe '#create' do
let!(:gestionnaire) { create(:gestionnaire) } let!(:gestionnaire) { create(:gestionnaire) }
let(:token) { gestionnaire.user.send(:set_reset_password_token) } let(:token) { gestionnaire.user.send(:set_reset_password_token) }
let(:password) { 'another-password-ok?' } let(:password) { '{another-password-ok?}' }
before { post :create, params: { gestionnaire: { reset_password_token: token, password: password } } } before { post :create, params: { gestionnaire: { reset_password_token: token, password: password } } }

View file

@ -3,7 +3,7 @@
describe PasswordComplexityController, type: :controller do describe PasswordComplexityController, type: :controller do
describe '#show' do describe '#show' do
let(:params) do let(:params) do
{ user: { password: 'moderately complex password' } } { user: { password: 'motDePasseTropFacile' } }
end end
subject { get :show, format: :turbo_stream, params: params } subject { get :show, format: :turbo_stream, params: params }
@ -15,7 +15,7 @@ describe PasswordComplexityController, type: :controller do
context 'with a different resource name' do context 'with a different resource name' do
let(:params) do let(:params) do
{ super_admin: { password: 'moderately complex password' } } { super_admin: { password: 'motDePasseTropFacile' } }
end end
it 'computes a password score' do it 'computes a password score' do

View file

@ -23,7 +23,7 @@ describe Users::ActivateController, type: :controller do
describe '#create' do describe '#create' do
let!(:user) { create(:user) } let!(:user) { create(:user) }
let(:token) { user.send(:set_reset_password_token) } let(:token) { user.send(:set_reset_password_token) }
let(:password) { 'another-password-ok?' } let(:password) { '{another-password-ok?}' }
before { post :create, params: { user: { reset_password_token: token, password: password } } } before { post :create, params: { user: { reset_password_token: token, password: password } } }

View file

@ -8,7 +8,7 @@ FactoryBot.define do
transient do transient do
email { generate(:expert_email) } email { generate(:expert_email) }
password { 'somethingverycomplated!' } password { '{My-$3cure-p4ssWord}' }
end end
end end
end end

View file

@ -8,7 +8,7 @@ FactoryBot.define do
transient do transient do
email { generate(:gestionnaire_email) } email { generate(:gestionnaire_email) }
password { 'somethingverycomplated!' } password { '{My-$3cure-p4ssWord}' }
end end
end end
end end

View file

@ -10,7 +10,7 @@ FactoryBot.define do
transient do transient do
email { generate(:instructeur_email) } email { generate(:instructeur_email) }
password { '{my-%s3cure[]-p4$$w0rd' } password { '{My-$3cure-p4ssWord}' }
end end
trait :email_verified do trait :email_verified do

View file

@ -74,7 +74,7 @@ describe SuperAdmin, type: :model do
# 2 - somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8) # 2 - somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8)
# 3 - safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10) # 3 - safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10)
# 4 - very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10) # 4 - very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10)
passwords = ['password', '12pass23', 'démarches ', 'démarches-simple', '{My-$3cure-p4ssWord}'] passwords = ['000000000000', '123456789123', 'megapass2024', 'lesdémarches', '{My-$3cure-p4ssWord}']
min_complexity = PASSWORD_COMPLEXITY_FOR_ADMIN min_complexity = PASSWORD_COMPLEXITY_FOR_ADMIN
let(:email) { 'mail@beta.gouv.fr' } let(:email) { 'mail@beta.gouv.fr' }
@ -89,7 +89,7 @@ describe SuperAdmin, type: :model do
let(:password) { 's' * (PASSWORD_MIN_LENGTH - 1) } let(:password) { 's' * (PASSWORD_MIN_LENGTH - 1) }
it 'reports an error about password length (but not about complexity)' do it 'reports an error about password length (but not about complexity)' do
expect(subject).to eq(["Le champ « Mot de passe » est trop court. Saisir un mot de passe avec au moins 8 caractères"]) expect(subject).to eq(["Le champ « Mot de passe » est trop court. Saisir un mot de passe avec au moins 12 caractères"])
end end
end end

View file

@ -103,7 +103,7 @@ describe User, type: :model do
describe '.create_or_promote_to_instructeur' do describe '.create_or_promote_to_instructeur' do
let(:email) { 'inst1@gmail.com' } let(:email) { 'inst1@gmail.com' }
let(:password) { 'un super password !' } let(:password) { SECURE_PASSWORD }
let(:admins) { [] } let(:admins) { [] }
subject { User.create_or_promote_to_instructeur(email, password, administrateurs: admins) } subject { User.create_or_promote_to_instructeur(email, password, administrateurs: admins) }
@ -390,7 +390,7 @@ describe User, type: :model do
# 2 - somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8) # 2 - somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8)
# 3 - safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10) # 3 - safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10)
# 4 - very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10) # 4 - very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10)
passwords = ['password', '12pass23', 'démarches ', 'démarches-simple', '{My-$3cure-p4ssWord}'] passwords = ['000000000000', '123456789123', '123456789 123', 'lesdémarches', '{My-$3cure-p4ssWord}']
min_complexity = PASSWORD_COMPLEXITY_FOR_ADMIN min_complexity = PASSWORD_COMPLEXITY_FOR_ADMIN
subject do subject do
@ -405,7 +405,7 @@ describe User, type: :model do
let(:password) { 's' * (PASSWORD_MIN_LENGTH - 1) } let(:password) { 's' * (PASSWORD_MIN_LENGTH - 1) }
it 'reports an error about password length (but not about complexity)' do it 'reports an error about password length (but not about complexity)' do
expect(subject).to eq(["Le champ « Mot de passe » est trop court. Saisir un mot de passe avec au moins 8 caractères"]) expect(subject).to eq(["Le champ « Mot de passe » est trop court. Saisir un mot de passe avec au moins 12 caractères"])
end end
end end
@ -431,16 +431,19 @@ describe User, type: :model do
let(:password) { 's' * (PASSWORD_MIN_LENGTH - 1) } let(:password) { 's' * (PASSWORD_MIN_LENGTH - 1) }
it 'reports an error about password length (but not about complexity)' do it 'reports an error about password length (but not about complexity)' do
expect(subject).to eq(["Le champ « Mot de passe » est trop court. Saisir un mot de passe avec au moins 8 caractères"]) expect(subject).to eq(["Le champ « Mot de passe » est trop court. Saisir un mot de passe avec au moins 12 caractères"])
end end
end end
context 'when the password is long enough, but simple' do context 'when the password is long enough, but simple' do
let(:password) { 'simple-password' } let(:password) { 'simple-password' }
it { expect(subject).to eq(["Le champ « Mot de passe » nest pas assez complexe. Saisir un mot de passe plus complexe"]) }
end
it 'doesnt enforce the password complexity' do context 'when the password is long and complex' do
expect(subject).to be_empty let(:password) { passwords[min_complexity] }
end
it { expect(subject).to be_empty }
end end
end end
end end

View file

@ -1,18 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
describe ZxcvbnService do describe ZxcvbnService do
let(:password) { 'medium-strength-password' } describe '.complexity' do
subject(:service) { ZxcvbnService.new(password) }
describe '#score' do
it 'returns the password complexity score' do it 'returns the password complexity score' do
expect(service.score).to eq 3 expect(ZxcvbnService.complexity(nil)).to eq 0
end expect(ZxcvbnService.complexity('motdepassefrançais')).to eq 1
end expect(ZxcvbnService.complexity(SECURE_PASSWORD)).to eq 4
describe '#complexity' do
it 'returns the password score, vulnerability and length' do
expect(service.complexity).to eq [3, 'medium, strength, password', 24]
end end
end end
@ -21,12 +14,8 @@ describe ZxcvbnService do
allow(Zxcvbn::Tester).to receive(:new).and_call_original allow(Zxcvbn::Tester).to receive(:new).and_call_original
allow(YAML).to receive(:safe_load).and_call_original allow(YAML).to receive(:safe_load).and_call_original
first_service = ZxcvbnService.new('some-password') _first_call = ZxcvbnService.complexity('some-password')
first_service.score _other_call = ZxcvbnService.complexity('other-password')
first_service.complexity
other_service = ZxcvbnService.new('other-password')
other_service.score
other_service.complexity
expect(Zxcvbn::Tester).to have_received(:new).at_most(:once) expect(Zxcvbn::Tester).to have_received(:new).at_most(:once)
expect(YAML).to have_received(:safe_load).at_most(:once) expect(YAML).to have_received(:safe_load).at_most(:once)
@ -37,12 +26,12 @@ describe ZxcvbnService do
threads = 1.upto(4).map do threads = 1.upto(4).map do
Thread.new do Thread.new do
ZxcvbnService.new(password).score ZxcvbnService.complexity(SECURE_PASSWORD)
end end
end.map(&:join) end.map(&:join)
scores = threads.map(&:value) complexities = threads.map(&:value)
expect(scores).to eq([3, 3, 3, 3]) expect(complexities).to eq([4, 4, 4, 4])
expect(Zxcvbn::Tester).to have_received(:new).at_most(:once) expect(Zxcvbn::Tester).to have_received(:new).at_most(:once)
end end
end end

View file

@ -24,7 +24,7 @@ require 'simplecov' if ENV["CI"] || ENV["COVERAGE"] # see config in .simplecov f
require 'rspec/retry' require 'rspec/retry'
SECURE_PASSWORD = 'my-s3cure-p4ssword' SECURE_PASSWORD = '{My-$3cure-p4ssWord}'
RSpec.configure do |config| RSpec.configure do |config|
config.filter_run_excluding disable: true config.filter_run_excluding disable: true

View file

@ -61,7 +61,7 @@ module SystemHelpers
confirmation_email = open_email(email) confirmation_email = open_email(email)
procedure_sign_in_link = confirmation_email.body.match(/href="([^"]*\/commencer\/[^"]*)"/)[1] procedure_sign_in_link = confirmation_email.body.match(/href="([^"]*\/commencer\/[^"]*)"/)[1]
visit procedure_sign_in_link visit URI.parse(procedure_sign_in_link).path
end end
def click_reset_password_link_for(email) def click_reset_password_link_for(email)

View file

@ -2,7 +2,7 @@
describe 'wcag rules for usager', chrome: true do describe 'wcag rules for usager', chrome: true do
let(:procedure) { create(:procedure, :published, :with_service, :for_individual) } let(:procedure) { create(:procedure, :published, :with_service, :for_individual) }
let(:password) { 'a very complicated password' } let(:password) { SECURE_PASSWORD }
let(:litteraire_user) { create(:user, password: password) } let(:litteraire_user) { create(:user, password: password) }
def test_external_links_have_title_says_it_opens_in_a_new_tab def test_external_links_have_title_says_it_opens_in_a_new_tab

View file

@ -4,7 +4,7 @@ describe 'As an administrateur', js: true do
let(:super_admin) { create(:super_admin) } let(:super_admin) { create(:super_admin) }
let(:admin_email) { 'new_admin@gouv.fr' } let(:admin_email) { 'new_admin@gouv.fr' }
let(:new_admin) { Administrateur.by_email(admin_email) } let(:new_admin) { Administrateur.by_email(admin_email) }
let(:weak_password) { '12345678' } let(:weak_password) { '000000000000' }
let(:strong_password) { 'a new, long, and complicated password!' } let(:strong_password) { 'a new, long, and complicated password!' }
before do before do

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
describe 'Inviting an expert:' do describe 'Inviting an expert:', js: true do
include ActiveJob::TestHelper include ActiveJob::TestHelper
include ActionView::Helpers include ActionView::Helpers
@ -34,7 +34,8 @@ describe 'Inviting an expert:' do
end end
scenario 'I can sign-in again afterwards' do scenario 'I can sign-in again afterwards' do
click_on 'Se déconnecter' click_on(avis.expert.email.to_s, visible: true)
click_on('Se déconnecter', visible: true)
visit new_user_session_path visit new_user_session_path
sign_in_with avis.expert.email, password sign_in_with avis.expert.email, password
@ -54,9 +55,10 @@ describe 'Inviting an expert:' do
expect(page).to have_current_path(new_user_session_path) expect(page).to have_current_path(new_user_session_path)
login_as avis.expert.user, scope: :user login_as avis.expert.user, scope: :user
sign_in_with(avis.expert.email, 'This is a very complicated password !') sign_in_with(avis.expert.email, '{My-$3cure-p4ssWord}')
expect(page).to have_content("connecté en tant quexpert") expect(page).to have_content("connecté en tant quexpert")
click_on 'Passer en usager' click_on(avis.expert.email.to_s, visible: true)
click_on('Passer en usager', visible: true)
expect(page).to have_current_path(dossiers_path) expect(page).to have_current_path(dossiers_path)
end end
end end
@ -111,10 +113,11 @@ describe 'Inviting an expert:' do
expect(page).to have_text('Cet avis est confidentiel') expect(page).to have_text('Cet avis est confidentiel')
# check validation # check validation
fill_in 'avis_answer', with: 'Ma réponse dexpert.'
click_on 'Envoyer votre avis' click_on 'Envoyer votre avis'
expect(page).to have_content("Le champ « Réponse oui/non » n'est pas inclus(e) dans la liste") expect(page).to have_content("Le champ « Réponse oui/non » n'est pas inclus(e) dans la liste")
choose 'non' find('label', text: 'non').click
fill_in 'avis_answer', with: 'Ma réponse dexpert.' fill_in 'avis_answer', with: 'Ma réponse dexpert.'
click_on 'Envoyer votre avis' click_on 'Envoyer votre avis'

View file

@ -2,7 +2,7 @@
describe 'Protecting against request forgeries:', :allow_forgery_protection, :show_exception_pages do describe 'Protecting against request forgeries:', :allow_forgery_protection, :show_exception_pages do
let(:user) { create(:user, password: password) } let(:user) { create(:user, password: password) }
let(:password) { 'ThisIsTheUserPassword' } let(:password) { SECURE_PASSWORD }
before do before do
visit new_user_session_path visit new_user_session_path

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
describe 'The routing with rules', js: true do describe 'The routing with rules', js: true do
let(:password) { 'a very complicated password' } let(:password) { SECURE_PASSWORD }
let(:procedure) do let(:procedure) do
create(:procedure, :with_service, :for_individual, :with_zone, types_de_champ_public: [ create(:procedure, :with_service, :for_individual, :with_zone, types_de_champ_public: [

View file

@ -3,7 +3,7 @@
describe 'Managing password:', js: true do describe 'Managing password:', js: true do
context 'for simple users' do context 'for simple users' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:new_password) { 'a simple password' } let(:new_password) { 'a new, long, and complicated password!' }
scenario 'a simple user can reset their password' do scenario 'a simple user can reset their password' do
visit root_path visit root_path
@ -33,7 +33,7 @@ describe 'Managing password:', js: true do
context 'for admins' do context 'for admins' do
let(:administrateur) { administrateurs(:default_admin) } let(:administrateur) { administrateurs(:default_admin) }
let(:user) { administrateur.user } let(:user) { administrateur.user }
let(:weak_password) { '12345678' } let(:weak_password) { '000000000000' }
let(:strong_password) { 'a new, long, and complicated password!' } let(:strong_password) { 'a new, long, and complicated password!' }
scenario 'an admin can reset their password' do scenario 'an admin can reset their password' do
@ -72,7 +72,7 @@ describe 'Managing password:', js: true do
context 'for super-admins' do context 'for super-admins' do
let(:super_admin) { create(:super_admin) } let(:super_admin) { create(:super_admin) }
let(:weak_password) { '12345678' } let(:weak_password) { '000000000000' }
let(:strong_password) { 'a new, long, and complicated password!' } let(:strong_password) { 'a new, long, and complicated password!' }
scenario 'a super-admin can reset their password' do scenario 'a super-admin can reset their password' do
@ -109,8 +109,8 @@ describe 'Managing password:', js: true do
visit edit_user_password_path(reset_password_token: 'invalid-password-token') visit edit_user_password_path(reset_password_token: 'invalid-password-token')
expect(page).to have_content 'Changement de mot de passe' expect(page).to have_content 'Changement de mot de passe'
fill_in 'user_password', with: 'SomePassword' fill_in 'user_password', with: SECURE_PASSWORD
fill_in 'user_password_confirmation', with: 'SomePassword' fill_in 'user_password_confirmation', with: SECURE_PASSWORD
click_on 'Changer le mot de passe' click_on 'Changer le mot de passe'
expect(page).to have_content('Votre lien de nouveau mot de passe a expiré') expect(page).to have_content('Votre lien de nouveau mot de passe a expiré')
end end

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
describe 'Signing up:' do describe 'Signing up:', js: true do
let(:user_email) { generate :user_email } let(:user_email) { generate :user_email }
let(:user_password) { SECURE_PASSWORD } let(:user_password) { SECURE_PASSWORD }
let(:procedure) { create :simple_procedure, :with_service } let(:procedure) { create :simple_procedure, :with_service }
@ -24,7 +24,7 @@ describe 'Signing up:' do
click_on "Créer un compte #{APPLICATION_NAME}" click_on "Créer un compte #{APPLICATION_NAME}"
expect(page).to have_selector('.suspect-email', visible: false) expect(page).to have_selector('.suspect-email', visible: false)
fill_in 'Adresse électronique', with: 'bidou@yahoo.rf' fill_in 'Adresse électronique', with: 'bidou@yahoo.rf'
fill_in 'Mot de passe', with: '12345' fill_in 'Mot de passe', with: '1 2 3 4 5 6 '
end end
scenario 'they can accept the suggestion', js: true do scenario 'they can accept the suggestion', js: true do
@ -51,12 +51,12 @@ describe 'Signing up:' do
scenario 'a new user cant sign-up with too short password when visiting a procedure' do scenario 'a new user cant sign-up with too short password when visiting a procedure' do
visit commencer_path(path: procedure.path) visit commencer_path(path: procedure.path)
click_on "Créer un compte #{APPLICATION_NAME}" click_on 'Créer un compte'
expect(page).to have_current_path new_user_registration_path expect(page).to have_current_path new_user_registration_path
sign_up_with user_email, '1234567' fill_in :user_email, with: user_email
expect(page).to have_current_path user_registration_path fill_in :user_password, with: '1234567'
expect(page).to have_content "Le champ « Mot de passe » est trop court. Saisir un mot de passe avec au moins 8 caractères" expect(page).to have_content "Le mot de passe doit faire au moins 12 caractères."
# Then with a good password # Then with a good password
sign_up_with user_email, user_password sign_up_with user_email, user_password