Fixed #14390 and #16262 -- Moved password related functions from auth models to utils module and stopped check_password from throwing an exception. Thanks, subsume and lrekucki.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@16456 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
2619dc8285
commit
4a10338986
|
@ -1,63 +1,19 @@
|
||||||
import datetime
|
import datetime
|
||||||
import hashlib
|
|
||||||
import random
|
|
||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
from django.contrib import auth
|
|
||||||
from django.contrib.auth.signals import user_logged_in
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.manager import EmptyManager
|
from django.db.models.manager import EmptyManager
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.utils.encoding import smart_str
|
from django.utils.encoding import smart_str
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils.crypto import constant_time_compare
|
|
||||||
|
|
||||||
|
from django.contrib import auth
|
||||||
UNUSABLE_PASSWORD = '!' # This will never be a valid hash
|
from django.contrib.auth.signals import user_logged_in
|
||||||
|
from django.contrib.auth.utils import (get_hexdigest, make_password,
|
||||||
def get_hexdigest(algorithm, salt, raw_password):
|
check_password, is_password_usable,
|
||||||
"""
|
get_random_string, UNUSABLE_PASSWORD)
|
||||||
Returns a string of the hexdigest of the given plaintext password and salt
|
from django.contrib.contenttypes.models import ContentType
|
||||||
using the given algorithm ('md5', 'sha1' or 'crypt').
|
|
||||||
"""
|
|
||||||
raw_password, salt = smart_str(raw_password), smart_str(salt)
|
|
||||||
if algorithm == 'crypt':
|
|
||||||
try:
|
|
||||||
import crypt
|
|
||||||
except ImportError:
|
|
||||||
raise ValueError('"crypt" password algorithm not supported in this environment')
|
|
||||||
return crypt.crypt(raw_password, salt)
|
|
||||||
|
|
||||||
if algorithm == 'md5':
|
|
||||||
return hashlib.md5(salt + raw_password).hexdigest()
|
|
||||||
elif algorithm == 'sha1':
|
|
||||||
return hashlib.sha1(salt + raw_password).hexdigest()
|
|
||||||
raise ValueError("Got unknown password algorithm type in password.")
|
|
||||||
|
|
||||||
def get_random_string(length=12, allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'):
|
|
||||||
"""
|
|
||||||
Returns a random string of length characters from the set of a-z, A-Z, 0-9
|
|
||||||
for use as a salt.
|
|
||||||
|
|
||||||
The default length of 12 with the a-z, A-Z, 0-9 character set returns
|
|
||||||
a 71-bit salt. log_2((26+26+10)^12) =~ 71 bits
|
|
||||||
"""
|
|
||||||
import random
|
|
||||||
try:
|
|
||||||
random = random.SystemRandom()
|
|
||||||
except NotImplementedError:
|
|
||||||
pass
|
|
||||||
return ''.join([random.choice(allowed_chars) for i in range(length)])
|
|
||||||
|
|
||||||
def check_password(raw_password, enc_password):
|
|
||||||
"""
|
|
||||||
Returns a boolean of whether the raw_password was correct. Handles
|
|
||||||
encryption formats behind the scenes.
|
|
||||||
"""
|
|
||||||
algo, salt, hsh = enc_password.split('$')
|
|
||||||
return constant_time_compare(hsh, get_hexdigest(algo, salt, raw_password))
|
|
||||||
|
|
||||||
def update_last_login(sender, user, **kwargs):
|
def update_last_login(sender, user, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -270,13 +226,7 @@ class User(models.Model):
|
||||||
return full_name.strip()
|
return full_name.strip()
|
||||||
|
|
||||||
def set_password(self, raw_password):
|
def set_password(self, raw_password):
|
||||||
if raw_password is None:
|
self.password = make_password('sha1', raw_password)
|
||||||
self.set_unusable_password()
|
|
||||||
else:
|
|
||||||
algo = 'sha1'
|
|
||||||
salt = get_random_string()
|
|
||||||
hsh = get_hexdigest(algo, salt, raw_password)
|
|
||||||
self.password = '%s$%s$%s' % (algo, salt, hsh)
|
|
||||||
|
|
||||||
def check_password(self, raw_password):
|
def check_password(self, raw_password):
|
||||||
"""
|
"""
|
||||||
|
@ -296,14 +246,10 @@ class User(models.Model):
|
||||||
|
|
||||||
def set_unusable_password(self):
|
def set_unusable_password(self):
|
||||||
# Sets a value that will never be a valid hash
|
# Sets a value that will never be a valid hash
|
||||||
self.password = UNUSABLE_PASSWORD
|
self.password = make_password('sha1', None)
|
||||||
|
|
||||||
def has_usable_password(self):
|
def has_usable_password(self):
|
||||||
if self.password is None \
|
return is_password_usable(self.password)
|
||||||
or self.password == UNUSABLE_PASSWORD:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_group_permissions(self, obj=None):
|
def get_group_permissions(self, obj=None):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from django.contrib.auth.tests.auth_backends import (BackendTest,
|
from django.contrib.auth.tests.auth_backends import (BackendTest,
|
||||||
RowlevelBackendTest, AnonymousUserBackendTest, NoAnonymousUserBackendTest,
|
RowlevelBackendTest, AnonymousUserBackendTest, NoAnonymousUserBackendTest,
|
||||||
NoBackendsTest, InActiveUserBackendTest, NoInActiveUserBackendTest)
|
NoBackendsTest, InActiveUserBackendTest, NoInActiveUserBackendTest)
|
||||||
from django.contrib.auth.tests.basic import BasicTestCase
|
from django.contrib.auth.tests.basic import BasicTestCase, PasswordUtilsTestCase
|
||||||
from django.contrib.auth.tests.context_processors import AuthContextProcessorTests
|
from django.contrib.auth.tests.context_processors import AuthContextProcessorTests
|
||||||
from django.contrib.auth.tests.decorators import LoginRequiredTestCase
|
from django.contrib.auth.tests.decorators import LoginRequiredTestCase
|
||||||
from django.contrib.auth.tests.forms import (UserCreationFormTest,
|
from django.contrib.auth.tests.forms import (UserCreationFormTest,
|
||||||
|
|
|
@ -1,8 +1,16 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from django.utils.unittest import skipUnless
|
||||||
from django.contrib.auth.models import User, AnonymousUser
|
from django.contrib.auth.models import User, AnonymousUser
|
||||||
|
from django.contrib.auth import utils
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from StringIO import StringIO
|
from StringIO import StringIO
|
||||||
|
|
||||||
|
try:
|
||||||
|
import crypt as crypt_module
|
||||||
|
except ImportError:
|
||||||
|
crypt_module = None
|
||||||
|
|
||||||
|
|
||||||
class BasicTestCase(TestCase):
|
class BasicTestCase(TestCase):
|
||||||
def test_user(self):
|
def test_user(self):
|
||||||
"Check that users can be created and can set their password"
|
"Check that users can be created and can set their password"
|
||||||
|
@ -93,3 +101,29 @@ class BasicTestCase(TestCase):
|
||||||
self.assertEqual(u.email, 'joe@somewhere.org')
|
self.assertEqual(u.email, 'joe@somewhere.org')
|
||||||
self.assertFalse(u.has_usable_password())
|
self.assertFalse(u.has_usable_password())
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordUtilsTestCase(TestCase):
|
||||||
|
|
||||||
|
def _test_make_password(self, algo):
|
||||||
|
password = utils.make_password(algo, "foobar")
|
||||||
|
self.assertTrue(utils.is_password_usable(password))
|
||||||
|
self.assertTrue(utils.check_password("foobar", password))
|
||||||
|
|
||||||
|
def test_make_unusable(self):
|
||||||
|
"Check that you can create an unusable password."
|
||||||
|
password = utils.make_password("any", None)
|
||||||
|
self.assertFalse(utils.is_password_usable(password))
|
||||||
|
self.assertFalse(utils.check_password("foobar", password))
|
||||||
|
|
||||||
|
def test_make_password_sha1(self):
|
||||||
|
"Check creating passwords with SHA1 algorithm."
|
||||||
|
self._test_make_password("sha1")
|
||||||
|
|
||||||
|
def test_make_password_md5(self):
|
||||||
|
"Check creating passwords with MD5 algorithm."
|
||||||
|
self._test_make_password("md5")
|
||||||
|
|
||||||
|
@skipUnless(crypt_module, "no crypt module to generate password.")
|
||||||
|
def test_make_password_crypt(self):
|
||||||
|
"Check creating passwords with CRYPT algorithm."
|
||||||
|
self._test_make_password("crypt")
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
import hashlib
|
||||||
|
from django.utils.encoding import smart_str
|
||||||
|
from django.utils.crypto import constant_time_compare
|
||||||
|
|
||||||
|
UNUSABLE_PASSWORD = '!' # This will never be a valid hash
|
||||||
|
|
||||||
|
def get_hexdigest(algorithm, salt, raw_password):
|
||||||
|
"""
|
||||||
|
Returns a string of the hexdigest of the given plaintext password and salt
|
||||||
|
using the given algorithm ('md5', 'sha1' or 'crypt').
|
||||||
|
"""
|
||||||
|
raw_password, salt = smart_str(raw_password), smart_str(salt)
|
||||||
|
if algorithm == 'crypt':
|
||||||
|
try:
|
||||||
|
import crypt
|
||||||
|
except ImportError:
|
||||||
|
raise ValueError('"crypt" password algorithm not supported in this environment')
|
||||||
|
return crypt.crypt(raw_password, salt)
|
||||||
|
|
||||||
|
if algorithm == 'md5':
|
||||||
|
return hashlib.md5(salt + raw_password).hexdigest()
|
||||||
|
elif algorithm == 'sha1':
|
||||||
|
return hashlib.sha1(salt + raw_password).hexdigest()
|
||||||
|
raise ValueError("Got unknown password algorithm type in password.")
|
||||||
|
|
||||||
|
def get_random_string(length=12, allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'):
|
||||||
|
"""
|
||||||
|
Returns a random string of length characters from the set of a-z, A-Z, 0-9
|
||||||
|
for use as a salt.
|
||||||
|
|
||||||
|
The default length of 12 with the a-z, A-Z, 0-9 character set returns
|
||||||
|
a 71-bit salt. log_2((26+26+10)^12) =~ 71 bits
|
||||||
|
"""
|
||||||
|
import random
|
||||||
|
try:
|
||||||
|
random = random.SystemRandom()
|
||||||
|
except NotImplementedError:
|
||||||
|
pass
|
||||||
|
return ''.join([random.choice(allowed_chars) for i in range(length)])
|
||||||
|
|
||||||
|
def check_password(raw_password, enc_password):
|
||||||
|
"""
|
||||||
|
Returns a boolean of whether the raw_password was correct. Handles
|
||||||
|
encryption formats behind the scenes.
|
||||||
|
"""
|
||||||
|
parts = enc_password.split('$')
|
||||||
|
if len(parts) != 3:
|
||||||
|
return False
|
||||||
|
algo, salt, hsh = parts
|
||||||
|
return constant_time_compare(hsh, get_hexdigest(algo, salt, raw_password))
|
||||||
|
|
||||||
|
def is_password_usable(encoded_password):
|
||||||
|
return encoded_password is not None and encoded_password != UNUSABLE_PASSWORD
|
||||||
|
|
||||||
|
def make_password(algo, raw_password):
|
||||||
|
"""
|
||||||
|
Produce a new password string in this format: algorithm$salt$hash
|
||||||
|
"""
|
||||||
|
if raw_password is None:
|
||||||
|
return UNUSABLE_PASSWORD
|
||||||
|
salt = get_random_string()
|
||||||
|
hsh = get_hexdigest(algo, salt, raw_password)
|
||||||
|
return '%s$%s$%s' % (algo, salt, hsh)
|
|
@ -192,6 +192,11 @@ Django 1.4 also includes several smaller improvements worth noting:
|
||||||
* In the documentation, a helpful :doc:`security overview </topics/security>`
|
* In the documentation, a helpful :doc:`security overview </topics/security>`
|
||||||
page.
|
page.
|
||||||
|
|
||||||
|
* Function :func:`django.contrib.auth.models.check_password` has been moved
|
||||||
|
to the :mod:`django.contrib.auth.utils` module. Importing it from the old
|
||||||
|
location will still work, but you should update your imports.
|
||||||
|
|
||||||
|
|
||||||
.. _backwards-incompatible-changes-1.4:
|
.. _backwards-incompatible-changes-1.4:
|
||||||
|
|
||||||
Backwards incompatible changes in 1.4
|
Backwards incompatible changes in 1.4
|
||||||
|
|
|
@ -627,19 +627,44 @@ Django provides two functions in :mod:`django.contrib.auth`:
|
||||||
|
|
||||||
.. _backends documentation: #other-authentication-sources
|
.. _backends documentation: #other-authentication-sources
|
||||||
|
|
||||||
Manually checking a user's password
|
Manually managing a user's password
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
.. currentmodule:: django.contrib.auth.models
|
.. currentmodule:: django.contrib.auth.utils
|
||||||
|
|
||||||
|
.. versionadded:: 1.4
|
||||||
|
|
||||||
|
The :mod:`django.contrib.auth.utils` module provides a set of functions
|
||||||
|
to create and validate hashed password. You can use them independently
|
||||||
|
from the ``User`` model.
|
||||||
|
|
||||||
.. function:: check_password()
|
.. function:: check_password()
|
||||||
|
|
||||||
If you'd like to manually authenticate a user by comparing a plain-text
|
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
|
password to the hashed password in the database, use the convenience
|
||||||
function :func:`django.contrib.auth.models.check_password`. It takes two
|
function :func:`django.contrib.auth.utils.check_password`. It takes two
|
||||||
arguments: the plain-text password to check, and the full value of a user's
|
arguments: the plain-text password to check, and the full value of a
|
||||||
``password`` field in the database to check against, and returns ``True``
|
user's ``password`` field in the database to check against, and returns
|
||||||
if they match, ``False`` otherwise.
|
``True`` if they match, ``False`` otherwise.
|
||||||
|
|
||||||
|
.. function:: make_password()
|
||||||
|
|
||||||
|
.. versionadded:: 1.4
|
||||||
|
|
||||||
|
Creates a hashed password in the format used by this application. It takes
|
||||||
|
two arguments: hashing algorithm to use and the password in plain-text.
|
||||||
|
Currently supported algorithms are: ``'sha1'``, ``'md5'`` and ``'crypt'``
|
||||||
|
if you have the ``crypt`` library installed. If the second argument is
|
||||||
|
``None``, an unusable password is returned (a one that will be never
|
||||||
|
accepted by :func:`django.contrib.auth.utils.check_password`).
|
||||||
|
|
||||||
|
.. function:: is_password_usable()
|
||||||
|
|
||||||
|
.. versionadded:: 1.4
|
||||||
|
|
||||||
|
Checks if the given string is a hashed password that has a chance
|
||||||
|
of being verified against :func:`django.contrib.auth.utils.check_password`.
|
||||||
|
|
||||||
|
|
||||||
How to log a user out
|
How to log a user out
|
||||||
---------------------
|
---------------------
|
||||||
|
|
Loading…
Reference in New Issue