From 2d321d2393ebf5a8d69604d857fd7df77887ccf7 Mon Sep 17 00:00:00 2001
From: Tim Graham <timograham@gmail.com>
Date: Wed, 10 Feb 2016 10:20:02 -0500
Subject: [PATCH] [1.8.x] Fixed #26188 -- Documented how to wrap password
 hashers.

Backport of 5a541e2e6cb01e254f20c302093a24d7dc9af8ce from master
---
 docs/topics/auth/passwords.txt | 83 ++++++++++++++++++++++++++++++++++
 1 file changed, 83 insertions(+)

diff --git a/docs/topics/auth/passwords.txt b/docs/topics/auth/passwords.txt
index 1c9cef8b0e..29da3ae1d1 100644
--- a/docs/topics/auth/passwords.txt
+++ b/docs/topics/auth/passwords.txt
@@ -194,6 +194,89 @@ sure never to *remove* entries from this list. If you do, users using
 unmentioned algorithms won't be able to upgrade. Passwords will be upgraded
 when changing the PBKDF2 iteration count.
 
+.. _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
+PBKDF2(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