(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.
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.
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
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.