Django has a contributed module called auth . This module supports a large amount of User account and authentication functionality which can be used in Django projects.

In this blog post I'll examine the auth module's raw storage of hashed passwords, password hash upgrading and remote DOS attacks using large passwords.

Start with a small Django project I've created a small Django project for this blog post: $ pip install Django == 1 .7.3 $ django-admin startproject passwords The file and folder layout of this project looks like this: $ tree . ├── manage.py └── passwords ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py The django.contrib.auth module is included in settings.INSTALLED_APPS by default when generating a Django boilerplate project. When I run the migrate command, Django will create the database tables for the auth module: $ python manage.py migrate Operations to perform: Apply all migrations: admin, contenttypes, auth, sessions Running migrations: ... Applying auth.0001_initial... OK ...

Tokenised Password Storage I'll create an example user and examine the raw password field: $ python manage.py shell >>> from django.contrib.auth.models import User >>> user = User ( username = "tester" ) >>> user . set_password ( 'testing' ) >>> user . save () >>> user . password u 'pbkdf2_sha256$15000$Pjun1TMGEQnM$lShdzU33covbDNiqGVDffdHh/86VaECJlaaNXchT0ew=' When using the set_password method the first (and presumably the strongest) password hasher that Django is configured with will be used. In the case of Django 1.7.3 its PBKDF2 + HMAC + SHA256 with 15,000 hashing iterations performed on the original password string. The raw password field has 4 parts to it, the hashing algorithm, the iteration count, the salt and finally the password hash itself. These are delimited by a dollar sign: >>> from pprint import pprint >>> pprint ( user . password . split ( '$' )) [ u 'pbkdf2_sha256' , u '15000' , u 'Pjun1TMGEQnM' , u 'lShdzU33covbDNiqGVDffdHh/86VaECJlaaNXchT0ew=' ]

Django Upgrades Passwords One of the features of the auth module I like the most is password hash upgrading. When a user successfully authenticates, if their password hash is using any other hashing algorithm or iteration count than the first hashing algorithm configured in Django then Django will take the raw password and re-hash it using the first configured hasher. If you were to create a password in Django 1.7.3 that was hashed with anything other than PBKDF2 and with the default iteration count then the password hash will be upgraded to PBKDF2 and 15,000 iterations. Here I've created an account with the password 'testing' and stored it as an unsalted MD5 hash: >>> from django.contrib.auth.hashers import make_password >>> user . password = make_password ( password = 'testing' , salt = None , hasher = 'unsalted_md5' ) >>> user . save () >>> user . password 'ae2b1fca515949e5d54fb22b8ed95575' As soon as I authenticate the account the password is re-hashed with PBKDF2: >>> from django.contrib.auth import authenticate >>> authenticate ( username = 'tester' , password = 'testing' ) >>> user = User . objects . get ( username = 'tester' ) >>> user . password u 'pbkdf2_sha256$15000$lPSA3r6AwELv$/6Frb75xtX5xmA8Ezcnl0UxPmHpUaeleY+QqM/dMRLw=' As of version 1.7.3, Django supports nine hashing types and variations which is handy for importing user accounts and their password hashes from other systems. >>> from django.contrib.auth import hashers >>> from pprint import pprint >>> pprint ( hashers . HASHERS . keys ()) [ u 'bcrypt_sha256' , u 'sha1' , u 'pbkdf2_sha256' , u 'pbkdf2_sha1' , u 'crypt' , u 'unsalted_md5' , u 'unsalted_sha1' , u 'bcrypt' , u 'md5' ]

Downgrades as well as Upgrades The only downside to password hash 'upgrading' is that if you do use PBKDF2 but use a higher iteration count than what is hard-coded into the PBKDF2PasswordHasher class in django.contrib.auth.hashers then the iteration count will be downgraded to that given hard-coded value. Django 1.7.3 set this iteration count to 15,000, Django 1.8 sets it to 20,000 and the current master branch of Django which targets the version 1.9 release of Django set the iteration count to 24,000. Here is a demonstration of a downgrade using Django 1.7.3. I've hashed a password 50,000 times and it's downgraded to 15,000 after authentication: >>> from django.contrib.auth import authenticate >>> from django.contrib.auth.hashers import PBKDF2PasswordHasher >>> hasher = PBKDF2PasswordHasher () >>> user . password = hasher . encode ( password = 'password' , salt = 'salt' , iterations = 50000 ) >>> user . save () >>> user = authenticate ( username = 'tester' , password = 'password' ) >>> user . password u 'pbkdf2_sha256$15000$NdqimFkxkuIe$YXO6x1A4XlVaFyu6V+Y/pXHnwpmNAcyFeX88R4JXf1k='

Why is computationally expensive hashing so important? If the raw password hash values, salts and iteration counts themselves were to ever be exposed it extremely computationally expensive to try and guess what the original password that was used to create the hash... or it would be if the passwords were hashed with PBKDF2, HMAC, SHA256 and tens of thousands of iterations. If you use a weak hashing algorithm, such as unsalted MD5, then it becomes trivial to find out what the original passwords are. To demonstrate, I'll create an unsalted MD5 hash for the string 'elephant123': >>> from django.contrib.auth.hashers import make_password >>> make_password ( password = 'elephant123' , salt = None , hasher = 'unsalted_md5' ) 'e68a95aadb0c73dfd968513174de4ddf' If I paste e68a95aadb0c73dfd968513174de4ddf into the form on md5crack it tells me in less than a second that 'elephant123' is a possible string that the hash e68a95aadb0c73dfd968513174de4ddf represents. MD5 has demonstrated attacks, SHA-1 hashes could be broken with a large amount of computing power but SHA256 (which Django auth's PBKDF2 class is configured to work with) has no known complete attacks. And just in case, having a configurable iteration count means password hashes can be upgraded as computers become more powerful.