2012-12-28 19:00:11 +00:00
|
|
|
=============================
|
|
|
|
Password management in Django
|
|
|
|
=============================
|
|
|
|
|
|
|
|
Password management is something that should generally not be reinvented
|
|
|
|
unnecessarily, and Django endeavors to provide a secure and flexible set of
|
|
|
|
tools for managing user passwords. This document describes how Django stores
|
|
|
|
passwords, how the storage hashing can be configured, and some utilities to
|
|
|
|
work with hashed passwords.
|
|
|
|
|
2015-03-30 10:25:51 +00:00
|
|
|
.. seealso::
|
|
|
|
|
|
|
|
Even though users may use strong passwords, attackers might be able to
|
|
|
|
eavesdrop on their connections. Use :ref:`HTTPS
|
|
|
|
<security-recommendation-ssl>` to avoid sending passwords (or any other
|
|
|
|
sensitive data) over plain HTTP connections because they will be vulnerable
|
|
|
|
to password sniffing.
|
|
|
|
|
2012-12-28 19:00:11 +00:00
|
|
|
.. _auth_password_storage:
|
|
|
|
|
|
|
|
How Django stores passwords
|
|
|
|
===========================
|
|
|
|
|
2012-12-26 20:47:29 +00:00
|
|
|
Django provides a flexible password storage system and uses PBKDF2 by default.
|
2012-12-28 19:00:11 +00:00
|
|
|
|
|
|
|
The :attr:`~django.contrib.auth.models.User.password` attribute of a
|
|
|
|
:class:`~django.contrib.auth.models.User` object is a string in this format:
|
2023-01-23 20:29:05 +00:00
|
|
|
|
2012-12-28 19:00:11 +00:00
|
|
|
.. code-block:: text
|
|
|
|
|
2013-01-05 01:42:25 +00:00
|
|
|
<algorithm>$<iterations>$<salt>$<hash>
|
2012-12-28 19:00:11 +00:00
|
|
|
|
2013-01-05 01:42:25 +00:00
|
|
|
Those are the components used for storing a User's password, separated by the
|
|
|
|
dollar-sign character and consist of: the hashing algorithm, the number of
|
|
|
|
algorithm iterations (work factor), the random salt, and the resulting password
|
|
|
|
hash. The algorithm is one of a number of one-way hashing or password storage
|
|
|
|
algorithms Django can use; see below. Iterations describe the number of times
|
|
|
|
the algorithm is run over the hash. Salt is the random seed used and the hash
|
|
|
|
is the result of the one-way function.
|
2012-12-28 19:00:11 +00:00
|
|
|
|
|
|
|
By default, Django uses the PBKDF2_ algorithm with a SHA256 hash, a
|
|
|
|
password stretching mechanism recommended by NIST_. This should be
|
|
|
|
sufficient for most users: it's quite secure, requiring massive
|
|
|
|
amounts of computing time to break.
|
|
|
|
|
|
|
|
However, depending on your requirements, you may choose a different
|
|
|
|
algorithm, or even use a custom algorithm to match your specific
|
|
|
|
security situation. Again, most users shouldn't need to do this -- if
|
|
|
|
you're not sure, you probably don't. If you do, please read on:
|
|
|
|
|
2013-01-05 01:42:25 +00:00
|
|
|
Django chooses the algorithm to use by consulting the
|
|
|
|
:setting:`PASSWORD_HASHERS` setting. This is a list of hashing algorithm
|
2022-01-13 19:46:18 +00:00
|
|
|
classes that this Django installation supports.
|
|
|
|
|
|
|
|
For storing passwords, Django will use the first hasher in
|
|
|
|
:setting:`PASSWORD_HASHERS`. To store new passwords with a different algorithm,
|
|
|
|
put your preferred algorithm first in :setting:`PASSWORD_HASHERS`.
|
|
|
|
|
|
|
|
For verifying passwords, Django will find the hasher in the list that matches
|
|
|
|
the algorithm name in the stored password. If a stored password names an
|
|
|
|
algorithm not found in :setting:`PASSWORD_HASHERS`, trying to verify it will
|
|
|
|
raise ``ValueError``.
|
2012-12-28 19:00:11 +00:00
|
|
|
|
|
|
|
The default for :setting:`PASSWORD_HASHERS` is::
|
|
|
|
|
2015-01-21 16:55:57 +00:00
|
|
|
PASSWORD_HASHERS = [
|
2012-12-28 19:00:11 +00:00
|
|
|
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
|
|
|
|
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
|
2015-12-26 12:14:07 +00:00
|
|
|
"django.contrib.auth.hashers.Argon2PasswordHasher",
|
2013-03-26 15:44:26 +00:00
|
|
|
"django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
|
2020-12-27 02:54:47 +00:00
|
|
|
"django.contrib.auth.hashers.ScryptPasswordHasher",
|
2015-01-21 16:55:57 +00:00
|
|
|
]
|
2012-12-28 19:00:11 +00:00
|
|
|
|
2016-02-08 19:22:38 +00:00
|
|
|
This means that Django will use PBKDF2_ to store all passwords but will support
|
2015-12-26 12:14:07 +00:00
|
|
|
checking passwords stored with PBKDF2SHA1, argon2_, and bcrypt_.
|
2016-02-08 19:22:38 +00:00
|
|
|
|
|
|
|
The next few sections describe a couple of common ways advanced users may want
|
|
|
|
to modify this setting.
|
2012-12-28 19:00:11 +00:00
|
|
|
|
2015-12-26 12:14:07 +00:00
|
|
|
.. _argon2_usage:
|
|
|
|
|
|
|
|
Using Argon2 with Django
|
|
|
|
------------------------
|
|
|
|
|
|
|
|
Argon2_ is the winner of the 2015 `Password Hashing Competition`_, a community
|
|
|
|
organized open competition to select a next generation hashing algorithm. It's
|
|
|
|
designed not to be easier to compute on custom hardware than it is to compute
|
2022-09-17 07:49:09 +00:00
|
|
|
on an ordinary CPU. The default variant for the Argon2 password hasher is
|
|
|
|
Argon2id.
|
2015-12-26 12:14:07 +00:00
|
|
|
|
|
|
|
Argon2_ is not the default for Django because it requires a third-party
|
|
|
|
library. The Password Hashing Competition panel, however, recommends immediate
|
|
|
|
use of Argon2 rather than the other algorithms supported by Django.
|
|
|
|
|
2022-09-17 07:49:09 +00:00
|
|
|
To use Argon2id as your default storage algorithm, do the following:
|
2015-12-26 12:14:07 +00:00
|
|
|
|
2023-04-16 23:14:09 +00:00
|
|
|
#. Install the :pypi:`argon2-cffi` package. This can be done by running
|
2019-04-14 15:02:36 +00:00
|
|
|
``python -m pip install django[argon2]``, which is equivalent to
|
|
|
|
``python -m pip install argon2-cffi`` (along with any version requirement
|
2019-11-07 14:53:30 +00:00
|
|
|
from Django's ``setup.cfg``).
|
2015-12-26 12:14:07 +00:00
|
|
|
|
2018-11-15 18:54:28 +00:00
|
|
|
#. Modify :setting:`PASSWORD_HASHERS` to list ``Argon2PasswordHasher`` first.
|
2015-12-26 12:14:07 +00:00
|
|
|
That is, in your settings file, you'd put::
|
|
|
|
|
|
|
|
PASSWORD_HASHERS = [
|
|
|
|
"django.contrib.auth.hashers.Argon2PasswordHasher",
|
|
|
|
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
|
|
|
|
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
|
|
|
|
"django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
|
2020-12-27 02:54:47 +00:00
|
|
|
"django.contrib.auth.hashers.ScryptPasswordHasher",
|
2015-12-26 12:14:07 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
Keep and/or add any entries in this list if you need Django to :ref:`upgrade
|
|
|
|
passwords <password-upgrades>`.
|
|
|
|
|
2012-12-28 19:00:11 +00:00
|
|
|
.. _bcrypt_usage:
|
|
|
|
|
2016-01-24 21:26:11 +00:00
|
|
|
Using ``bcrypt`` with Django
|
|
|
|
----------------------------
|
2012-12-28 19:00:11 +00:00
|
|
|
|
|
|
|
Bcrypt_ is a popular password storage algorithm that's specifically designed
|
|
|
|
for long-term password storage. It's not the default used by Django since it
|
|
|
|
requires the use of third-party libraries, but since many people may want to
|
|
|
|
use it Django supports bcrypt with minimal effort.
|
|
|
|
|
|
|
|
To use Bcrypt as your default storage algorithm, do the following:
|
|
|
|
|
2023-04-16 23:14:09 +00:00
|
|
|
#. Install the :pypi:`bcrypt` package. This can be done by running
|
2019-04-14 15:02:36 +00:00
|
|
|
``python -m pip install django[bcrypt]``, which is equivalent to
|
|
|
|
``python -m pip install bcrypt`` (along with any version requirement from
|
2019-11-07 14:53:30 +00:00
|
|
|
Django's ``setup.cfg``).
|
2012-12-28 19:00:11 +00:00
|
|
|
|
2018-11-15 18:54:28 +00:00
|
|
|
#. Modify :setting:`PASSWORD_HASHERS` to list ``BCryptSHA256PasswordHasher``
|
2012-12-28 19:00:11 +00:00
|
|
|
first. That is, in your settings file, you'd put::
|
|
|
|
|
2015-01-21 16:55:57 +00:00
|
|
|
PASSWORD_HASHERS = [
|
2013-03-26 15:44:26 +00:00
|
|
|
"django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
|
2012-12-28 19:00:11 +00:00
|
|
|
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
|
|
|
|
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
|
2015-12-26 12:14:07 +00:00
|
|
|
"django.contrib.auth.hashers.Argon2PasswordHasher",
|
2020-12-27 02:54:47 +00:00
|
|
|
"django.contrib.auth.hashers.ScryptPasswordHasher",
|
2015-01-21 16:55:57 +00:00
|
|
|
]
|
2012-12-28 19:00:11 +00:00
|
|
|
|
2016-02-08 19:22:38 +00:00
|
|
|
Keep and/or add any entries in this list if you need Django to :ref:`upgrade
|
|
|
|
passwords <password-upgrades>`.
|
2012-12-28 19:00:11 +00:00
|
|
|
|
|
|
|
That's it -- now your Django install will use Bcrypt as the default storage
|
|
|
|
algorithm.
|
|
|
|
|
2020-12-27 02:54:47 +00:00
|
|
|
.. _scrypt-usage:
|
|
|
|
|
|
|
|
Using ``scrypt`` with Django
|
|
|
|
----------------------------
|
|
|
|
|
|
|
|
scrypt_ is similar to PBKDF2 and bcrypt in utilizing a set number of iterations
|
|
|
|
to slow down brute-force attacks. However, because PBKDF2 and bcrypt do not
|
|
|
|
require a lot of memory, attackers with sufficient resources can launch
|
|
|
|
large-scale parallel attacks in order to speed up the attacking process.
|
|
|
|
scrypt_ is specifically designed to use more memory compared to other
|
|
|
|
password-based key derivation functions in order to limit the amount of
|
|
|
|
parallelism an attacker can use, see :rfc:`7914` for more details.
|
|
|
|
|
|
|
|
To use scrypt_ as your default storage algorithm, do the following:
|
|
|
|
|
|
|
|
#. Modify :setting:`PASSWORD_HASHERS` to list ``ScryptPasswordHasher`` first.
|
|
|
|
That is, in your settings file::
|
|
|
|
|
|
|
|
PASSWORD_HASHERS = [
|
|
|
|
"django.contrib.auth.hashers.ScryptPasswordHasher",
|
|
|
|
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
|
|
|
|
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
|
|
|
|
"django.contrib.auth.hashers.Argon2PasswordHasher",
|
|
|
|
"django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
|
|
|
|
]
|
|
|
|
|
|
|
|
Keep and/or add any entries in this list if you need Django to :ref:`upgrade
|
|
|
|
passwords <password-upgrades>`.
|
|
|
|
|
|
|
|
.. note::
|
|
|
|
|
|
|
|
``scrypt`` requires OpenSSL 1.1+.
|
|
|
|
|
2020-06-25 02:28:07 +00:00
|
|
|
Increasing the salt entropy
|
|
|
|
---------------------------
|
|
|
|
|
|
|
|
Most password hashes include a salt along with their password hash in order to
|
|
|
|
protect against rainbow table attacks. The salt itself is a random value which
|
|
|
|
increases the size and thus the cost of the rainbow table and is currently set
|
|
|
|
at 128 bits with the ``salt_entropy`` value in the ``BasePasswordHasher``. As
|
|
|
|
computing and storage costs decrease this value should be raised. When
|
|
|
|
implementing your own password hasher you are free to override this value in
|
|
|
|
order to use a desired entropy level for your password hashes. ``salt_entropy``
|
|
|
|
is measured in bits.
|
|
|
|
|
|
|
|
.. admonition:: Implementation detail
|
|
|
|
|
|
|
|
Due to the method in which salt values are stored the ``salt_entropy``
|
|
|
|
value is effectively a minimum value. For instance a value of 128 would
|
|
|
|
provide a salt which would actually contain 131 bits of entropy.
|
|
|
|
|
2013-10-18 13:09:56 +00:00
|
|
|
.. _increasing-password-algorithm-work-factor:
|
|
|
|
|
2012-12-28 19:00:11 +00:00
|
|
|
Increasing the work factor
|
|
|
|
--------------------------
|
|
|
|
|
2015-12-26 12:14:07 +00:00
|
|
|
PBKDF2 and bcrypt
|
|
|
|
~~~~~~~~~~~~~~~~~
|
|
|
|
|
2012-12-28 19:00:11 +00:00
|
|
|
The PBKDF2 and bcrypt algorithms use a number of iterations or rounds of
|
|
|
|
hashing. This deliberately slows down attackers, making attacks against hashed
|
|
|
|
passwords harder. However, as computing power increases, the number of
|
|
|
|
iterations needs to be increased. We've chosen a reasonable default (and will
|
|
|
|
increase it with each release of Django), but you may wish to tune it up or
|
|
|
|
down, depending on your security needs and available processing power. To do so,
|
|
|
|
you'll subclass the appropriate algorithm and override the ``iterations``
|
2021-04-01 00:43:09 +00:00
|
|
|
parameter (use the ``rounds`` parameter when subclassing a bcrypt hasher). For
|
|
|
|
example, to increase the number of iterations used by the default PBKDF2
|
|
|
|
algorithm:
|
2012-12-28 19:00:11 +00:00
|
|
|
|
2018-11-15 18:54:28 +00:00
|
|
|
#. Create a subclass of ``django.contrib.auth.hashers.PBKDF2PasswordHasher``
|
|
|
|
::
|
2012-12-28 19:00:11 +00:00
|
|
|
|
|
|
|
from django.contrib.auth.hashers import PBKDF2PasswordHasher
|
|
|
|
|
2023-03-01 12:35:43 +00:00
|
|
|
|
2012-12-28 19:00:11 +00:00
|
|
|
class MyPBKDF2PasswordHasher(PBKDF2PasswordHasher):
|
|
|
|
"""
|
|
|
|
A subclass of PBKDF2PasswordHasher that uses 100 times more iterations.
|
|
|
|
"""
|
2023-03-01 12:35:43 +00:00
|
|
|
|
2012-12-28 19:00:11 +00:00
|
|
|
iterations = PBKDF2PasswordHasher.iterations * 100
|
|
|
|
|
|
|
|
Save this somewhere in your project. For example, you might put this in
|
|
|
|
a file like ``myproject/hashers.py``.
|
|
|
|
|
2018-11-15 18:54:28 +00:00
|
|
|
#. Add your new hasher as the first entry in :setting:`PASSWORD_HASHERS`::
|
2012-12-28 19:00:11 +00:00
|
|
|
|
2015-01-21 16:55:57 +00:00
|
|
|
PASSWORD_HASHERS = [
|
2012-12-28 19:00:11 +00:00
|
|
|
"myproject.hashers.MyPBKDF2PasswordHasher",
|
|
|
|
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
|
|
|
|
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
|
2015-12-26 12:14:07 +00:00
|
|
|
"django.contrib.auth.hashers.Argon2PasswordHasher",
|
2013-03-26 15:44:26 +00:00
|
|
|
"django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
|
2020-12-27 02:54:47 +00:00
|
|
|
"django.contrib.auth.hashers.ScryptPasswordHasher",
|
2015-01-21 16:55:57 +00:00
|
|
|
]
|
2012-12-28 19:00:11 +00:00
|
|
|
|
|
|
|
That's it -- now your Django install will use more iterations when it
|
|
|
|
stores passwords using PBKDF2.
|
|
|
|
|
2021-04-01 00:43:09 +00:00
|
|
|
.. note::
|
|
|
|
|
|
|
|
bcrypt ``rounds`` is a logarithmic work factor, e.g. 12 rounds means
|
|
|
|
``2 ** 12`` iterations.
|
|
|
|
|
2015-12-26 12:14:07 +00:00
|
|
|
Argon2
|
|
|
|
~~~~~~
|
|
|
|
|
2021-12-28 11:36:57 +00:00
|
|
|
Argon2 has the following attributes that can be customized:
|
2015-12-26 12:14:07 +00:00
|
|
|
|
|
|
|
#. ``time_cost`` controls the number of iterations within the hash.
|
|
|
|
#. ``memory_cost`` controls the size of memory that must be used during the
|
|
|
|
computation of the hash.
|
|
|
|
#. ``parallelism`` controls how many CPUs the computation of the hash can be
|
|
|
|
parallelized on.
|
|
|
|
|
|
|
|
The default values of these attributes are probably fine for you. If you
|
|
|
|
determine that the password hash is too fast or too slow, you can tweak it as
|
|
|
|
follows:
|
|
|
|
|
|
|
|
#. Choose ``parallelism`` to be the number of threads you can
|
|
|
|
spare computing the hash.
|
|
|
|
#. Choose ``memory_cost`` to be the KiB of memory you can spare.
|
|
|
|
#. Adjust ``time_cost`` and measure the time hashing a password takes.
|
|
|
|
Pick a ``time_cost`` that takes an acceptable time for you.
|
|
|
|
If ``time_cost`` set to 1 is unacceptably slow, lower ``memory_cost``.
|
|
|
|
|
2016-05-27 08:41:52 +00:00
|
|
|
.. admonition:: ``memory_cost`` interpretation
|
|
|
|
|
|
|
|
The argon2 command-line utility and some other libraries interpret the
|
|
|
|
``memory_cost`` parameter differently from the value that Django uses. The
|
|
|
|
conversion is given by ``memory_cost == 2 ** memory_cost_commandline``.
|
|
|
|
|
2020-12-27 02:54:47 +00:00
|
|
|
``scrypt``
|
|
|
|
~~~~~~~~~~
|
|
|
|
|
2021-12-28 11:36:57 +00:00
|
|
|
scrypt_ has the following attributes that can be customized:
|
2020-12-27 02:54:47 +00:00
|
|
|
|
|
|
|
#. ``work_factor`` controls the number of iterations within the hash.
|
|
|
|
#. ``block_size``
|
|
|
|
#. ``parallelism`` controls how many threads will run in parallel.
|
|
|
|
#. ``maxmem`` limits the maximum size of memory that can be used during the
|
|
|
|
computation of the hash. Defaults to ``0``, which means the default
|
|
|
|
limitation from the OpenSSL library.
|
|
|
|
|
|
|
|
We've chosen reasonable defaults, but you may wish to tune it up or down,
|
|
|
|
depending on your security needs and available processing power.
|
|
|
|
|
|
|
|
.. admonition:: Estimating memory usage
|
|
|
|
|
|
|
|
The minimum memory requirement of scrypt_ is::
|
|
|
|
|
|
|
|
work_factor * 2 * block_size * 64
|
|
|
|
|
|
|
|
so you may need to tweak ``maxmem`` when changing the ``work_factor`` or
|
|
|
|
``block_size`` values.
|
|
|
|
|
2013-10-18 13:09:56 +00:00
|
|
|
.. _password-upgrades:
|
|
|
|
|
2012-12-28 19:00:11 +00:00
|
|
|
Password upgrading
|
|
|
|
------------------
|
|
|
|
|
|
|
|
When users log in, if their passwords are stored with anything other than
|
|
|
|
the preferred algorithm, Django will automatically upgrade the algorithm
|
|
|
|
to the preferred one. This means that old installs of Django will get
|
|
|
|
automatically more secure as users log in, and it also means that you
|
|
|
|
can switch to new (and better) storage algorithms as they get invented.
|
|
|
|
|
|
|
|
However, Django can only upgrade passwords that use algorithms mentioned in
|
|
|
|
:setting:`PASSWORD_HASHERS`, so as you upgrade to new systems you should make
|
2014-02-28 16:44:03 +00:00
|
|
|
sure never to *remove* entries from this list. If you do, users using
|
2015-09-22 23:30:31 +00:00
|
|
|
unmentioned algorithms won't be able to upgrade. Hashed passwords will be
|
2020-08-10 21:30:39 +00:00
|
|
|
updated when increasing (or decreasing) the number of PBKDF2 iterations, bcrypt
|
|
|
|
rounds, or argon2 attributes.
|
2015-09-22 23:30:31 +00:00
|
|
|
|
2016-02-13 20:09:46 +00:00
|
|
|
Be aware that if all the passwords in your database aren't encoded in the
|
|
|
|
default hasher's algorithm, you may be vulnerable to a user enumeration timing
|
|
|
|
attack due to a difference between the duration of a login request for a user
|
|
|
|
with a password encoded in a non-default algorithm and the duration of a login
|
|
|
|
request for a nonexistent user (which runs the default hasher). You may be able
|
|
|
|
to mitigate this by :ref:`upgrading older password hashes
|
|
|
|
<wrapping-password-hashers>`.
|
|
|
|
|
2016-02-10 15:20:02 +00:00
|
|
|
.. _wrapping-password-hashers:
|
|
|
|
|
|
|
|
Password upgrading without requiring a login
|
|
|
|
--------------------------------------------
|
|
|
|
|
2022-07-23 10:45:24 +00:00
|
|
|
If you have an existing database with an older, weak hash such as MD5, you
|
|
|
|
might want to upgrade those hashes yourself instead of waiting for the upgrade
|
|
|
|
to happen when a user logs in (which may never happen if a user doesn't return
|
|
|
|
to your site). In this case, you can use a "wrapped" password hasher.
|
2016-02-10 15:20:02 +00:00
|
|
|
|
2022-07-23 10:45:24 +00:00
|
|
|
For this example, we'll migrate a collection of MD5 hashes to use
|
2023-01-30 07:31:39 +00:00
|
|
|
PBKDF2(MD5(password)) and add the corresponding password hasher for checking
|
2016-02-10 15:20:02 +00:00
|
|
|
if a user entered the correct password on login. We assume we're using the
|
|
|
|
built-in ``User`` model and that our project has an ``accounts`` app. You can
|
|
|
|
modify the pattern to work with any algorithm or with a custom user model.
|
|
|
|
|
|
|
|
First, we'll add the custom hasher:
|
|
|
|
|
2018-09-10 17:00:34 +00:00
|
|
|
.. code-block:: python
|
2022-05-31 05:40:54 +00:00
|
|
|
:caption: ``accounts/hashers.py``
|
2016-02-10 15:20:02 +00:00
|
|
|
|
|
|
|
from django.contrib.auth.hashers import (
|
2022-07-23 10:45:24 +00:00
|
|
|
PBKDF2PasswordHasher,
|
|
|
|
MD5PasswordHasher,
|
2016-02-10 15:20:02 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2022-07-23 10:45:24 +00:00
|
|
|
class PBKDF2WrappedMD5PasswordHasher(PBKDF2PasswordHasher):
|
|
|
|
algorithm = "pbkdf2_wrapped_md5"
|
2016-02-10 15:20:02 +00:00
|
|
|
|
2022-07-23 10:45:24 +00:00
|
|
|
def encode_md5_hash(self, md5_hash, salt, iterations=None):
|
|
|
|
return super().encode(md5_hash, salt, iterations)
|
2016-02-10 15:20:02 +00:00
|
|
|
|
|
|
|
def encode(self, password, salt, iterations=None):
|
2022-07-23 10:45:24 +00:00
|
|
|
_, _, md5_hash = MD5PasswordHasher().encode(password, salt).split("$", 2)
|
|
|
|
return self.encode_md5_hash(md5_hash, salt, iterations)
|
2016-02-10 15:20:02 +00:00
|
|
|
|
|
|
|
The data migration might look something like:
|
|
|
|
|
2018-09-10 17:00:34 +00:00
|
|
|
.. code-block:: python
|
2022-07-23 10:45:24 +00:00
|
|
|
:caption: ``accounts/migrations/0002_migrate_md5_passwords.py``
|
2016-02-10 15:20:02 +00:00
|
|
|
|
|
|
|
from django.db import migrations
|
|
|
|
|
2022-07-23 10:45:24 +00:00
|
|
|
from ..hashers import PBKDF2WrappedMD5PasswordHasher
|
2016-02-10 15:20:02 +00:00
|
|
|
|
|
|
|
|
|
|
|
def forwards_func(apps, schema_editor):
|
|
|
|
User = apps.get_model("auth", "User")
|
2022-07-23 10:45:24 +00:00
|
|
|
users = User.objects.filter(password__startswith="md5$")
|
|
|
|
hasher = PBKDF2WrappedMD5PasswordHasher()
|
2016-02-10 15:20:02 +00:00
|
|
|
for user in users:
|
2022-07-23 10:45:24 +00:00
|
|
|
algorithm, salt, md5_hash = user.password.split("$", 2)
|
|
|
|
user.password = hasher.encode_md5_hash(md5_hash, salt)
|
2016-02-10 15:20:02 +00:00
|
|
|
user.save(update_fields=["password"])
|
|
|
|
|
|
|
|
|
|
|
|
class Migration(migrations.Migration):
|
|
|
|
dependencies = [
|
|
|
|
("accounts", "0001_initial"),
|
|
|
|
# replace this with the latest migration in contrib.auth
|
|
|
|
("auth", "####_migration_name"),
|
|
|
|
]
|
|
|
|
|
|
|
|
operations = [
|
|
|
|
migrations.RunPython(forwards_func),
|
|
|
|
]
|
|
|
|
|
|
|
|
Be aware that this migration will take on the order of several minutes for
|
|
|
|
several thousand users, depending on the speed of your hardware.
|
|
|
|
|
|
|
|
Finally, we'll add a :setting:`PASSWORD_HASHERS` setting:
|
|
|
|
|
2018-09-10 17:00:34 +00:00
|
|
|
.. code-block:: python
|
2022-05-31 05:40:54 +00:00
|
|
|
:caption: ``mysite/settings.py``
|
2016-02-10 15:20:02 +00:00
|
|
|
|
|
|
|
PASSWORD_HASHERS = [
|
|
|
|
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
|
2022-07-23 10:45:24 +00:00
|
|
|
"accounts.hashers.PBKDF2WrappedMD5PasswordHasher",
|
2016-02-10 15:20:02 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
Include any other hashers that your site uses in this list.
|
|
|
|
|
2015-08-08 10:02:32 +00:00
|
|
|
.. _pbkdf2: https://en.wikipedia.org/wiki/PBKDF2
|
2021-04-27 11:09:00 +00:00
|
|
|
.. _nist: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf
|
2015-08-08 10:02:32 +00:00
|
|
|
.. _bcrypt: https://en.wikipedia.org/wiki/Bcrypt
|
2015-12-26 12:14:07 +00:00
|
|
|
.. _argon2: https://en.wikipedia.org/wiki/Argon2
|
2020-12-27 02:54:47 +00:00
|
|
|
.. _scrypt: https://en.wikipedia.org/wiki/Scrypt
|
2021-04-27 11:09:00 +00:00
|
|
|
.. _`Password Hashing Competition`: https://www.password-hashing.net/
|
2012-12-28 19:00:11 +00:00
|
|
|
|
2016-02-08 19:22:38 +00:00
|
|
|
.. _auth-included-hashers:
|
|
|
|
|
|
|
|
Included hashers
|
|
|
|
----------------
|
|
|
|
|
|
|
|
The full list of hashers included in Django is::
|
|
|
|
|
|
|
|
[
|
|
|
|
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
|
|
|
|
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
|
2015-12-26 12:14:07 +00:00
|
|
|
"django.contrib.auth.hashers.Argon2PasswordHasher",
|
2016-02-08 19:22:38 +00:00
|
|
|
"django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
|
|
|
|
"django.contrib.auth.hashers.BCryptPasswordHasher",
|
2020-12-27 02:54:47 +00:00
|
|
|
"django.contrib.auth.hashers.ScryptPasswordHasher",
|
2016-02-08 19:22:38 +00:00
|
|
|
"django.contrib.auth.hashers.MD5PasswordHasher",
|
|
|
|
]
|
|
|
|
|
|
|
|
The corresponding algorithm names are:
|
|
|
|
|
|
|
|
* ``pbkdf2_sha256``
|
|
|
|
* ``pbkdf2_sha1``
|
2015-12-26 12:14:07 +00:00
|
|
|
* ``argon2``
|
2016-02-08 19:22:38 +00:00
|
|
|
* ``bcrypt_sha256``
|
|
|
|
* ``bcrypt``
|
2020-12-27 02:54:47 +00:00
|
|
|
* ``scrypt``
|
2016-02-08 19:22:38 +00:00
|
|
|
* ``md5``
|
|
|
|
|
2016-02-13 20:09:46 +00:00
|
|
|
.. _write-your-own-password-hasher:
|
|
|
|
|
|
|
|
Writing your own hasher
|
|
|
|
-----------------------
|
|
|
|
|
|
|
|
If you write your own password hasher that contains a work factor such as a
|
|
|
|
number of iterations, you should implement a
|
|
|
|
``harden_runtime(self, password, encoded)`` method to bridge the runtime gap
|
|
|
|
between the work factor supplied in the ``encoded`` password and the default
|
|
|
|
work factor of the hasher. This prevents a user enumeration timing attack due
|
|
|
|
to difference between a login request for a user with a password encoded in an
|
|
|
|
older number of iterations and a nonexistent user (which runs the default
|
|
|
|
hasher's default number of iterations).
|
|
|
|
|
|
|
|
Taking PBKDF2 as example, if ``encoded`` contains 20,000 iterations and the
|
|
|
|
hasher's default ``iterations`` is 30,000, the method should run ``password``
|
|
|
|
through another 10,000 iterations of PBKDF2.
|
|
|
|
|
|
|
|
If your hasher doesn't have a work factor, implement the method as a no-op
|
|
|
|
(``pass``).
|
|
|
|
|
2012-12-28 19:00:11 +00:00
|
|
|
Manually managing a user's password
|
|
|
|
===================================
|
|
|
|
|
|
|
|
.. module:: django.contrib.auth.hashers
|
|
|
|
|
2013-01-01 13:12:42 +00:00
|
|
|
The :mod:`django.contrib.auth.hashers` module provides a set of functions
|
2018-04-23 11:37:26 +00:00
|
|
|
to create and validate hashed passwords. You can use them independently
|
2013-01-01 13:12:42 +00:00
|
|
|
from the ``User`` model.
|
2012-12-28 19:00:11 +00:00
|
|
|
|
2022-11-28 07:13:51 +00:00
|
|
|
.. function:: check_password(password, encoded, setter=None, preferred="default")
|
2012-12-28 19:00:11 +00:00
|
|
|
|
|
|
|
If you'd like to manually authenticate a user by comparing a plain-text
|
|
|
|
password to the hashed password in the database, use the convenience
|
2022-11-28 07:13:51 +00:00
|
|
|
function :func:`check_password`. It takes two mandatory arguments: the
|
|
|
|
plain-text password to check, and the full value of a user's ``password``
|
|
|
|
field in the database to check against. It returns ``True`` if they match,
|
|
|
|
``False`` otherwise. Optionally, you can pass a callable ``setter`` that
|
|
|
|
takes the password and will be called when you need to regenerate it. You
|
|
|
|
can also pass ``preferred`` to change a hashing algorithm if you don't want
|
|
|
|
to use the default (first entry of ``PASSWORD_HASHERS`` setting). See
|
|
|
|
:ref:`auth-included-hashers` for the algorithm name of each hasher.
|
2012-12-28 19:00:11 +00:00
|
|
|
|
2014-10-27 04:49:07 +00:00
|
|
|
.. function:: make_password(password, salt=None, hasher='default')
|
2012-12-28 19:00:11 +00:00
|
|
|
|
|
|
|
Creates a hashed password in the format used by this application. It takes
|
2020-03-26 12:23:32 +00:00
|
|
|
one mandatory argument: the password in plain-text (string or bytes).
|
|
|
|
Optionally, you can provide a salt and a hashing algorithm to use, if you
|
|
|
|
don't want to use the defaults (first entry of ``PASSWORD_HASHERS``
|
|
|
|
setting). See :ref:`auth-included-hashers` for the algorithm name of each
|
|
|
|
hasher. If the password argument is ``None``, an unusable password is
|
|
|
|
returned (one that will never be accepted by :func:`check_password`).
|
|
|
|
|
2012-12-28 19:00:11 +00:00
|
|
|
.. function:: is_password_usable(encoded_password)
|
|
|
|
|
2018-03-20 21:19:27 +00:00
|
|
|
Returns ``False`` if the password is a result of
|
|
|
|
:meth:`.User.set_unusable_password`.
|
|
|
|
|
2015-03-08 14:07:57 +00:00
|
|
|
.. _password-validation:
|
|
|
|
|
|
|
|
Password validation
|
|
|
|
===================
|
|
|
|
|
2015-06-08 17:27:47 +00:00
|
|
|
.. module:: django.contrib.auth.password_validation
|
|
|
|
|
2015-03-08 14:07:57 +00:00
|
|
|
Users often choose poor passwords. To help mitigate this problem, Django
|
|
|
|
offers pluggable password validation. You can configure multiple password
|
2019-06-17 14:54:55 +00:00
|
|
|
validators at the same time. A few validators are included in Django, but you
|
|
|
|
can write your own as well.
|
2015-03-08 14:07:57 +00:00
|
|
|
|
|
|
|
Each password validator must provide a help text to explain the requirements to
|
|
|
|
the user, validate a given password and return an error message if it does not
|
|
|
|
meet the requirements, and optionally receive passwords that have been set.
|
|
|
|
Validators can also have optional settings to fine tune their behavior.
|
|
|
|
|
|
|
|
Validation is controlled by the :setting:`AUTH_PASSWORD_VALIDATORS` setting.
|
2015-06-08 17:27:47 +00:00
|
|
|
The default for the setting is an empty list, which means no validators are
|
2015-03-08 14:07:57 +00:00
|
|
|
applied. In new projects created with the default :djadmin:`startproject`
|
2019-06-17 14:54:55 +00:00
|
|
|
template, a set of validators is enabled by default.
|
2015-03-08 14:07:57 +00:00
|
|
|
|
2016-08-10 19:52:16 +00:00
|
|
|
By default, validators are used in the forms to reset or change passwords and
|
|
|
|
in the :djadmin:`createsuperuser` and :djadmin:`changepassword` management
|
|
|
|
commands. Validators aren't applied at the model level, for example in
|
|
|
|
``User.objects.create_user()`` and ``create_superuser()``, because we assume
|
|
|
|
that developers, not users, interact with Django at that level and also because
|
|
|
|
model validation doesn't automatically run as part of creating models.
|
|
|
|
|
2015-03-08 14:07:57 +00:00
|
|
|
.. note::
|
|
|
|
|
|
|
|
Password validation can prevent the use of many types of weak passwords.
|
2015-06-08 17:27:47 +00:00
|
|
|
However, the fact that a password passes all the validators doesn't
|
2015-03-08 14:07:57 +00:00
|
|
|
guarantee that it is a strong password. There are many factors that can
|
|
|
|
weaken a password that are not detectable by even the most advanced
|
|
|
|
password validators.
|
|
|
|
|
|
|
|
Enabling password validation
|
|
|
|
----------------------------
|
|
|
|
|
|
|
|
Password validation is configured in the
|
|
|
|
:setting:`AUTH_PASSWORD_VALIDATORS` setting::
|
|
|
|
|
|
|
|
AUTH_PASSWORD_VALIDATORS = [
|
|
|
|
{
|
|
|
|
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
|
|
|
"OPTIONS": {
|
|
|
|
"min_length": 9,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
|
|
|
},
|
|
|
|
]
|
|
|
|
|
|
|
|
This example enables all four included validators:
|
|
|
|
|
|
|
|
* ``UserAttributeSimilarityValidator``, which checks the similarity between
|
|
|
|
the password and a set of attributes of the user.
|
2019-06-17 14:54:55 +00:00
|
|
|
* ``MinimumLengthValidator``, which checks whether the password meets a minimum
|
|
|
|
length. This validator is configured with a custom option: it now requires
|
|
|
|
the minimum length to be nine characters, instead of the default eight.
|
2015-03-08 14:07:57 +00:00
|
|
|
* ``CommonPasswordValidator``, which checks whether the password occurs in a
|
|
|
|
list of common passwords. By default, it compares to an included list of
|
2018-04-10 16:02:03 +00:00
|
|
|
20,000 common passwords.
|
2015-03-08 14:07:57 +00:00
|
|
|
* ``NumericPasswordValidator``, which checks whether the password isn't
|
|
|
|
entirely numeric.
|
|
|
|
|
|
|
|
For ``UserAttributeSimilarityValidator`` and ``CommonPasswordValidator``,
|
2019-06-17 14:54:55 +00:00
|
|
|
we're using the default settings in this example. ``NumericPasswordValidator``
|
|
|
|
has no settings.
|
2015-03-08 14:07:57 +00:00
|
|
|
|
|
|
|
The help texts and any errors from password validators are always returned in
|
|
|
|
the order they are listed in :setting:`AUTH_PASSWORD_VALIDATORS`.
|
|
|
|
|
|
|
|
Included validators
|
|
|
|
-------------------
|
|
|
|
|
|
|
|
Django includes four validators:
|
|
|
|
|
|
|
|
.. class:: MinimumLengthValidator(min_length=8)
|
|
|
|
|
2021-12-13 17:53:07 +00:00
|
|
|
Validates that the password is of a minimum length.
|
2015-03-08 14:07:57 +00:00
|
|
|
The minimum length can be customized with the ``min_length`` parameter.
|
|
|
|
|
|
|
|
.. class:: UserAttributeSimilarityValidator(user_attributes=DEFAULT_USER_ATTRIBUTES, max_similarity=0.7)
|
|
|
|
|
2021-12-13 17:53:07 +00:00
|
|
|
Validates that the password is sufficiently different from certain
|
2015-03-08 14:07:57 +00:00
|
|
|
attributes of the user.
|
|
|
|
|
|
|
|
The ``user_attributes`` parameter should be an iterable of names of user
|
|
|
|
attributes to compare to. If this argument is not provided, the default
|
|
|
|
is used: ``'username', 'first_name', 'last_name', 'email'``.
|
|
|
|
Attributes that don't exist are ignored.
|
|
|
|
|
2021-12-27 13:48:03 +00:00
|
|
|
The maximum allowed similarity of passwords can be set on a scale of 0.1
|
|
|
|
to 1.0 with the ``max_similarity`` parameter. This is compared to the
|
|
|
|
result of :meth:`difflib.SequenceMatcher.quick_ratio`. A value of 0.1
|
|
|
|
rejects passwords unless they are substantially different from the
|
|
|
|
``user_attributes``, whereas a value of 1.0 rejects only passwords that are
|
|
|
|
identical to an attribute's value.
|
|
|
|
|
|
|
|
.. versionchanged:: 2.2.26
|
|
|
|
|
|
|
|
The ``max_similarity`` parameter was limited to a minimum value of 0.1.
|
2015-03-08 14:07:57 +00:00
|
|
|
|
|
|
|
.. class:: CommonPasswordValidator(password_list_path=DEFAULT_PASSWORD_LIST_PATH)
|
|
|
|
|
2021-12-13 17:53:07 +00:00
|
|
|
Validates that the password is not a common password. This converts the
|
2018-01-13 23:48:33 +00:00
|
|
|
password to lowercase (to do a case-insensitive comparison) and checks it
|
2018-04-10 16:02:03 +00:00
|
|
|
against a list of 20,000 common password created by `Royce Williams
|
2022-09-28 09:27:10 +00:00
|
|
|
<https://gist.github.com/roycewilliams/226886fd01572964e1431ac8afc999ce>`_.
|
2015-03-08 14:07:57 +00:00
|
|
|
|
|
|
|
The ``password_list_path`` can be set to the path of a custom file of
|
2018-01-13 23:48:33 +00:00
|
|
|
common passwords. This file should contain one lowercase password per line
|
|
|
|
and may be plain text or gzipped.
|
2015-03-08 14:07:57 +00:00
|
|
|
|
2022-09-28 09:27:10 +00:00
|
|
|
.. versionchanged:: 4.2
|
|
|
|
|
|
|
|
The list of 20,000 common passwords was updated to the most recent
|
|
|
|
version.
|
|
|
|
|
2015-03-08 14:07:57 +00:00
|
|
|
.. class:: NumericPasswordValidator()
|
|
|
|
|
2021-12-13 17:53:07 +00:00
|
|
|
Validate that the password is not entirely numeric.
|
2015-03-08 14:07:57 +00:00
|
|
|
|
|
|
|
Integrating validation
|
2019-02-08 20:38:30 +00:00
|
|
|
----------------------
|
2015-03-08 14:07:57 +00:00
|
|
|
|
|
|
|
There are a few functions in ``django.contrib.auth.password_validation`` that
|
|
|
|
you can call from your own forms or other code to integrate password
|
|
|
|
validation. This can be useful if you use custom forms for password setting,
|
|
|
|
or if you have API calls that allow passwords to be set, for example.
|
|
|
|
|
|
|
|
.. function:: validate_password(password, user=None, password_validators=None)
|
|
|
|
|
|
|
|
Validates a password. If all validators find the password valid, returns
|
|
|
|
``None``. If one or more validators reject the password, raises a
|
|
|
|
:exc:`~django.core.exceptions.ValidationError` with all the error messages
|
|
|
|
from the validators.
|
|
|
|
|
2015-06-08 17:27:47 +00:00
|
|
|
The ``user`` object is optional: if it's not provided, some validators may
|
|
|
|
not be able to perform any validation and will accept any password.
|
2015-03-08 14:07:57 +00:00
|
|
|
|
|
|
|
.. function:: password_changed(password, user=None, password_validators=None)
|
|
|
|
|
|
|
|
Informs all validators that the password has been changed. This can be used
|
2015-06-08 17:27:47 +00:00
|
|
|
by validators such as one that prevents password reuse. This should be
|
|
|
|
called once the password has been successfully changed.
|
2015-03-08 14:07:57 +00:00
|
|
|
|
2015-07-06 22:12:26 +00:00
|
|
|
For subclasses of :class:`~django.contrib.auth.models.AbstractBaseUser`,
|
|
|
|
the password field will be marked as "dirty" when calling
|
|
|
|
:meth:`~django.contrib.auth.models.AbstractBaseUser.set_password` which
|
|
|
|
triggers a call to ``password_changed()`` after the user is saved.
|
|
|
|
|
2015-03-08 14:07:57 +00:00
|
|
|
.. function:: password_validators_help_texts(password_validators=None)
|
|
|
|
|
|
|
|
Returns a list of the help texts of all validators. These explain the
|
|
|
|
password requirements to the user.
|
|
|
|
|
|
|
|
.. function:: password_validators_help_text_html(password_validators=None)
|
|
|
|
|
|
|
|
Returns an HTML string with all help texts in an ``<ul>``. This is
|
|
|
|
helpful when adding password validation to forms, as you can pass the
|
|
|
|
output directly to the ``help_text`` parameter of a form field.
|
|
|
|
|
|
|
|
.. function:: get_password_validators(validator_config)
|
|
|
|
|
|
|
|
Returns a set of validator objects based on the ``validator_config``
|
|
|
|
parameter. By default, all functions use the validators defined in
|
|
|
|
:setting:`AUTH_PASSWORD_VALIDATORS`, but by calling this function with an
|
|
|
|
alternate set of validators and then passing the result into the
|
|
|
|
``password_validators`` parameter of the other functions, your custom set
|
|
|
|
of validators will be used instead. This is useful when you have a typical
|
|
|
|
set of validators to use for most scenarios, but also have a special
|
|
|
|
situation that requires a custom set. If you always use the same set
|
|
|
|
of validators, there is no need to use this function, as the configuration
|
|
|
|
from :setting:`AUTH_PASSWORD_VALIDATORS` is used by default.
|
|
|
|
|
|
|
|
The structure of ``validator_config`` is identical to the
|
|
|
|
structure of :setting:`AUTH_PASSWORD_VALIDATORS`. The return value of
|
|
|
|
this function can be passed into the ``password_validators`` parameter
|
|
|
|
of the functions listed above.
|
|
|
|
|
|
|
|
Note that where the password is passed to one of these functions, this should
|
|
|
|
always be the clear text password - not a hashed password.
|
|
|
|
|
|
|
|
Writing your own validator
|
|
|
|
--------------------------
|
|
|
|
|
|
|
|
If Django's built-in validators are not sufficient, you can write your own
|
2019-06-17 14:54:55 +00:00
|
|
|
password validators. Validators have a fairly small interface. They must
|
|
|
|
implement two methods:
|
2015-03-08 14:07:57 +00:00
|
|
|
|
|
|
|
* ``validate(self, password, user=None)``: validate a password. Return
|
|
|
|
``None`` if the password is valid, or raise a
|
|
|
|
:exc:`~django.core.exceptions.ValidationError` with an error message if the
|
|
|
|
password is not valid. You must be able to deal with ``user`` being
|
2019-06-17 14:54:55 +00:00
|
|
|
``None`` - if that means your validator can't run, return ``None`` for no
|
|
|
|
error.
|
2015-03-08 14:07:57 +00:00
|
|
|
* ``get_help_text()``: provide a help text to explain the requirements to
|
|
|
|
the user.
|
|
|
|
|
|
|
|
Any items in the ``OPTIONS`` in :setting:`AUTH_PASSWORD_VALIDATORS` for your
|
|
|
|
validator will be passed to the constructor. All constructor arguments should
|
|
|
|
have a default value.
|
|
|
|
|
|
|
|
Here's a basic example of a validator, with one optional setting::
|
|
|
|
|
|
|
|
from django.core.exceptions import ValidationError
|
2017-01-26 19:58:33 +00:00
|
|
|
from django.utils.translation import gettext as _
|
2015-03-08 14:07:57 +00:00
|
|
|
|
2023-03-01 12:35:43 +00:00
|
|
|
|
2017-06-26 14:30:31 +00:00
|
|
|
class MinimumLengthValidator:
|
2015-03-08 14:07:57 +00:00
|
|
|
def __init__(self, min_length=8):
|
|
|
|
self.min_length = min_length
|
|
|
|
|
|
|
|
def validate(self, password, user=None):
|
|
|
|
if len(password) < self.min_length:
|
2015-06-08 17:27:47 +00:00
|
|
|
raise ValidationError(
|
|
|
|
_("This password must contain at least %(min_length)d characters."),
|
|
|
|
code="password_too_short",
|
|
|
|
params={"min_length": self.min_length},
|
|
|
|
)
|
2015-03-08 14:07:57 +00:00
|
|
|
|
|
|
|
def get_help_text(self):
|
2015-06-08 17:27:47 +00:00
|
|
|
return _(
|
|
|
|
"Your password must contain at least %(min_length)d characters."
|
2015-03-08 14:07:57 +00:00
|
|
|
% {"min_length": self.min_length}
|
2015-06-08 17:27:47 +00:00
|
|
|
)
|
2015-03-08 14:07:57 +00:00
|
|
|
|
|
|
|
You can also implement ``password_changed(password, user=None``), which will
|
|
|
|
be called after a successful password change. That can be used to prevent
|
|
|
|
password reuse, for example. However, if you decide to store a user's previous
|
|
|
|
passwords, you should never do so in clear text.
|