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-sidekiq'
gem 'zipline'
gem 'zxcvbn-ruby', require: 'zxcvbn'
gem 'zxcvbn'
group :test do
gem 'axe-core-rspec' # accessibility rspec matchers

View file

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

View file

@ -11,13 +11,16 @@
.fr-fieldset__element
%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]
.fr-fieldset__element
.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'
= 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') }

View file

@ -7,4 +7,4 @@ en:
strong: Congratulations! Password is strong and secure enough.
weak: 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é.
weak: Mot de passe 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 }
%h3.fr-alert__title= title
- if !success?
%p= t(".hint")
= t(".hint_html")

View file

@ -2,7 +2,8 @@
class PasswordComplexityController < ApplicationController
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_complexity = PASSWORD_COMPLEXITY_FOR_ADMIN
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
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
def send_confirmation_instructions
unless @raw_confirmation_token

View file

@ -3,51 +3,16 @@
class ZxcvbnService
@tester_mutex = Mutex.new
class << self
# 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
# that storing it in a per-thread accessor, we prefer to use a mutex
# to cache it between threads.
def tester
@tester_mutex.synchronize do
@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
# 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
# that storing it in a per-thread accessor, we prefer to use a mutex
# to cache it between threads.
def self.tester
@tester_mutex.synchronize do
@tester ||= Zxcvbn::Tester.new
end
end
def initialize(password)
@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
def self.complexity(password)= tester.test(password.to_s).score
end

View file

@ -2,7 +2,7 @@
class PasswordComplexityValidator < ActiveModel::EachValidator
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)
end
end

View file

@ -23,4 +23,4 @@
#password_complexity
= 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
= 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
- if populated_resource.validate_password_complexity?
%div{ id: c.describedby_id }
#password_complexity
= render PasswordComplexityComponent.new
- else
= render partial: "devise/password_rules", locals: { id: c.describedby_id }
%div{ id: c.describedby_id }
#password_complexity
= render PasswordComplexityComponent.new
.fr-fieldset__element
= 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
.columns-container
.column.left
%h2.fr-py-5w.text-center= @dossier.procedure.libelle
%p.dossier Dossier nº #{@dossier.id}
.column
.fr-container.fr-my-5w
.fr-grid-row.fr-grid-row--center
.fr-col-lg-6
= 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' } }
.fr-fieldset__element
%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
= render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field, opts: { autocomplete: 'new-password', minlength: PASSWORD_MIN_LENGTH }) do |c|
- c.with_describedby do
= render partial: "devise/password_rules", locals: { id: c.describedby_id }
= render Dsfr::InputComponent.new(form: f, attribute: :email, input_type: :email_field, opts: { disabled: true, autocomplete: 'email' })
%ul.fr-btns-group
%li= f.submit t('views.shared.account.create'), class: "fr-btn"
.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.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
= 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: :password, input_type: :password_field, opts: { autocomplete: 'new-password', minlength: PASSWORD_MIN_LENGTH }) do |c|
- c.with_describedby do
= render partial: "devise/password_rules", locals: { id: c.describedby_id }
= 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 }})
%ul.fr-btns-group
%li= f.submit t('views.shared.account.create'), class: "fr-btn"
#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

@ -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_ADMIN = ENV.fetch('PASSWORD_COMPLEXITY_FOR_ADMIN', '4').to_i
# 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

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"
email_label: 'Email (name@site.com)'
password_label: "Password (%{min_length} characters minimum)"
password_message: "Your password must have :"
password_placeholder: "%{min_length} characters minimum"
confirmation:
new:
title: 'Confirm your email address'
@ -661,7 +659,7 @@ en:
invalid: 'is invalid. Fill in a valid email address, example: address@mail.com'
taken: 'already in use. Fill in another email'
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"
password_confirmation:
confirmation: ": The two passwords do not match"

View file

@ -303,8 +303,6 @@ fr:
subtitle: "Se créer un compte avec une adresse email"
email_label: 'Email'
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:
new:
title: 'Confirmez votre adresse email'
@ -662,7 +660,7 @@ fr:
invalid: 'est invalide. Saisir une adresse électronique valide, exemple : adresse@mail.com'
taken: 'déjà utilisé. Saisir une autre adresse électronique.'
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"
password_confirmation:
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
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(response).to redirect_to(admin_activate_path(token: token)) }

