Skip to content

Commit ec7d690

Browse files
bcailsarahboyce
authored andcommitted
Fixed #35782 -- Allowed overriding password validation error messages.
1 parent 06bf06a commit ec7d690

File tree

4 files changed

+131
-18
lines changed

4 files changed

+131
-18
lines changed

django/contrib/auth/password_validation.py

+22-14
Original file line numberDiff line numberDiff line change
@@ -106,17 +106,16 @@ def __init__(self, min_length=8):
106106

107107
def validate(self, password, user=None):
108108
if len(password) < self.min_length:
109-
raise ValidationError(
110-
ngettext(
111-
"This password is too short. It must contain at least "
112-
"%(min_length)d character.",
113-
"This password is too short. It must contain at least "
114-
"%(min_length)d characters.",
115-
self.min_length,
116-
),
117-
code="password_too_short",
118-
params={"min_length": self.min_length},
119-
)
109+
raise ValidationError(self.get_error_message(), code="password_too_short")
110+
111+
def get_error_message(self):
112+
return ngettext(
113+
"This password is too short. It must contain at least %d character."
114+
% self.min_length,
115+
"This password is too short. It must contain at least %d characters."
116+
% self.min_length,
117+
self.min_length,
118+
)
120119

121120
def get_help_text(self):
122121
return ngettext(
@@ -203,11 +202,14 @@ def validate(self, password, user=None):
203202
except FieldDoesNotExist:
204203
verbose_name = attribute_name
205204
raise ValidationError(
206-
_("The password is too similar to the %(verbose_name)s."),
205+
self.get_error_message(),
207206
code="password_too_similar",
208207
params={"verbose_name": verbose_name},
209208
)
210209

210+
def get_error_message(self):
211+
return _("The password is too similar to the %(verbose_name)s.")
212+
211213
def get_help_text(self):
212214
return _(
213215
"Your password can’t be too similar to your other personal information."
@@ -242,10 +244,13 @@ def __init__(self, password_list_path=DEFAULT_PASSWORD_LIST_PATH):
242244
def validate(self, password, user=None):
243245
if password.lower().strip() in self.passwords:
244246
raise ValidationError(
245-
_("This password is too common."),
247+
self.get_error_message(),
246248
code="password_too_common",
247249
)
248250

251+
def get_error_message(self):
252+
return _("This password is too common.")
253+
249254
def get_help_text(self):
250255
return _("Your password can’t be a commonly used password.")
251256

@@ -258,9 +263,12 @@ class NumericPasswordValidator:
258263
def validate(self, password, user=None):
259264
if password.isdigit():
260265
raise ValidationError(
261-
_("This password is entirely numeric."),
266+
self.get_error_message(),
262267
code="password_entirely_numeric",
263268
)
264269

270+
def get_error_message(self):
271+
return _("This password is entirely numeric.")
272+
265273
def get_help_text(self):
266274
return _("Your password can’t be entirely numeric.")

docs/releases/5.2.txt

+4
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ Minor features
8282
improves performance. See :ref:`adding an async interface
8383
<writing-authentication-backends-async-interface>` for more details.
8484

85+
* The :ref:`password validator classes <included-password-validators>`
86+
now have a new method ``get_error_message()``, which can be overridden in
87+
subclasses to customize the error messages.
88+
8589
:mod:`django.contrib.contenttypes`
8690
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
8791

docs/topics/auth/passwords.txt

+35-4
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,8 @@ has no settings.
590590
The help texts and any errors from password validators are always returned in
591591
the order they are listed in :setting:`AUTH_PASSWORD_VALIDATORS`.
592592

593+
.. _included-password-validators:
594+
593595
Included validators
594596
-------------------
595597

@@ -600,10 +602,18 @@ Django includes four validators:
600602
Validates that the password is of a minimum length.
601603
The minimum length can be customized with the ``min_length`` parameter.
602604

605+
.. method:: get_error_message()
606+
607+
.. versionadded:: 5.2
608+
609+
A hook for customizing the ``ValidationError`` error message. Defaults
610+
to ``"This password is too short. It must contain at least <min_length>
611+
characters."``.
612+
603613
.. method:: get_help_text()
604614

605615
A hook for customizing the validator's help text. Defaults to ``"Your
606-
password must contain at least <min_length> characters."``
616+
password must contain at least <min_length> characters."``.
607617

608618
.. class:: UserAttributeSimilarityValidator(user_attributes=DEFAULT_USER_ATTRIBUTES, max_similarity=0.7)
609619

@@ -622,10 +632,17 @@ Django includes four validators:
622632
``user_attributes``, whereas a value of 1.0 rejects only passwords that are
623633
identical to an attribute's value.
624634

635+
.. method:: get_error_message()
636+
637+
.. versionadded:: 5.2
638+
639+
A hook for customizing the ``ValidationError`` error message. Defaults
640+
to ``"The password is too similar to the <user_attribute>."``.
641+
625642
.. method:: get_help_text()
626643

627644
A hook for customizing the validator's help text. Defaults to ``"Your
628-
password can’t be too similar to your other personal information."``
645+
password can’t be too similar to your other personal information."``.
629646

630647
.. class:: CommonPasswordValidator(password_list_path=DEFAULT_PASSWORD_LIST_PATH)
631648

@@ -638,19 +655,33 @@ Django includes four validators:
638655
common passwords. This file should contain one lowercase password per line
639656
and may be plain text or gzipped.
640657

658+
.. method:: get_error_message()
659+
660+
.. versionadded:: 5.2
661+
662+
A hook for customizing the ``ValidationError`` error message. Defaults
663+
to ``"This password is too common."``.
664+
641665
.. method:: get_help_text()
642666

643667
A hook for customizing the validator's help text. Defaults to ``"Your
644-
password can’t be a commonly used password."``
668+
password can’t be a commonly used password."``.
645669

646670
.. class:: NumericPasswordValidator()
647671

648672
Validate that the password is not entirely numeric.
649673

674+
.. method:: get_error_message()
675+
676+
.. versionadded:: 5.2
677+
678+
A hook for customizing the ``ValidationError`` error message. Defaults
679+
to ``"This password is entirely numeric."``.
680+
650681
.. method:: get_help_text()
651682

652683
A hook for customizing the validator's help text. Defaults to ``"Your
653-
password can’t be entirely numeric."``
684+
password can’t be entirely numeric."``.
654685

655686
Integrating validation
656687
----------------------

tests/auth_tests/test_validators.py

+70
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,20 @@ def test_help_text(self):
144144
"Your password must contain at least 8 characters.",
145145
)
146146

147+
def test_custom_error(self):
148+
class CustomMinimumLengthValidator(MinimumLengthValidator):
149+
def get_error_message(self):
150+
return "Your password must be %d characters long" % self.min_length
151+
152+
expected_error = "Your password must be %d characters long"
153+
154+
with self.assertRaisesMessage(ValidationError, expected_error % 8) as cm:
155+
CustomMinimumLengthValidator().validate("1234567")
156+
self.assertEqual(cm.exception.error_list[0].code, "password_too_short")
157+
158+
with self.assertRaisesMessage(ValidationError, expected_error % 3) as cm:
159+
CustomMinimumLengthValidator(min_length=3).validate("12")
160+
147161

148162
class UserAttributeSimilarityValidatorTest(TestCase):
149163
def test_validate(self):
@@ -213,6 +227,42 @@ def test_help_text(self):
213227
"Your password can’t be too similar to your other personal information.",
214228
)
215229

