Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

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
  • Loading branch information...
commit 4a1033898636f8c2cafc74c7934fdf7411716fdf 1 parent 2619dc8
@jezdez jezdez authored
View
72 django/contrib/auth/models.py
@@ -1,63 +1,19 @@
import datetime
-import hashlib
-import random
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.mail import send_mail
from django.db import models
from django.db.models.manager import EmptyManager
-from django.contrib.contenttypes.models import ContentType
from django.utils.encoding import smart_str
from django.utils.translation import ugettext_lazy as _
-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.
- """
- algo, salt, hsh = enc_password.split('$')
- return constant_time_compare(hsh, get_hexdigest(algo, salt, raw_password))
+from django.contrib import auth
+from django.contrib.auth.signals import user_logged_in
+from django.contrib.auth.utils import (get_hexdigest, make_password,
+ check_password, is_password_usable,
+ get_random_string, UNUSABLE_PASSWORD)
+from django.contrib.contenttypes.models import ContentType
def update_last_login(sender, user, **kwargs):
"""
@@ -270,13 +226,7 @@ def get_full_name(self):
return full_name.strip()
def set_password(self, raw_password):
- if raw_password is None:
- 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)
+ self.password = make_password('sha1', raw_password)
def check_password(self, raw_password):
"""
@@ -296,14 +246,10 @@ def check_password(self, raw_password):
def set_unusable_password(self):
# 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):
- if self.password is None \
- or self.password == UNUSABLE_PASSWORD:
- return False
- else:
- return True
+ return is_password_usable(self.password)
def get_group_permissions(self, obj=None):
"""
View
2  django/contrib/auth/tests/__init__.py
@@ -1,7 +1,7 @@
from django.contrib.auth.tests.auth_backends import (BackendTest,
RowlevelBackendTest, AnonymousUserBackendTest, NoAnonymousUserBackendTest,
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.decorators import LoginRequiredTestCase
from django.contrib.auth.tests.forms import (UserCreationFormTest,
View
34 django/contrib/auth/tests/basic.py
@@ -1,8 +1,16 @@
from django.test import TestCase
+from django.utils.unittest import skipUnless
from django.contrib.auth.models import User, AnonymousUser
+from django.contrib.auth import utils
from django.core.management import call_command
from StringIO import StringIO
+try:
+ import crypt as crypt_module
+except ImportError:
+ crypt_module = None
+
+
class BasicTestCase(TestCase):
def test_user(self):
"Check that users can be created and can set their password"
@@ -93,3 +101,29 @@ def test_createsuperuser_management_command(self):
self.assertEqual(u.email, 'joe@somewhere.org')
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")
View
63 django/contrib/auth/utils.py
@@ -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)
View
5 docs/releases/1.4.txt
@@ -192,6 +192,11 @@ Django 1.4 also includes several smaller improvements worth noting:
* In the documentation, a helpful :doc:`security overview </topics/security>`
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 in 1.4
View
37 docs/topics/auth.txt
@@ -627,19 +627,44 @@ Django provides two functions in :mod:`django.contrib.auth`:
.. _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()
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
- function :func:`django.contrib.auth.models.check_password`. It takes two
- arguments: the plain-text password to check, and the full value of a user's
- ``password`` field in the database to check against, and returns ``True``
- if they match, ``False`` otherwise.
+ 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 ``password`` field in the database to check against, and returns
+ ``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
---------------------
Please sign in to comment.
Something went wrong with that request. Please try again.