Source

django-rest-interface / django_restapi / authentication.py

Full commit
from django.http import HttpResponse
from django.utils.translation import ugettext as _
import md5, time, random

def djangouser_auth(username, password):
    """
    Check username and password against
    django.contrib.auth.models.User
    """
    from django.contrib.auth.models import User
    try:
        user = User.objects.get(username=username)
        if user.check_password(password):
            return True
        else:
            return False
    except User.DoesNotExist:
        return False

class NoAuthentication(object):
    """
    No authentication: Permit every request.
    """
    def is_authenticated(self, request):
        return True

    def challenge_headers(self):
        return {}

class HttpBasicAuthentication(object):
    """
    HTTP/1.0 basic authentication.
    """    
    def __init__(self, authfunc=djangouser_auth, realm=_('Restricted Access')):
        """
        authfunc:
            A user-defined function which takes a username and
            password as its first and second arguments respectively
            and returns True if the user is authenticated
        realm:
            An identifier for the authority that is requesting
            authorization
        """
        self.realm = realm
        self.authfunc = authfunc
    
    def challenge_headers(self):
        """
        Returns the http headers that ask for appropriate
        authorization.
        """
        return {'WWW-Authenticate' : 'Basic realm="%s"' % self.realm}
    
    def is_authenticated(self, request):
        """
        Checks whether a request comes from an authorized user.
        """
        if not request.META.has_key('HTTP_AUTHORIZATION'):
            return False
        (authmeth, auth) = request.META['HTTP_AUTHORIZATION'].split(' ', 1)
        if authmeth.lower() != 'basic':
            return False
        auth = auth.strip().decode('base64')
        username, password = auth.split(':', 1)
        return self.authfunc(username=username, password=password)

def digest_password(realm, username, password):
    """
    Construct the appropriate hashcode needed for HTTP digest
    """
    return md5.md5("%s:%s:%s" % (username, realm, password)).hexdigest()

class HttpDigestAuthentication(object):
    """
    HTTP/1.1 digest authentication (RFC 2617).
    Uses code from the Python Paste Project (MIT Licence).
    """    
    def __init__(self, authfunc, realm=_('Restricted Access')):
        """
        authfunc:
            A user-defined function which takes a username and
            a realm as its first and second arguments respectively
            and returns the combined md5 hash of username,
            authentication realm and password.
        realm:
            An identifier for the authority that is requesting
            authorization
        """
        self.realm = realm
        self.authfunc = authfunc
        self.nonce    = {} # prevention of replay attacks

    def get_auth_dict(self, auth_string):
        """
        Splits WWW-Authenticate and HTTP_AUTHORIZATION strings
        into a dictionaries, e.g.
        {
            nonce  : "951abe58eddbb49c1ed77a3a5fb5fc2e"',
            opaque : "34de40e4f2e4f4eda2a3952fd2abab16"',
            realm  : "realm1"',
            qop    : "auth"'
        }
        """
        amap = {}
        for itm in auth_string.split(", "):
            (k, v) = [s.strip() for s in itm.split("=", 1)]
            amap[k] = v.replace('"', '')
        return amap

    def get_auth_response(self, http_method, fullpath, username, nonce, realm, qop, cnonce, nc):
        """
        Returns the server-computed digest response key.
        
        http_method:
            The request method, e.g. GET
        username:
            The user to be authenticated
        fullpath:
            The absolute URI to be accessed by the user
        nonce:
            A server-specified data string which should be 
            uniquely generated each time a 401 response is made
        realm:
            A string to be displayed to users so they know which 
            username and password to use
        qop:
            Indicates the "quality of protection" values supported 
            by the server.  The value "auth" indicates authentication.
        cnonce:
            An opaque quoted string value provided by the client 
            and used by both client and server to avoid chosen 
            plaintext attacks, to provide mutual authentication, 
            and to provide some message integrity protection.
        nc:
            Hexadecimal request counter
        """
        ha1 = self.authfunc(realm, username)
        ha2 = md5.md5('%s:%s' % (http_method, fullpath)).hexdigest()
        if qop:
            chk = "%s:%s:%s:%s:%s:%s" % (ha1, nonce, nc, cnonce, qop, ha2)
        else:
            chk = "%s:%s:%s" % (ha1, nonce, ha2)
        computed_response = md5.md5(chk).hexdigest()
        return computed_response
    
    def challenge_headers(self, stale=''):
        """
        Returns the http headers that ask for appropriate
        authorization.
        """
        nonce  = md5.md5(
            "%s:%s" % (time.time(), random.random())).hexdigest()
        opaque = md5.md5(
            "%s:%s" % (time.time(), random.random())).hexdigest()
        self.nonce[nonce] = None
        parts = {'realm': self.realm, 'qop': 'auth',
                 'nonce': nonce, 'opaque': opaque }
        if stale:
            parts['stale'] = 'true'
        head = ", ".join(['%s="%s"' % (k, v) for (k, v) in parts.items()])
        return {'WWW-Authenticate':'Digest %s' % head}
    
    def is_authenticated(self, request):
        """
        Checks whether a request comes from an authorized user.
        """
        
        # Make sure the request is a valid HttpDigest request
        if not request.META.has_key('HTTP_AUTHORIZATION'):
            return False
        fullpath = request.META['SCRIPT_NAME'] + request.META['PATH_INFO']
        (authmeth, auth) = request.META['HTTP_AUTHORIZATION'].split(" ", 1)
        if authmeth.lower() != 'digest':
            return False
        
        # Extract auth parameters from request
        amap = self.get_auth_dict(auth)
        try:
            username = amap['username']
            authpath = amap['uri']
            nonce    = amap['nonce']
            realm    = amap['realm']
            response = amap['response']
            assert authpath.split("?", 1)[0] in fullpath
            assert realm == self.realm
            qop      = amap.get('qop', '')
            cnonce   = amap.get('cnonce', '')
            nc       = amap.get('nc', '00000000')
            if qop:
                assert 'auth' == qop
                assert nonce and nc
        except:
            return False

        # Compute response key    
        computed_response = self.get_auth_response(request.method, fullpath, username, nonce, realm, qop, cnonce, nc)
        
        # Compare server-side key with key from client
        # Prevent replay attacks
        if not computed_response or computed_response != response:
            if nonce in self.nonce:
                del self.nonce[nonce]
            return False
        pnc = self.nonce.get(nonce,'00000000')
        if nc <= pnc:
            if nonce in self.nonce:
                del self.nonce[nonce]
            return False # stale = True
        self.nonce[nonce] = nc
        return True