A password strength meter wherever a password is chosen #26

Closed
opened 2026-09-05 13:47:35 +00:00 by tiagoagueda · 0 comments
Owner

Observation

on the password change, having a meter of the strength of the password is good

What exists today

  • Passwords are checked on submit by Django's four default validators (config/settings/base.py, line 116): similarity to the person's own attributes, minimum length (Django's default of 8), the common-passwords list, and not-all-numeric. A rejected password comes back as form errors after the round trip; there is no feedback while typing.
  • Every page that asks for a password is django-allauth's — sign-up, password change, set, reset — rendered inside Postulo's shell through the account/base_*.html overrides in templates/account/. No project template contains a password field of its own, so the meter has to attach to allauth's rendered forms rather than to markup Postulo writes.
  • The CSP is script-src 'self', and base.html has no block for page-specific scripts (only title, chrome and content), so anything loaded for password pages alone needs a scripts block added first.

Shape

1. Where. Every place a password is chosen: sign-up (when registration is open), invitation acceptance, password change, password set (an SSO account adding a password, #6), password reset from a link. Never on the sign-in form: a meter there tells an attacker nothing useful but it is noise.

2. Client-side estimation with zxcvbn, vendored. @zxcvbn-ts/core with @zxcvbn-ts/language-common and language-en (MIT), served from static/js/vendor/ like htmx, loaded only on password pages through the new block. Rationale over a server-side check on every keystroke: the password never leaves the browser until the person submits it, there is no new endpoint to rate-limit, and the estimate — dictionary words, keyboard walks, dates, repeats — is far better than the length-and-variety heuristics that make "P@ssw0rd1" look strong. Feed userInputs with the email's local part and the person's name so a password built from them scores low, which is what Django's similarity validator will say on submit anyway. Language packs for French exist and for Portuguese partially (language-pt-br); load them with the interface language once translations arrive.

3. What it shows. A four-segment bar with a word — weak, fair, good, strong — and zxcvbn's own suggestion ("add another word or two; uncommon words are better") when the score is low. Under it, Django's rules as a checklist that the meter cannot contradict, from password_validators_help_texts: the meter says how good, the list says what is required. Colour never carries the meaning alone; the word does.

4. Behaviour. Delegated from app.js on input events for [data-password-meter], which the password templates add to allauth's password1 field by overriding the form's widget attributes in accounts/forms.py (allauth's ACCOUNT_FORMS setting). With scripts off: no bar, the checklist stays; nothing is required from the meter to submit.

5. Announce it. The meter's text is a live region so a screen-reader user hears "fair" change to "strong" as they type.

6. While in there. Raise the minimum length to 12. Length is the one factor that reliably matters; NIST's guidance is a minimum of 8 with a blocklist and no composition rules, and 12 costs nothing with a meter beside it. It applies only to new passwords, so nobody is locked out.

Classification

Enhancement. Not breaking: existing passwords are untouched; a validator change applies to new passwords only.

Open questions

  1. Client-side zxcvbn (proposal) or a server-side check through htmx using Django's own validators plus the Python port of zxcvbn? The server-side version is always right about what will be accepted, at the cost of sending each keystroke of a password to the server and adding an endpoint. The proposal above keeps the checklist so both are satisfied.
  2. Minimum length 12, or leave Django's 8?
## Observation > on the password change, having a meter of the strength of the password is good ## What exists today - Passwords are checked on submit by Django's four default validators (`config/settings/base.py`, line 116): similarity to the person's own attributes, minimum length (Django's default of 8), the common-passwords list, and not-all-numeric. A rejected password comes back as form errors after the round trip; there is no feedback while typing. - Every page that asks for a password is django-allauth's — sign-up, password change, set, reset — rendered inside Postulo's shell through the `account/base_*.html` overrides in `templates/account/`. No project template contains a password field of its own, so the meter has to attach to allauth's rendered forms rather than to markup Postulo writes. - The CSP is `script-src 'self'`, and `base.html` has no block for page-specific scripts (only `title`, `chrome` and `content`), so anything loaded for password pages alone needs a `scripts` block added first. ## Shape **1. Where.** Every place a password is chosen: sign-up (when registration is open), invitation acceptance, password change, password set (an SSO account adding a password, #6), password reset from a link. Never on the sign-in form: a meter there tells an attacker nothing useful but it is noise. **2. Client-side estimation with zxcvbn, vendored.** `@zxcvbn-ts/core` with `@zxcvbn-ts/language-common` and `language-en` (MIT), served from `static/js/vendor/` like htmx, loaded only on password pages through the new block. Rationale over a server-side check on every keystroke: the password never leaves the browser until the person submits it, there is no new endpoint to rate-limit, and the estimate — dictionary words, keyboard walks, dates, repeats — is far better than the length-and-variety heuristics that make "P@ssw0rd1" look strong. Feed `userInputs` with the email's local part and the person's name so a password built from them scores low, which is what Django's similarity validator will say on submit anyway. Language packs for French exist and for Portuguese partially (`language-pt-br`); load them with the interface language once translations arrive. **3. What it shows.** A four-segment bar with a word — weak, fair, good, strong — and zxcvbn's own suggestion ("add another word or two; uncommon words are better") when the score is low. Under it, Django's rules as a checklist that the meter cannot contradict, from `password_validators_help_texts`: the meter says how good, the list says what is required. Colour never carries the meaning alone; the word does. **4. Behaviour.** Delegated from `app.js` on `input` events for `[data-password-meter]`, which the password templates add to allauth's `password1` field by overriding the form's widget attributes in `accounts/forms.py` (allauth's `ACCOUNT_FORMS` setting). With scripts off: no bar, the checklist stays; nothing is required from the meter to submit. **5. Announce it.** The meter's text is a live region so a screen-reader user hears "fair" change to "strong" as they type. **6. While in there.** Raise the minimum length to 12. Length is the one factor that reliably matters; NIST's guidance is a minimum of 8 with a blocklist and no composition rules, and 12 costs nothing with a meter beside it. It applies only to new passwords, so nobody is locked out. ## Classification Enhancement. Not breaking: existing passwords are untouched; a validator change applies to new passwords only. ## Open questions 1. Client-side zxcvbn (proposal) or a server-side check through htmx using Django's own validators plus the Python port of zxcvbn? The server-side version is always right about what will be accepted, at the cost of sending each keystroke of a password to the server and adding an endpoint. The proposal above keeps the checklist so both are satisfied. 2. Minimum length 12, or leave Django's 8?
tiagoagueda added this to the 0.2.0 milestone 2026-09-05 13:47:35 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Postulo/postulo#26
No description provided.