230+
def test_custom_error(self):
231+
class CustomUserAttributeSimilarityValidator(UserAttributeSimilarityValidator):
232+
def get_error_message(self):
233+
return "The password is too close to the %(verbose_name)s."
234+
235+
user = User.objects.create_user(
236+
username="testclient",
237+
password="password",
238+
239+
first_name="Test",
240+
last_name="Client",
241+
)
242+
243+
expected_error = "The password is too close to the %s."
244+
245+
with self.assertRaisesMessage(ValidationError, expected_error % "username"):
246+
CustomUserAttributeSimilarityValidator().validate("testclient", user=user)
247+
248+
def test_custom_error_verbose_name_not_used(self):
249+
class CustomUserAttributeSimilarityValidator(UserAttributeSimilarityValidator):
250+
def get_error_message(self):
251+
return "The password is too close to a user attribute."
252+
253+
user = User.objects.create_user(
254+
username="testclient",
255+
password="password",
256+
257+
first_name="Test",
258+
last_name="Client",
259+
)
260+
261+
expected_error = "The password is too close to a user attribute."
262+
263+
with self.assertRaisesMessage(ValidationError, expected_error):
264+
CustomUserAttributeSimilarityValidator().validate("testclient", user=user)
265+
216266

217267
class CommonPasswordValidatorTest(SimpleTestCase):
218268
def test_validate(self):
@@ -247,6 +297,16 @@ def test_help_text(self):
247297
"Your password can’t be a commonly used password.",
248298
)
249299

300+
def test_custom_error(self):
301+
class CustomCommonPasswordValidator(CommonPasswordValidator):
302+
def get_error_message(self):
303+
return "This password has been used too much."
304+
305+
expected_error = "This password has been used too much."
306+
307+
with self.assertRaisesMessage(ValidationError, expected_error):
308+
CustomCommonPasswordValidator().validate("godzilla")
309+
250310

251311
class NumericPasswordValidatorTest(SimpleTestCase):
252312
def test_validate(self):
@@ -264,6 +324,16 @@ def test_help_text(self):
264324
"Your password can’t be entirely numeric.",
265325
)
266326

327+
def test_custom_error(self):
328+
class CustomNumericPasswordValidator(NumericPasswordValidator):
329+
def get_error_message(self):
330+
return "This password is all digits."
331+
332+
expected_error = "This password is all digits."
333+
334+
with self.assertRaisesMessage(ValidationError, expected_error):
335+
CustomNumericPasswordValidator().validate("42424242")
336+
267337

268338
class UsernameValidatorsTests(SimpleTestCase):
269339
def test_unicode_validator(self):

0 commit comments

Comments
 (0)