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:
commit
3d50f9363f
50 changed files with 6528 additions and 5216 deletions
2
Gemfile
2
Gemfile
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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') }
|
||||||
|
|
|
@ -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)
|
|
|
@ -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') }
|
||||||
|
|
|
@ -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') }
|
||||||
|
|
|
@ -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') }
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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') }
|
||||||
|
|
|
@ -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
|
||||||
|
|
11
config/initializers/zxcvbn.rb
Normal file
11
config/initializers/zxcvbn.rb
Normal 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
|
@ -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"
|
||||||
|
|
|
@ -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: "n’est pas assez complexe. Saisir un mot de passe plus complexe"
|
not_strong: "n’est 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'
|
||||||
|
|
100
config/zxcvbn_frequency_lists/female_names_fr.txt
Normal file
100
config/zxcvbn_frequency_lists/female_names_fr.txt
Normal 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
|
100
config/zxcvbn_frequency_lists/male_names_fr.txt
Normal file
100
config/zxcvbn_frequency_lists/male_names_fr.txt
Normal 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
|
1000
config/zxcvbn_frequency_lists/passwords_fr.txt
Normal file
1000
config/zxcvbn_frequency_lists/passwords_fr.txt
Normal file
File diff suppressed because it is too large
Load diff
200
config/zxcvbn_frequency_lists/surnames_fr.txt
Normal file
200
config/zxcvbn_frequency_lists/surnames_fr.txt
Normal 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
|
4999
config/zxcvbn_frequency_lists/words_fr.txt
Normal file
4999
config/zxcvbn_frequency_lists/words_fr.txt
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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)) }
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 'doesn’t change the random user password' do
|
it 'doesn’t 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 'doesn’t change the random user password' do
|
it 'doesn’t change the random user password' do
|
||||||
|
@ -629,7 +629,7 @@ describe Experts::AvisController, type: :controller do
|
||||||
|
|
||||||
it 'doesn’t change the expert password' do
|
it 'doesn’t 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 }
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 } } }
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 } } }
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 » n’est pas assez complexe. Saisir un mot de passe plus complexe"]) }
|
||||||
|
end
|
||||||
|
|
||||||
it 'doesn’t 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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 qu’expert")
|
expect(page).to have_content("connecté en tant qu’expert")
|
||||||
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 d’expert.'
|
||||||
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 d’expert.'
|
fill_in 'avis_answer', with: 'Ma réponse d’expert.'
|
||||||
click_on 'Envoyer votre avis'
|
click_on 'Envoyer votre avis'
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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: [
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 can’t sign-up with too short password when visiting a procedure' do
|
scenario 'a new user can’t 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
|
||||||
|
|
Loading…
Add table
Reference in a new issue