Welcome, guest | Sign In | My Account | Store | Cart

(Incomplete) Python implementation of Digest Authentication

Update: It works now (with IE as well) and all relevant code has now been wrapped into the class so basically all you need is feed it the Authorization-header. Unfortunately this means no CGI on apache.

Python, 126 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import md5
import time

class DigestAuth:
    def __init__(self, realm, users):
        """Incomplete Python implementation of Digest Authentication.

        For the full specification. http://www.faqs.org/rfcs/rfc2617.html

        realm = AuthName in httpd.conf
        users = a dict of users containing {username:password}
        """
        self.realm = realm
        self.users = users
        self._headers= []
        self.params = {}

    def H(self, data):
        return md5.md5(data).hexdigest()

    def KD(self, secret, data):
        return self.H(secret + ":" + data)

    def A1(self):
        # If the "algorithm" directive's value is "MD5" or is
        # unspecified, then A1 is:
        # A1 = unq(username-value) ":" unq(realm-value) ":" passwd
        username = self.params["username"]
        passwd = self.users.get(username, "")
        return "%s:%s:%s" % (username, self.realm, passwd)
        # This is A1 if qop is set
        # A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd )
        #         ":" unq(nonce-value) ":" unq(cnonce-value)

    def A2(self):
        # If the "qop" directive's value is "auth" or is unspecified, then A2 is:
        # A2 = Method ":" digest-uri-value
        return self.method + ":" + self.uri
        # Not implemented
        # If the "qop" value is "auth-int", then A2 is:
        # A2 = Method ":" digest-uri-value ":" H(entity-body)

    def response(self):
        if self.params.has_key("qop"):
            # Check? and self.params["qop"].lower()=="auth":
            # If the "qop" value is "auth" or "auth-int":
            # request-digest  = <"> < KD ( H(A1),     unq(nonce-value)
            #                              ":" nc-value
            #                              ":" unq(cnonce-value)
            #                              ":" unq(qop-value)
            #                              ":" H(A2)
            #                      ) <">
            return self.KD(self.H(self.A1()), \
                           self.params["nonce"] 
                           + ":" + self.params["nc"]
                           + ":" + self.params["cnonce"]
                           + ":" + self.params["qop"]                    
                           + ":" + self.H(self.A2()))
        else:
            # If the "qop" directive is not present (this construction is
            # for compatibility with RFC 2069):
            # request-digest  =
            #         <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <">
            return self.KD(self.H(self.A1()), \
                           self.params["nonce"] + ":" + self.H(self.A2()))
    
    def _parseHeader(self, authheader):
        try:
            n = len("Digest ")
            authheader = authheader[n:].strip()
            items = authheader.split(", ")
            keyvalues = [i.split("=", 1) for i in items]
            keyvalues = [(k.strip(), v.strip().replace('"', '')) for k, v in keyvalues]
            self.params = dict(keyvalues)
        except:
            self.params = []

    def _returnTuple(self, code):
        return (code, self._headers, self.params.get("username", ""))

    def _createNonce(self):
        return md5.md5("%d:%s" % (time.time(), self.realm)).hexdigest()

    def createAuthheaer(self):
        self._headers.append((
            "WWW-Authenticate", 
            'Digest realm="%s", nonce="%s", algorithm="MD5", qop="auth"'
            % (self.realm, self._createNonce())
            ))

    def authenticate(self, method, uri, authheader=""):
        """ Check the response for this method and uri with authheader

        returns a tuple with:
          - HTTP_CODE
          - a tuple with header info (key, value) or None
          - and the username which was authenticated or None
        """
        self.method = method
        self.uri = uri
        if authheader.strip() == "":
            self.createAuthheaer()
            return self._returnTuple(401)
        self._parseHeader(authheader)
        if not len(self.params):
            return self._returnTuple(400)
        # Check for required parameters
        required = ["username", "realm", "nonce", "uri", "response"]
        for k in required:
            if not self.params.has_key(k):
                return self._returnTuple(400)
        # If the user is unknown we can deny access right away
        if not self.users.has_key(self.params["username"]):
            self.createAuthheaer()
            return self._returnTuple(401)
        # If qop is sent then cnonce and cn MUST be present
        if self.params.has_key("qop"):
            if not self.params.has_key("cnonce") \
               and self.params.has_key("cn"):
                return self._returnTuple(400)
        # All else is OK, now check the response.
        if self.response() == self.params["response"]:
            return self._returnTuple(200)
        else:
            self.createAuthheaer()
            return self._returnTuple(401)

This class only implements algorithm 'MD5' (not MD5-Sess) and qop 'auth' (not auth-int) and the fallback protocal for RFC2069.

Here's an example with mod_python 3.1 and apache2.

Save the code above as digestauth.py

Update apache configuration:

<Directory /var/www/playground/da> SetHandler mod_python PythonAuthenHandler test PythonDebug On require valid-user </Directory>

Save this file as /var/www/playground/da/test.py

from mod_python import apache

import digestauth

realm = "Restricted Area"

users = { "user":"password" }

def authenhandler(req): authheader = req.headers_in.get("Authorization", "") da = digestauth.DigestAuth(realm, users) code, headers, req.user = da.authenticate(req.method, req.uri, authheader) for k, v in headers: req.headers_out.add(k, v) if code == 200: return apache.OK else: req.status = code

return apache.OK

restart apache2

The files in directory /var/www/playground/da are now protected.

2 comments

Tim Diggins 15 years, 11 months ago  # | flag

patch: authentication-handler needs req.write(). The test.py authentication-handler needs a req.write() when return non-OK status code, otherwise, apache will deliver a header with a 401, but in the body return the requested document! (visible if you sniff the network, or using a custom browser, or more critically if you press "cancel" on the provided dialog box (Firefox/IE).

I enclose the whole of the file, as it's easier to see with markup

from mod_python import apache

import digestauth

realm = "Restricted Area"

users = {
"user":"password"
}

def authenhandler(req):
    authheader = req.headers_in.get("Authorization", "")
    da = digestauth.DigestAuth(realm, users)
    code, headers, req.user = da.authenticate(req.method, req.uri, authheader)
    for k, v in headers:
        req.headers_out.add(k, v)
    if code == 200:
        return apache.OK
    else:
        req.status = code
        req.write("""Sorry.
           You need to provide adequate credentials to view this.
        """)
           # or some other message to stop
           # apache from doing the default
           # - delivering the requested document anyway!
        return apache.OK
Graham Dumpleton 15 years, 7 months ago  # | flag

Use req.err_headers_out. If writing back your own error response content from an authenhandler in mod_python, you should be returning apache.DONE to avoid Apache adding its own on the end.

You don't specifically need to write your own error response content anyway. The reason the original code possibly failed is that it was adding the 'WWW-Authenticate' header to req.headers_out and not req.err_headers_out. As a consequence Apache would have ignored it when using its own error response.

Created by Peter van Kampen on Fri, 27 Aug 2004 (PSF)
Python recipes (4591)
Peter van Kampen's recipes (4)

Required Modules

Other Information and Tasks