View file

@ -61,7 +61,7 @@ end
RSpec.describe "CSRF cleanup", type: :request do
describe 'csrf_cleaner hook', :allow_forgery_protection do
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
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
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 }
it 'doesnt change the random user password' do
@ -613,7 +613,7 @@ describe Experts::AvisController, type: :controller do
let(:avis) { create(:avis) }
let(:avis_id) { avis.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 }
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
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
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)
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
subject
@ -408,7 +408,7 @@ describe FranceConnect::ParticulierController, type: :controller do
end
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
subject

View file

@ -23,7 +23,7 @@ describe Gestionnaires::ActivateController, type: :controller do
describe '#create' do
let!(:gestionnaire) { create(:gestionnaire) }
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 } } }

View file

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

View file

@ -23,7 +23,7 @@ describe Users::ActivateController, type: :controller do
describe '#create' do
let!(:user) { create(:user) }
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 } } }

View file

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

View file

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

View file

@ -10,7 +10,7 @@ FactoryBot.define do
transient do
email { generate(:instructeur_email) }
password { '{my-%s3cure[]-p4$$w0rd' }
password { '{My-$3cure-p4ssWord}' }
end
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)
# 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)
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
let(:email) { 'mail@beta.gouv.fr' }
@ -89,7 +89,7 @@ describe SuperAdmin, type: :model do
let(:password) { 's' * (PASSWORD_MIN_LENGTH - 1) }
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

View file

@ -103,7 +103,7 @@ describe User, type: :model do
describe '.create_or_promote_to_instructeur' do
let(:email) { 'inst1@gmail.com' }
let(:password) { 'un super password !' }
let(:password) { SECURE_PASSWORD }
let(: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)
# 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)
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
subject do
@ -405,7 +405,7 @@ describe User, type: :model do
let(:password) { 's' * (PASSWORD_MIN_LENGTH - 1) }
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
@ -431,16 +431,19 @@ describe User, type: :model do
let(:password) { 's' * (PASSWORD_MIN_LENGTH - 1) }
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
context 'when the password is long enough, but simple' do
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
expect(subject).to be_empty
end
context 'when the password is long and complex' do
let(:password) { passwords[min_complexity] }
it { expect(subject).to be_empty }
end
end
end

View file

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

View file

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

View file

@ -61,7 +61,7 @@ module SystemHelpers
confirmation_email = open_email(email)
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
def click_reset_password_link_for(email)

View file

@ -2,7 +2,7 @@
describe 'wcag rules for usager', chrome: true do
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) }
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(:admin_email) { 'new_admin@gouv.fr' }
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!' }
before do

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true
describe 'Inviting an expert:' do
describe 'Inviting an expert:', js: true do
include ActiveJob::TestHelper
include ActionView::Helpers
@ -34,7 +34,8 @@ describe 'Inviting an expert:' do
end
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
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)
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")
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)
end
end
@ -111,10 +113,11 @@ describe 'Inviting an expert:' do
expect(page).to have_text('Cet avis est confidentiel')
# check validation
fill_in 'avis_answer', with: 'Ma réponse dexpert.'
click_on 'Envoyer votre avis'
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.'
click_on 'Envoyer votre avis'

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true
describe 'Signing up:' do
describe 'Signing up:', js: true do
let(:user_email) { generate :user_email }
let(:user_password) { SECURE_PASSWORD }
let(:procedure) { create :simple_procedure, :with_service }
@ -24,7 +24,7 @@ describe 'Signing up:' do
click_on "Créer un compte #{APPLICATION_NAME}"
expect(page).to have_selector('.suspect-email', visible: false)
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
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
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
sign_up_with user_email, '1234567'
expect(page).to have_current_path user_registration_path
expect(page).to have_content "Le champ « Mot de passe » est trop court. Saisir un mot de passe avec au moins 8 caractères"
fill_in :user_email, with: user_email
fill_in :user_password, with: '1234567'
expect(page).to have_content "Le mot de passe doit faire au moins 12 caractères."
# Then with a good password
sign_up_with user_email, user_password