diff --git a/docs/topics/auth/passwords.txt b/docs/topics/auth/passwords.txt index ef14f7f330..b57c846085 100644 --- a/docs/topics/auth/passwords.txt +++ b/docs/topics/auth/passwords.txt @@ -199,6 +199,89 @@ bcrypt rounds. Passwords updates when changing the number of bcrypt rounds was added. +.. _wrapping-password-hashers: + +Password upgrading without requiring a login +-------------------------------------------- + +If you have an existing database with an older, weak hash such as MD5 or SHA1, +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. + +For this example, we'll migrate a collection of SHA1 hashes to use +PDKDF2(SHA1(password)) and add the corresponding password hasher for checking +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: + +.. snippet:: + :filename: accounts/hashers.py + + from django.contrib.auth.hashers import ( + PBKDF2PasswordHasher, SHA1PasswordHasher, + ) + + + class PBKDF2WrappedSHA1PasswordHasher(PBKDF2PasswordHasher): + algorithm = 'pbkdf2_wrapped_sha1' + + def encode_sha1_hash(self, sha1_hash, salt, iterations=None): + return super(PBKDF2WrappedSHA1PasswordHasher, self).encode(sha1_hash, salt, iterations) + + def encode(self, password, salt, iterations=None): + _, _, sha1_hash = SHA1PasswordHasher().encode(password, salt).split('$', 2) + return self.encode_sha1_hash(sha1_hash, salt, iterations) + +The data migration might look something like: + +.. snippet:: + :filename: accounts/migrations/0002_migrate_sha1_passwords.py + + from django.db import migrations + + from ..hashers import PBKDF2WrappedSHA1PasswordHasher + + + def forwards_func(apps, schema_editor): + User = apps.get_model('auth', 'User') + users = User.objects.filter(password__startswith='sha1$') + hasher = PBKDF2WrappedSHA1PasswordHasher() + for user in users: + algorithm, salt, sha1_hash = user.password.split('$', 2) + user.password = hasher.encode_sha1_hash(sha1_hash, salt) + 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: + +.. snippet:: + :filename: mysite/settings.py + + PASSWORD_HASHERS = [ + 'django.contrib.auth.hashers.PBKDF2PasswordHasher', + 'accounts.hashers.PBKDF2WrappedSHA1PasswordHasher', + ] + +Include any other hashers that your site uses in this list. + .. _sha1: https://en.wikipedia.org/wiki/SHA1 .. _pbkdf2: https://en.wikipedia.org/wiki/PBKDF2 .. _nist: http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf