Merge pull request #4 from nitmir/dev
Add more optional password check to the mysql auth backend
This commit is contained in:
		
							
								
								
									
										15
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								README.rst
									
									
									
									
									
								
							| @@ -199,8 +199,19 @@ Mysql backend settings. Only usefull if you are using the mysql authentication b | |||||||
|   The username must be in field ``username``, the password in ``password``, |   The username must be in field ``username``, the password in ``password``, | ||||||
|   additional fields are used as the user attributes. |   additional fields are used as the user attributes. | ||||||
|   The default is ``"SELECT user AS usersame, pass AS password, users.* FROM users WHERE user = %s"`` |   The default is ``"SELECT user AS usersame, pass AS password, users.* FROM users WHERE user = %s"`` | ||||||
| * ``CAS_SQL_PASSWORD_CHECK``: The method used to check the user password. Must be | * ``CAS_SQL_PASSWORD_CHECK``: The method used to check the user password. Must be one of the following: | ||||||
|   ``"crypt"`` or ``"plain``". The default is ``"crypt"``. |  | ||||||
|  |     * ``"crypt"`` (see <https://en.wikipedia.org/wiki/Crypt_(C)>), the password in the database | ||||||
|  |       should begin this $ | ||||||
|  |     * ``"ldap"`` (see https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html) | ||||||
|  |       the password in the database must begin with one of {MD5}, {SMD5}, {SHA}, {SSHA}, {SHA256}, | ||||||
|  |       {SSHA256}, {SHA384}, {SSHA384}, {SHA512}, {SSHA512}, {CRYPT}. | ||||||
|  |     * ``"hex_HASH_NAME"`` with ``HASH_NAME`` in md5, sha1, sha224, sha256, sha384, sha512. | ||||||
|  |       The hashed password in the database is compare to the hexadecimal digest of the clear | ||||||
|  |       password hashed with the corresponding algorithm. | ||||||
|  |     * ``"plain"``, the password in the database must be in clear. | ||||||
|  |  | ||||||
|  |   The default is ``"crypt"``. | ||||||
|  |  | ||||||
|  |  | ||||||
| Test backend settings. Only usefull if you are using the test authentication backend: | Test backend settings. Only usefull if you are using the test authentication backend: | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ from django.contrib.auth import get_user_model | |||||||
| try:  # pragma: no cover | try:  # pragma: no cover | ||||||
|     import MySQLdb |     import MySQLdb | ||||||
|     import MySQLdb.cursors |     import MySQLdb.cursors | ||||||
|     import crypt |     from utils import check_password | ||||||
| except ImportError: | except ImportError: | ||||||
|     MySQLdb = None |     MySQLdb = None | ||||||
|  |  | ||||||
| @@ -90,17 +90,12 @@ class MysqlAuthUser(AuthUser):  # pragma: no cover | |||||||
|     def test_password(self, password): |     def test_password(self, password): | ||||||
|         """test `password` agains the user""" |         """test `password` agains the user""" | ||||||
|         if self.user: |         if self.user: | ||||||
|             if settings.CAS_SQL_PASSWORD_CHECK == "plain": |             check_password( | ||||||
|                 return password == self.user["password"] |                 settings.CAS_SQL_PASSWORD_CHECK, | ||||||
|             elif settings.CAS_SQL_PASSWORD_CHECK == "crypt": |                 password, | ||||||
|                 if self.user["password"].startswith('$'): |                 self.user["password"], | ||||||
|                     salt = '$'.join(self.user["password"].split('$', 3)[:-1]) |                 settings.CAS_SQL_DBCHARSET | ||||||
|                     return crypt.crypt(password, salt) == self.user["password"] |             ) | ||||||
|                 else: |  | ||||||
|                     return crypt.crypt( |  | ||||||
|                         password, |  | ||||||
|                         self.user["password"][:2] |  | ||||||
|                     ) == self.user["password"] |  | ||||||
|         else: |         else: | ||||||
|             return False |             return False | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ from .default_settings import settings | |||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from django.test import Client | from django.test import Client | ||||||
|  |  | ||||||
|  | import six | ||||||
| from lxml import etree | from lxml import etree | ||||||
|  |  | ||||||
| from cas_server import models | from cas_server import models | ||||||
| @@ -59,6 +60,68 @@ def get_pgt(): | |||||||
|     return params |     return params | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CheckPasswordCase(TestCase): | ||||||
|  |     """Tests for the utils function `utils.check_password`""" | ||||||
|  |  | ||||||
|  |     def setUp(self): | ||||||
|  |         """Generate random bytes string that will be used ass passwords""" | ||||||
|  |         self.password1 = utils.gen_saml_id() | ||||||
|  |         self.password2 = utils.gen_saml_id() | ||||||
|  |         if not isinstance(self.password1, bytes): | ||||||
|  |             self.password1 = self.password1.encode("utf8") | ||||||
|  |             self.password2 = self.password2.encode("utf8") | ||||||
|  |  | ||||||
|  |     def test_setup(self): | ||||||
|  |         """check that generated password are bytes""" | ||||||
|  |         self.assertIsInstance(self.password1, bytes) | ||||||
|  |         self.assertIsInstance(self.password2, bytes) | ||||||
|  |  | ||||||
|  |     def test_plain(self): | ||||||
|  |         """test the plain auth method""" | ||||||
|  |         self.assertTrue(utils.check_password("plain", self.password1, self.password1, "utf8")) | ||||||
|  |         self.assertFalse(utils.check_password("plain", self.password1, self.password2, "utf8")) | ||||||
|  |  | ||||||
|  |     def test_crypt(self): | ||||||
|  |         """test the crypt auth method""" | ||||||
|  |         if six.PY3: | ||||||
|  |             hashed_password1 = utils.crypt.crypt( | ||||||
|  |                 self.password1.decode("utf8"), | ||||||
|  |                 "$6$UVVAQvrMyXMF3FF3" | ||||||
|  |             ).encode("utf8") | ||||||
|  |         else: | ||||||
|  |             hashed_password1 = utils.crypt.crypt(self.password1, "$6$UVVAQvrMyXMF3FF3") | ||||||
|  |  | ||||||
|  |         self.assertTrue(utils.check_password("crypt", self.password1, hashed_password1, "utf8")) | ||||||
|  |         self.assertFalse(utils.check_password("crypt", self.password2, hashed_password1, "utf8")) | ||||||
|  |  | ||||||
|  |     def test_ldap_ssha(self): | ||||||
|  |         """test the ldap auth method with a {SSHA} scheme""" | ||||||
|  |         salt = b"UVVAQvrMyXMF3FF3" | ||||||
|  |         hashed_password1 = utils.LdapHashUserPassword.hash(b'{SSHA}', self.password1, salt, "utf8") | ||||||
|  |  | ||||||
|  |         self.assertIsInstance(hashed_password1, bytes) | ||||||
|  |         self.assertTrue(utils.check_password("ldap", self.password1, hashed_password1, "utf8")) | ||||||
|  |         self.assertFalse(utils.check_password("ldap", self.password2, hashed_password1, "utf8")) | ||||||
|  |  | ||||||
|  |     def test_hex_md5(self): | ||||||
|  |         """test the hex_md5 auth method""" | ||||||
|  |         hashed_password1 = utils.hashlib.md5(self.password1).hexdigest() | ||||||
|  |  | ||||||
|  |         self.assertTrue(utils.check_password("hex_md5", self.password1, hashed_password1, "utf8")) | ||||||
|  |         self.assertFalse(utils.check_password("hex_md5", self.password2, hashed_password1, "utf8")) | ||||||
|  |  | ||||||
|  |     def test_hox_sha512(self): | ||||||
|  |         """test the hex_sha512 auth method""" | ||||||
|  |         hashed_password1 = utils.hashlib.sha512(self.password1).hexdigest() | ||||||
|  |  | ||||||
|  |         self.assertTrue( | ||||||
|  |             utils.check_password("hex_sha512", self.password1, hashed_password1, "utf8") | ||||||
|  |         ) | ||||||
|  |         self.assertFalse( | ||||||
|  |             utils.check_password("hex_sha512", self.password2, hashed_password1, "utf8") | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class LoginTestCase(TestCase): | class LoginTestCase(TestCase): | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|   | |||||||
| @@ -19,6 +19,10 @@ from django.contrib import messages | |||||||
| import random | import random | ||||||
| import string | import string | ||||||
| import json | import json | ||||||
|  | import hashlib | ||||||
|  | import crypt | ||||||
|  | import base64 | ||||||
|  | import six | ||||||
| from threading import Thread | from threading import Thread | ||||||
| from importlib import import_module | from importlib import import_module | ||||||
| from six.moves import BaseHTTPServer | from six.moves import BaseHTTPServer | ||||||
| @@ -172,3 +176,177 @@ class PGTUrlHandler(BaseHTTPServer.BaseHTTPRequestHandler): | |||||||
|         httpd_thread.daemon = True |         httpd_thread.daemon = True | ||||||
|         httpd_thread.start() |         httpd_thread.start() | ||||||
|         return (httpd_thread, host, port) |         return (httpd_thread, host, port) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LdapHashUserPassword(object): | ||||||
|  |     """Please see https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html""" | ||||||
|  |  | ||||||
|  |     schemes_salt = {b"{SMD5}", b"{SSHA}", b"{SSHA256}", b"{SSHA384}", b"{SSHA512}", b"{CRYPT}"} | ||||||
|  |     schemes_nosalt = {b"{MD5}", b"{SHA}", b"{SHA256}", b"{SHA384}", b"{SHA512}"} | ||||||
|  |  | ||||||
|  |     _schemes_to_hash = { | ||||||
|  |         b"{SMD5}": hashlib.md5, | ||||||
|  |         b"{MD5}": hashlib.md5, | ||||||
|  |         b"{SSHA}": hashlib.sha1, | ||||||
|  |         b"{SHA}": hashlib.sha1, | ||||||
|  |         b"{SSHA256}": hashlib.sha256, | ||||||
|  |         b"{SHA256}": hashlib.sha256, | ||||||
|  |         b"{SSHA384}": hashlib.sha384, | ||||||
|  |         b"{SHA384}": hashlib.sha384, | ||||||
|  |         b"{SSHA512}": hashlib.sha512, | ||||||
|  |         b"{SHA512}": hashlib.sha512 | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     _schemes_to_len = { | ||||||
|  |         b"{SMD5}": 16, | ||||||
|  |         b"{SSHA}": 20, | ||||||
|  |         b"{SSHA256}": 32, | ||||||
|  |         b"{SSHA384}": 48, | ||||||
|  |         b"{SSHA512}": 64, | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     class BadScheme(ValueError): | ||||||
|  |         """Error raised then the hash scheme is not in schemes_salt + schemes_nosalt""" | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |     class BadHash(ValueError): | ||||||
|  |         """Error raised then the hash is too short""" | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |     class BadSalt(ValueError): | ||||||
|  |         """Error raised then with the scheme {CRYPT} the salt is invalid""" | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def _raise_bad_scheme(cls, scheme, valid, msg): | ||||||
|  |         """ | ||||||
|  |             Raise BadScheme error for `scheme`, possible valid scheme are | ||||||
|  |             in `valid`, the error message is `msg` | ||||||
|  |         """ | ||||||
|  |         valid_schemes = [s.decode() for s in valid] | ||||||
|  |         valid_schemes.sort() | ||||||
|  |         raise cls.BadScheme(msg % (scheme, u", ".join(valid_schemes))) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def _test_scheme(cls, scheme): | ||||||
|  |         """Test if a scheme is valide or raise BadScheme""" | ||||||
|  |         if scheme not in cls.schemes_salt and scheme not in cls.schemes_nosalt: | ||||||
|  |             cls._raise_bad_scheme( | ||||||
|  |                 scheme, | ||||||
|  |                 cls.schemes_salt | cls.schemes_nosalt, | ||||||
|  |                 "The scheme %r is not valid. Valide schemes are %s." | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def _test_scheme_salt(cls, scheme): | ||||||
|  |         """Test if the scheme need a salt or raise BadScheme""" | ||||||
|  |         if scheme not in cls.schemes_salt: | ||||||
|  |             cls._raise_bad_scheme( | ||||||
|  |                 scheme, | ||||||
|  |                 cls.schemes_salt, | ||||||
|  |                 "The scheme %r is only valid without a salt. Valide schemes with salt are %s." | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def _test_scheme_nosalt(cls, scheme): | ||||||
|  |         """Test if the scheme need no salt or raise BadScheme""" | ||||||
|  |         if scheme not in cls.schemes_nosalt: | ||||||
|  |             cls._raise_bad_scheme( | ||||||
|  |                 scheme, | ||||||
|  |                 cls.schemes_nosalt, | ||||||
|  |                 "The scheme %r is only valid with a salt. Valide schemes without salt are %s." | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def hash(cls, scheme, password, salt=None, charset="utf8"): | ||||||
|  |         """ | ||||||
|  |            Hash `password` with `scheme` using `salt`. | ||||||
|  |            This three variable beeing encoded in `charset`. | ||||||
|  |         """ | ||||||
|  |         scheme = scheme.upper() | ||||||
|  |         cls._test_scheme(scheme) | ||||||
|  |         if salt is None or salt == b"": | ||||||
|  |             salt = b"" | ||||||
|  |             cls._test_scheme_nosalt(scheme) | ||||||
|  |         elif salt is not None: | ||||||
|  |             cls._test_scheme_salt(scheme) | ||||||
|  |         try: | ||||||
|  |             return scheme + base64.b64encode( | ||||||
|  |                 cls._schemes_to_hash[scheme](password + salt).digest() + salt | ||||||
|  |             ) | ||||||
|  |         except KeyError: | ||||||
|  |             if six.PY3: | ||||||
|  |                 password = password.decode(charset) | ||||||
|  |                 salt = salt.decode(charset) | ||||||
|  |             hashed_password = crypt.crypt(password, salt) | ||||||
|  |             if hashed_password is None: | ||||||
|  |                 raise cls.BadSalt("System crypt implementation do not support the salt %r" % salt) | ||||||
|  |             if six.PY3: | ||||||
|  |                 hashed_password = hashed_password.encode(charset) | ||||||
|  |             return scheme + hashed_password | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def get_scheme(cls, hashed_passord): | ||||||
|  |         """Return the scheme of `hashed_passord` or raise BadHash""" | ||||||
|  |         if not hashed_passord[0] == b'{'[0] or b'}' not in hashed_passord: | ||||||
|  |             raise cls.BadHash("%r should start with the scheme enclosed with { }" % hashed_passord) | ||||||
|  |         scheme = hashed_passord.split(b'}', 1)[0] | ||||||
|  |         scheme = scheme.upper() + b"}" | ||||||
|  |         return scheme | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def get_salt(cls, hashed_passord): | ||||||
|  |         """Return the salt of `hashed_passord` possibly empty""" | ||||||
|  |         scheme = cls.get_scheme(hashed_passord) | ||||||
|  |         cls._test_scheme(scheme) | ||||||
|  |         if scheme in cls.schemes_nosalt: | ||||||
|  |             return b"" | ||||||
|  |         elif scheme == b'{CRYPT}': | ||||||
|  |             return b'$'.join(hashed_passord.split(b'$', 3)[:-1]) | ||||||
|  |         else: | ||||||
|  |             hashed_passord = base64.b64decode(hashed_passord[len(scheme):]) | ||||||
|  |             if len(hashed_passord) < cls._schemes_to_len[scheme]: | ||||||
|  |                 raise cls.BadHash("Hash too short for the scheme %s" % scheme) | ||||||
|  |             return hashed_passord[cls._schemes_to_len[scheme]:] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def check_password(method, password, hashed_password, charset): | ||||||
|  |     """ | ||||||
|  |         Check that `password` match `hashed_password` using `method`, | ||||||
|  |         assuming the encoding is `charset`. | ||||||
|  |     """ | ||||||
|  |     if not isinstance(password, six.binary_type): | ||||||
|  |         password = password.encode(charset) | ||||||
|  |     if not isinstance(hashed_password, six.binary_type): | ||||||
|  |         hashed_password = hashed_password.encode(charset) | ||||||
|  |     if method == "plain": | ||||||
|  |         return password == hashed_password | ||||||
|  |     elif method == "crypt": | ||||||
|  |         if hashed_password.startswith(b'$'): | ||||||
|  |             salt = b'$'.join(hashed_password.split(b'$', 3)[:-1]) | ||||||
|  |         elif hashed_password.startswith(b'_'): | ||||||
|  |             salt = hashed_password[:9] | ||||||
|  |         else: | ||||||
|  |             salt = hashed_password[:2] | ||||||
|  |         if six.PY3: | ||||||
|  |             password = password.decode(charset) | ||||||
|  |             salt = salt.decode(charset) | ||||||
|  |             hashed_password = hashed_password.decode(charset) | ||||||
|  |         crypted_password = crypt.crypt(password, salt) | ||||||
|  |         if crypted_password is None: | ||||||
|  |             raise ValueError("System crypt implementation do not support the salt %r" % salt) | ||||||
|  |         return crypted_password == hashed_password | ||||||
|  |     elif method == "ldap": | ||||||
|  |         scheme = LdapHashUserPassword.get_scheme(hashed_password) | ||||||
|  |         salt = LdapHashUserPassword.get_salt(hashed_password) | ||||||
|  |         return LdapHashUserPassword.hash(scheme, password, salt, charset=charset) == hashed_password | ||||||
|  |     elif ( | ||||||
|  |        method.startswith("hex_") and | ||||||
|  |        method[4:] in {"md5", "sha1", "sha224", "sha256", "sha384", "sha512"} | ||||||
|  |     ): | ||||||
|  |         return getattr( | ||||||
|  |             hashlib, | ||||||
|  |             method[4:] | ||||||
|  |         )(password).hexdigest().encode("ascii") == hashed_password.lower() | ||||||
|  |     else: | ||||||
|  |         raise ValueError("Unknown password method check %r" % method) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user