Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
4e143f2
feat: enforce pw strength at login
jennifer-richards Apr 23, 2025
20be298
Merge branch 'main' into password-strength
jennifer-richards Jun 10, 2025
7d6b1f4
chore(deps): add zxcvbn to requirements.txt
jennifer-richards Jun 10, 2025
001dfaf
feat: use zxcvbn for password strength check
jennifer-richards Jun 10, 2025
425b38f
feat: validate password strength for setting pw
jennifer-richards Jun 10, 2025
fdc82bb
feat: feedback on password change page
jennifer-richards Jun 10, 2025
10f88a1
refactor: avoid field validator munging
jennifer-richards Jun 10, 2025
4d6a543
feat: give more info about how to choose a pw
jennifer-richards Jun 10, 2025
db20b54
refactor: use password_validation module
jennifer-richards Jun 11, 2025
fc26691
feat: password min_length validdation
jennifer-richards Jun 11, 2025
29414ef
refactor: use password_validation for login form
jennifer-richards Jun 11, 2025
e3a806e
fix: UI feedback consistent with validation
jennifer-richards Jun 11, 2025
3fb44a1
chore: update chpw page to state length req
jennifer-richards Jun 11, 2025
5b6b9f0
Merge branch 'main' into password-strength
jennifer-richards Jun 11, 2025
cbec4bc
chore(dev): disable password validators in dev
jennifer-richards Jun 12, 2025
d6cea56
fix: drop JS validation when password val disabled
jennifer-richards Jun 12, 2025
8c721ba
style: ruff on ChangePasswordForm
jennifer-richards Jun 12, 2025
a3ab53e
chore: lint
jennifer-richards Jun 18, 2025
510900c
chore(dev): preserve pw validator cfg for tests
jennifer-richards Jun 18, 2025
df4e588
test: fix test_change_password
jennifer-richards Jun 18, 2025
c63bcd8
test: fix test_change_username
jennifer-richards Jun 18, 2025
9e6c03d
test: fix test_reset_password
jennifer-richards Jun 18, 2025
3ac7ade
style: ruff refactored tests
jennifer-richards Jun 18, 2025
377def6
chore: type lint
jennifer-richards Jun 18, 2025
49e248b
Merge branch 'main' into password-strength
jennifer-richards Jun 18, 2025
1a98ef4
feat: require pw reset for very stale accounts
jennifer-richards Jun 18, 2025
ae0d90b
test: test stale account login
jennifer-richards Jun 18, 2025
8097e30
test: rejection of short/simple PWs
jennifer-richards Jun 18, 2025
0eae8a1
Revert "test: test stale account login"
jennifer-richards Jun 18, 2025
7db71a9
Revert "feat: require pw reset for very stale accounts"
jennifer-richards Jun 18, 2025
8d48677
test: disable pw validators for playwright legacy tests
jennifer-richards Jun 18, 2025
ea3333b
Merge branch 'main' into password-strength
jennifer-richards Jun 24, 2025
3898f4d
feat: make pw enforcement at login optional
jennifer-richards Jun 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 38 additions & 16 deletions ietf/ietfauth/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@


import re

from unidecode import unidecode

from django import forms
from django.contrib.auth.models import User
from django.contrib.auth import password_validation
from django.core.exceptions import ValidationError
from django.db import models
from django.contrib.auth.models import User

from ietf.person.models import Person, Email
from ietf.mailinglists.models import Allowlisted
from ietf.utils.text import isascii
from .password_validation import StrongPasswordValidator

from .validators import prevent_at_symbol, prevent_system_name, prevent_anonymous_name, is_allowed_address
from .widgets import PasswordStrengthInput, PasswordConfirmationInput
Expand Down Expand Up @@ -170,33 +173,52 @@ class Meta:
model = Allowlisted
exclude = ['by', 'time' ]


from django import forms


class ChangePasswordForm(forms.Form):
current_password = forms.CharField(widget=forms.PasswordInput)

new_password = forms.CharField(widget=PasswordStrengthInput(attrs={'class':'password_strength'}))
new_password_confirmation = forms.CharField(widget=PasswordConfirmationInput(
confirm_with='new_password',
attrs={'class':'password_confirmation'}))
new_password = forms.CharField(
widget=PasswordStrengthInput(
attrs={
"class": "password_strength",
"data-disable-strength-enforcement": "", # usually removed in init
}
),
)
new_password_confirmation = forms.CharField(
widget=PasswordConfirmationInput(
confirm_with="new_password", attrs={"class": "password_confirmation"}
)
)

def __init__(self, user, data=None):
self.user = user
super(ChangePasswordForm, self).__init__(data)
super().__init__(data)
# Check whether we have validators to enforce
new_password_field = self.fields["new_password"]
for pwval in password_validation.get_default_password_validators():
if isinstance(pwval, password_validation.MinimumLengthValidator):
new_password_field.widget.attrs["minlength"] = pwval.min_length
elif isinstance(pwval, StrongPasswordValidator):
new_password_field.widget.attrs.pop(
"data-disable-strength-enforcement", None
)

def clean_current_password(self):
password = self.cleaned_data.get('current_password', None)
# n.b., password = None is handled by check_password and results in a failed check
password = self.cleaned_data.get("current_password", None)
if not self.user.check_password(password):
raise ValidationError('Invalid password')
raise ValidationError("Invalid password")
return password

def clean(self):
new_password = self.cleaned_data.get('new_password', None)
conf_password = self.cleaned_data.get('new_password_confirmation', None)
if not new_password == conf_password:
raise ValidationError("The password confirmation is different than the new password")
new_password = self.cleaned_data.get("new_password", "")
conf_password = self.cleaned_data.get("new_password_confirmation", "")
if new_password != conf_password:
raise ValidationError(
"The password confirmation is different than the new password"
)
password_validation.validate_password(conf_password, self.user)


class ChangeUsernameForm(forms.Form):
Expand Down
23 changes: 23 additions & 0 deletions ietf/ietfauth/password_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright The IETF Trust 2025, All Rights Reserved
from django.core.exceptions import ValidationError
from zxcvbn import zxcvbn


class StrongPasswordValidator:
message = "This password does not meet complexity requirements and is easily guessable."
code = "weak"
min_zxcvbn_score = 3

def __init__(self, message=None, code=None, min_zxcvbn_score=None):
if message is not None:
self.message = message
if code is not None:
self.code = code
if min_zxcvbn_score is not None:
self.min_zxcvbn_score = min_zxcvbn_score

def validate(self, password, user=None):
"""Validate that a password is strong enough"""
strength_report = zxcvbn(password[:72], max_length=72)
if strength_report["score"] < self.min_zxcvbn_score:
raise ValidationError(message=self.message, code=self.code)
Loading
Loading