Add some password check methods to the MySQL auth backend

This commit is contained in:
Valentin Samir 2016-06-26 20:29:47 +02:00
parent b36a9a1523
commit ac206d56d6
3 changed files with 175 additions and 13 deletions

View file

@ -199,8 +199,18 @@ 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:

View file

@ -16,6 +16,7 @@ try: # pragma: no cover
import MySQLdb import MySQLdb
import MySQLdb.cursors import MySQLdb.cursors
import crypt import crypt
from utils import check_password
except ImportError: except ImportError:
MySQLdb = None MySQLdb = None
@ -90,17 +91,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":
if self.user["password"].startswith('$'):
salt = '$'.join(self.user["password"].split('$', 3)[:-1])
return crypt.crypt(password, salt) == self.user["password"]
else:
return crypt.crypt(
password, password,
self.user["password"][:2] self.user["password"],
) == self.user["password"] settings.CAS_SQL_DBCHARSET
)
else: else:
return False return False

View file

@ -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,155 @@ 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):
pass
class BadHash(ValueError):
pass
class BadSalt(ValueError):
pass
@classmethod
def _raise_bad_scheme(cls, scheme, valid, msg):
valid_schemes = [s for s in valid]
valid_schemes.sort()
raise cls.BadScheme(msg % (scheme, ", ".join(valid_schemes)))
@classmethod
def _test_scheme(cls, scheme):
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):
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):
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"):
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):
if not hashed_passord[0] == b'{' or not b'}' 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):
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):
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() == hashed_password.lower()
else:
raise ValueError("Unknown password method check %r" % method)