diff --git a/app/assets/stylesheets/components/_password.scss b/app/assets/stylesheets/components/_password.scss index 7ea776858a3..741656eece5 100644 --- a/app/assets/stylesheets/components/_password.scss +++ b/app/assets/stylesheets/components/_password.scss @@ -1,7 +1,7 @@ // password strength module $weak: #e80e0e; -$so-so: #ffac00; +$average: #ffac00; $good: #9ac056; $great: #00b200; @@ -20,9 +20,9 @@ $great: #00b200; } } -.pw-so-so { +.pw-average { .pw-bar:nth-child(-n + 2) { - background-color: $so-so; + background-color: $average; } } diff --git a/app/javascript/packs/pw-strength.js b/app/javascript/packs/pw-strength.js index 0e792f1dea2..0e067dfb07c 100644 --- a/app/javascript/packs/pw-strength.js +++ b/app/javascript/packs/pw-strength.js @@ -8,7 +8,7 @@ import { t } from '@18f/identity-i18n'; const scale = { 0: ['pw-very-weak', t('instructions.password.strength.i')], 1: ['pw-weak', t('instructions.password.strength.ii')], - 2: ['pw-so-so', t('instructions.password.strength.iii')], + 2: ['pw-average', t('instructions.password.strength.iii')], 3: ['pw-good', t('instructions.password.strength.iv')], 4: ['pw-great', t('instructions.password.strength.v')], }; @@ -26,13 +26,21 @@ function getStrength(z) { return z && z.password.length ? scale[z.score] : fallback; } -export function getFeedback(z) { - if (!z || !z.password || z.score > 2) { +export function getFeedback(z, { minimumLength }) { + if (!z || !z.password) { return ' '; } const { warning, suggestions } = z.feedback; + if (!warning && !suggestions.length) { + if (z.password.length < minimumLength) { + return t('errors.attributes.password.too_short.other', { count: minimumLength }); + } + + return ' '; + } + function lookup(str) { // i18n-tasks-use t('zxcvbn.feedback.a_word_by_itself_is_easy_to_guess') // i18n-tasks-use t('zxcvbn.feedback.add_another_word_or_two_uncommon_words_are_better') @@ -66,9 +74,6 @@ export function getFeedback(z) { return t(`zxcvbn.feedback.${snakeCase(str)}`); } - if (!warning && !suggestions.length) { - return ' '; - } if (warning) { return lookup(warning); } @@ -96,11 +101,7 @@ function analyzePw() { const pwFeedback = document.getElementById('pw-strength-feedback'); const forbiddenPasswordsElement = document.querySelector('[data-forbidden]'); const forbiddenPasswords = getForbiddenPasswords(forbiddenPasswordsElement); - - // the pw strength module is hidden by default ("display-none" CSS class) - // (so that javascript disabled browsers won't see it) - // thus, first step is unhiding it - pwCntnr.className = ''; + const minPasswordLength = +pwCntnr.getAttribute('data-pw-min-length'); function updatePasswordFeedback(cls, strength, feedback) { pwCntnr.className = cls; @@ -118,8 +119,9 @@ function analyzePw() { function checkPasswordStrength(password) { const z = zxcvbn(password, forbiddenPasswords); + const [cls, strength] = getStrength(z); - const feedback = getFeedback(z); + const feedback = getFeedback(z, { minimumLength: minPasswordLength }); validatePasswordField(z.score); updatePasswordFeedback(cls, strength, feedback); diff --git a/app/views/devise/shared/_password_strength.html.erb b/app/views/devise/shared/_password_strength.html.erb index 1003611b6b1..98eae167a2f 100644 --- a/app/views/devise/shared/_password_strength.html.erb +++ b/app/views/devise/shared/_password_strength.html.erb @@ -1,4 +1,4 @@ -
- <%= t('instructions.password.info.lead', min_length: Devise.password_length.first) %> + <%= t('instructions.password.info.lead_html', min_length: Devise.password_length.first) %>
<%= simple_form_for( @password_form, diff --git a/app/views/users/passwords/edit.html.erb b/app/views/users/passwords/edit.html.erb index c59fba7e63f..10e4ee7c12a 100644 --- a/app/views/users/passwords/edit.html.erb +++ b/app/views/users/passwords/edit.html.erb @@ -3,7 +3,7 @@ <%= render PageHeadingComponent.new.with_content(t('headings.edit_info.password')) %>- <%= t('instructions.password.info.lead', min_length: Devise.password_length.first) %> + <%= t('instructions.password.info.lead_html', min_length: Devise.password_length.first) %>
<%= simple_form_for( diff --git a/config/locales/errors/en.yml b/config/locales/errors/en.yml index 2abeb0cee11..e75202c22f0 100644 --- a/config/locales/errors/en.yml +++ b/config/locales/errors/en.yml @@ -13,8 +13,8 @@ en: attributes: password: too_short: - one: This password is too short (minimum is 1 character) - other: This password is too short (minimum is %{count} characters) + one: Password must be at least one character long + other: Password must be at least %{count} characters long capture_doc: invalid_link: This link is expired or not valid. Please request another link to verify your identity on a mobile phone. diff --git a/config/locales/errors/es.yml b/config/locales/errors/es.yml index ba7ec77acd0..c566664dd78 100644 --- a/config/locales/errors/es.yml +++ b/config/locales/errors/es.yml @@ -13,8 +13,8 @@ es: attributes: password: too_short: - one: Esta contraseña es demasiado corta (debe tener 1 carácter como mínimo) - other: Esta contraseña es demasiado corta (%{count} caracteres como mínimo) + one: La contraseña debe tener al menos un carácter + other: La contraseña debe tener al menos %{count} caracteres de longitud. capture_doc: invalid_link: Este enlace ha caducado o no es válido. Solicite otro enlace para verificar su identidad en un teléfono móvil. diff --git a/config/locales/errors/fr.yml b/config/locales/errors/fr.yml index 96ef1c445db..2c283a4ad1f 100644 --- a/config/locales/errors/fr.yml +++ b/config/locales/errors/fr.yml @@ -15,8 +15,8 @@ fr: attributes: password: too_short: - one: Ce mot de passe est trop court (le minimum est de 1 caractère) - other: Ce mot de passe est trop court (le minimum est de %{count} caractères) + one: Le mot de passe doit comporter au moins un caractère + other: Le mot de passe doit comporter au moins %{count} caractères capture_doc: invalid_link: Ce lien a expiré ou n’est pas valide. Veuillez demander un autre lien pour vérifier votre identité sur un téléphone mobile. diff --git a/config/locales/instructions/en.yml b/config/locales/instructions/en.yml index 1a5883f9102..acb7d0ac969 100644 --- a/config/locales/instructions/en.yml +++ b/config/locales/instructions/en.yml @@ -86,23 +86,23 @@ en: wrong_number: Entered the wrong phone number? password: forgot: Don’t know your password? Reset it after confirming your email address. - help_text: The longer and more unusual the password, the harder it is to guess. - So avoid using common phrases. Also avoid repeating passwords from other - online accounts such as banks, email and social media. + help_text: Avoid reusing passwords from your other accounts, such as your banks, + email and social media. Don’t include words from your email address. help_text_header: Password safety tips info: - lead: It must be at least %{min_length} characters long and not be a commonly - used password. That’s it! + lead_html: Your password must be %{min_length} characters or + longer. Don’t use common phrases or repeated characters, like abc or + 111. password_key: You need your 16-character personal key to reset your password if you verified your identity with this account. If you don’t have it, you can still reset your password and then reverify your identity. strength: i: Very weak ii: Weak - iii: So-so + iii: Average intro: 'Password strength: ' iv: Good - v: Great! + v: Great sp_handoff_bounced: Your sign in was successful, but %{sp_name} sent you back to %{app_name}. Please contact %{sp_link} for help. sp_handoff_bounced_with_no_sp: your service provider diff --git a/config/locales/instructions/es.yml b/config/locales/instructions/es.yml index f5b9ae6acb9..9e6e98c9839 100644 --- a/config/locales/instructions/es.yml +++ b/config/locales/instructions/es.yml @@ -91,14 +91,14 @@ es: wrong_number: '¿Ingresó el número de teléfono equivocado?' password: forgot: '¿No sabe su contraseña? Restablézcala después de confirmar su email.' - help_text: Cuanto más larga y más inusual sea la contraseña, más difícil es de - adivinarla. Así que evite usar frases comunes. También evite repetir - contraseñas de otras cuentas en línea, por ejemplo de sus bancos, email - y medios sociales. + help_text: Evite reutilizar las contraseñas de sus otras cuentas, como las + bancarias, las de correo electrónico y las de redes sociales. No incluya + palabras de su dirección de correo electrónico. help_text_header: Consejos de seguridad de contraseña info: - lead: Su contraseña debe tener al menos %{min_length} caracteres y no ser una - contraseña común. ¡Eso es todo! + lead_html: Su contraseña deberá tener %{min_length} caracteres + o más. No use expresiones comunes ni caracteres repetidos, como “abc” + o “111″. password_key: Si verificó su identidad con esta cuenta, necesita su clave personal de 16 caracteres para restablecer su contraseña. Si no cuenta con ella, de todos modos puede restablecer su contraseña y luego volver @@ -106,10 +106,10 @@ es: strength: i: Muy débil ii: Débil - iii: Más o menos + iii: Promedio intro: 'Seguridad de la contraseña:' iv: Buena - v: '¡Muy buena!' + v: 'Muy buena' sp_handoff_bounced: Su inicio de sesión fue exitoso, pero %{sp_name} lo envió de regreso a %{app_name}. Póngase en contacto con %{sp_link} para obtener ayuda. diff --git a/config/locales/instructions/fr.yml b/config/locales/instructions/fr.yml index e27050d52be..574a010883d 100644 --- a/config/locales/instructions/fr.yml +++ b/config/locales/instructions/fr.yml @@ -100,14 +100,15 @@ fr: password: forgot: Vous ne connaissez pas votre mot de passe? Réinitialisez-le après avoir confirmé votre adresse courriel. - help_text: Plus long et inhabituel est le mot de passe, plus difficile il sera à - trouver. Évitez donc d’utiliser des phrases communes. Évitez aussi de - répéter des mots de passe d’autres comptes en ligne comme les comptes - bancaires, les comptes courriel et les comptes de médias sociaux. + help_text: Évitez de réutiliser les mots de passe de vos autres comptes, tels + que ceux de vos banques, de vos comptes de courriel et de vos réseaux + sociaux. N’incluez pas les mots de votre adresse de courriel. bancaires, + les comptes courriel et les comptes de médias sociaux. help_text_header: Conseils sur la sécurité du mot de passe info: - lead: Il doit avoir une longueur minimale de %{min_length} caractères et ne pas - être un mot de passe couramment utilisé. C’est tout! + lead_html: Votre mot de passe doit comporter %{min_length} + caractères ou plus. N’utilisez pas de phrases courantes ou de + caractères répétés, comme « abc » ou « 111 ». password_key: Vous avez besoin de votre clé personnelle de 16 caractères pour réinitialiser votre mot de passe si vous avez vérifié votre identité avec ce compte. Si vous ne l’avez pas, vous pouvez toujours @@ -115,10 +116,10 @@ fr: strength: i: Très faible ii: Faible - iii: Correct + iii: Moyen intro: 'Force du mot de passe : ' iv: Bonne - v: Excellente! + v: Excellente sp_handoff_bounced: Votre connexion a réussi, mais %{sp_name} vous a renvoyé à %{app_name}. Veuillez contacter %{sp_link} pour obtenir de l’aide. sp_handoff_bounced_with_no_sp: votre fournisseur de service diff --git a/spec/controllers/event_disavowal_controller_spec.rb b/spec/controllers/event_disavowal_controller_spec.rb index 318396377b1..1a0e82044e8 100644 --- a/spec/controllers/event_disavowal_controller_spec.rb +++ b/spec/controllers/event_disavowal_controller_spec.rb @@ -90,7 +90,13 @@ 'Event disavowal password reset', build_analytics_hash( success: false, - errors: { password: ['This password is too short (minimum is 12 characters)'] }, + errors: { + password: [ + t( + 'errors.attributes.password.too_short.other', count: Devise.password_length.first + ), + ], + }, ), ) @@ -107,7 +113,7 @@ 'Event disavowal password reset', build_analytics_hash( success: false, - errors: { password: ['This password is too short (minimum is 12 characters)'] }, + errors: { password: ['Password must be at least 12 characters long'] }, ), ) diff --git a/spec/controllers/sign_up/passwords_controller_spec.rb b/spec/controllers/sign_up/passwords_controller_spec.rb index 68f3a5f796e..246d07b7ab4 100644 --- a/spec/controllers/sign_up/passwords_controller_spec.rb +++ b/spec/controllers/sign_up/passwords_controller_spec.rb @@ -80,7 +80,7 @@ success: false, errors: { password: - ["This password is too short (minimum is #{Devise.password_length.first} characters)"], + ["Password must be at least #{Devise.password_length.first} characters long"], }, error_details: password_short_error, user_id: user.uuid, diff --git a/spec/controllers/users/reset_passwords_controller_spec.rb b/spec/controllers/users/reset_passwords_controller_spec.rb index 93255438def..aac2703983b 100644 --- a/spec/controllers/users/reset_passwords_controller_spec.rb +++ b/spec/controllers/users/reset_passwords_controller_spec.rb @@ -2,7 +2,7 @@ describe Users::ResetPasswordsController, devise: true do let(:password_error_message) do - "This password is too short (minimum is #{Devise.password_length.first} characters)" + t('errors.attributes.password.too_short.other', count: Devise.password_length.first) end let(:success_properties) { { success: true, failure_reason: nil } } let(:token_expired_error) { 'token_expired' } diff --git a/spec/features/event_disavowal_spec.rb b/spec/features/event_disavowal_spec.rb index eecbeadf2f3..9e99cfddc9c 100644 --- a/spec/features/event_disavowal_spec.rb +++ b/spec/features/event_disavowal_spec.rb @@ -111,7 +111,9 @@ fill_in 'New password', with: 'invalid' click_button t('forms.passwords.edit.buttons.submit') - expect(page).to have_content('is too short (minimum is 12 characters)') + expect(page).to have_content t( + 'errors.attributes.password.too_short.other', count: Devise.password_length.first + ) fill_in 'New password', with: 'NewVal!dPassw0rd' click_button t('forms.passwords.edit.buttons.submit') diff --git a/spec/features/users/user_profile_spec.rb b/spec/features/users/user_profile_spec.rb index 6af7b031388..a584bf1ec32 100644 --- a/spec/features/users/user_profile_spec.rb +++ b/spec/features/users/user_profile_spec.rb @@ -115,11 +115,11 @@ click_link 'Edit', href: manage_password_path end - expect(page).to_not have_css('#pw-strength-cntnr.display-none') - expect(page).to have_content '...' + expect(page).not_to have_content(t('instructions.password.strength.intro')) fill_in t('forms.passwords.edit.labels.password'), with: 'this is a great sentence' - expect(page).to have_content 'Great' + expect(page).to have_content(t('instructions.password.strength.intro')) + expect(page).to have_content t('instructions.password.strength.v') check t('components.password_toggle.toggle_label') diff --git a/spec/features/visitors/password_recovery_spec.rb b/spec/features/visitors/password_recovery_spec.rb index fac3404acb2..cc077cff257 100644 --- a/spec/features/visitors/password_recovery_spec.rb +++ b/spec/features/visitors/password_recovery_spec.rb @@ -211,7 +211,9 @@ click_button t('forms.passwords.edit.buttons.submit') expect(page). - to have_content "is too short (minimum is #{Devise.password_length.first} characters)" + to have_content t( + 'errors.attributes.password.too_short.other', count: Devise.password_length.first + ) end it "does not update the user's password when password is invalid" do @@ -226,12 +228,16 @@ fill_in 'New password', with: '1234' click_button t('forms.passwords.edit.buttons.submit') - expect(page).to have_content 'is too short' + expect(page).to have_content t( + 'errors.attributes.password.too_short.other', count: Devise.password_length.first + ) fill_in 'New password', with: '5678' click_button t('forms.passwords.edit.buttons.submit') - expect(page).to have_content 'is too short' + expect(page).to have_content t( + 'errors.attributes.password.too_short.other', count: Devise.password_length.first + ) end end end diff --git a/spec/features/visitors/set_password_spec.rb b/spec/features/visitors/set_password_spec.rb index d87fdaf0777..4251c20b0c5 100644 --- a/spec/features/visitors/set_password_spec.rb +++ b/spec/features/visitors/set_password_spec.rb @@ -38,16 +38,20 @@ end it 'updates strength feedback as password changes' do - expect(page).to have_content '...' + expect(page).not_to have_content(t('instructions.password.strength.intro')) fill_in t('forms.password'), with: 'password' - expect(page).to have_content 'Very weak' + expect(page).to have_content(t('instructions.password.strength.intro')) + expect(page).to have_content t('instructions.password.strength.i') fill_in t('forms.password'), with: '123456789' expect(page).to have_content t('zxcvbn.feedback.this_is_a_top_10_common_password') fill_in t('forms.password'), with: 'this is a great sentence' - expect(page).to have_content 'Great!' + expect(page).to have_content t('instructions.password.strength.v') + + fill_in t('forms.password'), with: ':b/}6tT#,' + expect(page).to have_content t('errors.attributes.password.too_short.other', count: 12) end end diff --git a/spec/forms/password_form_spec.rb b/spec/forms/password_form_spec.rb index ca67acd207c..e4beaa5def1 100644 --- a/spec/forms/password_form_spec.rb +++ b/spec/forms/password_form_spec.rb @@ -32,8 +32,11 @@ form = PasswordForm.new(user) password = 'invalid' errors = { - password: - ["This password is too short (minimum is #{Devise.password_length.first} characters)"], + password: [ + t( + 'errors.attributes.password.too_short.other', count: Devise.password_length.first + ), + ], } extra = { user_id: '123', diff --git a/spec/forms/reset_password_form_spec.rb b/spec/forms/reset_password_form_spec.rb index 3ba6da42dd5..343fc0717a2 100644 --- a/spec/forms/reset_password_form_spec.rb +++ b/spec/forms/reset_password_form_spec.rb @@ -39,7 +39,7 @@ errors = { password: - ["This password is too short (minimum is #{Devise.password_length.first} characters)"], + ["Password must be at least #{Devise.password_length.first} characters long"], } extra = { user_id: '123', profile_deactivated: false } @@ -84,8 +84,9 @@ password = 'short' errors = { - password: - ["This password is too short (minimum is #{Devise.password_length.first} characters)"], + password: [ + t('errors.attributes.password.too_short.other', count: Devise.password_length.first), + ], reset_password_token: ['token_expired'], } diff --git a/spec/javascripts/packs/pw-strength-spec.js b/spec/javascripts/packs/pw-strength-spec.js index b743dca80ea..c89f3b737da 100644 --- a/spec/javascripts/packs/pw-strength-spec.js +++ b/spec/javascripts/packs/pw-strength-spec.js @@ -40,19 +40,31 @@ describe('pw-strength', () => { it('returns an empty result for empty password', () => { const z = zxcvbn(''); - expect(getFeedback(z)).to.equal(EMPTY_RESULT); + expect(getFeedback(z, { minimumLength: 12 })).to.equal(EMPTY_RESULT); }); it('returns an empty result for a strong password', () => { const z = zxcvbn('!Juq2Uk2**RBEsA8'); - expect(getFeedback(z)).to.equal(EMPTY_RESULT); + expect(getFeedback(z, { minimumLength: 12 })).to.equal(EMPTY_RESULT); }); it('returns feedback for a weak password', () => { const z = zxcvbn('password'); - expect(getFeedback(z)).to.equal('zxcvbn.feedback.this_is_a_top_10_common_password'); + expect(getFeedback(z, { minimumLength: 12 })).to.equal( + 'zxcvbn.feedback.this_is_a_top_10_common_password', + ); + }); + + it('shows feedback when a password is too short', () => { + const minPasswordLength = 12; + const z = zxcvbn('_3G%JMyR"'); + + expect(getFeedback(z, { minimumLength: minPasswordLength })).to.equal( + 'errors.attributes.password.too_short.other', + { count: minPasswordLength }, + ); }); }); }); diff --git a/spec/views/sign_up/passwords/new.html.erb_spec.rb b/spec/views/sign_up/passwords/new.html.erb_spec.rb index 1e3cca6f0d2..a88f25003d2 100644 --- a/spec/views/sign_up/passwords/new.html.erb_spec.rb +++ b/spec/views/sign_up/passwords/new.html.erb_spec.rb @@ -21,8 +21,11 @@ end it 'renders the proper help text' do - expect(rendered).to have_content( - t('instructions.password.info.lead', min_length: Devise.password_length.first), + expect(rendered).to have_content strip_tags( + t( + 'instructions.password.info.lead_html', + min_length: Devise.password_length.min, + ), ) end diff --git a/spec/views/users/passwords/edit.html.erb_spec.rb b/spec/views/users/passwords/edit.html.erb_spec.rb index 36556f841bb..974c03cbce5 100644 --- a/spec/views/users/passwords/edit.html.erb_spec.rb +++ b/spec/views/users/passwords/edit.html.erb_spec.rb @@ -28,8 +28,11 @@ it 'contains minimum password length requirements' do render - expect(rendered).to have_content t( - 'instructions.password.info.lead', min_length: Devise.password_length.first + expect(rendered).to have_content strip_tags( + t( + 'instructions.password.info.lead_html', + min_length: Devise.password_length.min, + ), ) end end