summaryrefslogtreecommitdiff
path: root/paste/auth/digest.py
diff options
context:
space:
mode:
Diffstat (limited to 'paste/auth/digest.py')
-rw-r--r--paste/auth/digest.py193
1 files changed, 193 insertions, 0 deletions
diff --git a/paste/auth/digest.py b/paste/auth/digest.py
new file mode 100644
index 0000000..598a103
--- /dev/null
+++ b/paste/auth/digest.py
@@ -0,0 +1,193 @@
+# (c) 2005 Clark C. Evans
+# This module is part of the Python Paste Project and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+# This code was written with funding by http://prometheusresearch.com
+"""
+HTTP Digest Authentication (RFC 2617)
+
+NOTE: This has not been audited by a security expert, please use
+ with caution (or better yet, report security holes).
+
+ At this time, this implementation does not provide for further
+ challenges, nor does it support Authentication-Info header. It
+ also uses md5, and an option to use sha would be a good thing.
+"""
+from paste.httpexceptions import HTTPUnauthorized
+import md5, time, random, urllib2
+
+def digest_password(username, realm, password):
+ """ Constructs the appropriate hashcode needed for HTTP Digest """
+ return md5.md5("%s:%s:%s" % (username,realm,password)).hexdigest()
+
+def response(challenge, realm, path, username, password):
+ """
+ Build an authorization response for a given challenge. This
+ implementation uses urllib2 to do the dirty work.
+ """
+ auth = urllib2.AbstractDigestAuthHandler()
+ auth.add_password(realm,path,username,password)
+ (token,challenge) = challenge.split(' ',1)
+ chal = urllib2.parse_keqv_list(urllib2.parse_http_list(challenge))
+ class FakeRequest:
+ def get_full_url(self):
+ return path
+ def has_data(self):
+ return False
+ def get_method(self):
+ return "GET"
+ get_selector = get_full_url
+ return "Digest %s" % auth.get_authorization(FakeRequest(), chal)
+
+class DigestAuthenticator:
+ """ Simple implementation of RFC 2617 - HTTP Digest Authentication """
+ def __init__(self, realm, userfunc):
+ """
+ realm is a globally unique URI, like tag:clarkevans.com,2005:bing
+ userfunc(realm, username) -> MD5('%s:%s:%s') % (user,realm,pass)
+ """
+ self.nonce = {} # list to prevent replay attacks
+ self.userfunc = userfunc
+ self.realm = realm
+
+ def build_authentication(self, stale = ''):
+ """ raises an authentication exception """
+ 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()])
+ head = [("WWW-Authenticate", 'Digest %s' % head)]
+ return HTTPUnauthorized(headers=head)
+
+ def compute(self, ha1, username, response, method,
+ path, nonce, nc, cnonce, qop):
+ """ computes the authentication, raises error if unsuccessful """
+ if not ha1:
+ return self.build_authentication()
+ ha2 = md5.md5('%s:%s' % (method,path)).hexdigest()
+ if qop:
+ chk = "%s:%s:%s:%s:%s:%s" % (ha1,nonce,nc,cnonce,qop,ha2)
+ else:
+ chk = "%s:%s:%s" % (ha1,nonce,ha2)
+ if response != md5.md5(chk).hexdigest():
+ if nonce in self.nonce:
+ del self.nonce[nonce]
+ return self.build_authentication()
+ pnc = self.nonce.get(nonce,'00000000')
+ if nc <= pnc:
+ if nonce in self.nonce:
+ del self.nonce[nonce]
+ return self.build_authentication(stale = True)
+ self.nonce[nonce] = nc
+ return username
+
+ def authenticate(self, authorization, path, method):
+ """ This function takes the value of the 'Authorization' header,
+ the method used (e.g. GET), and the path of the request
+ relative to the server. The function either returns an
+ authenticated user, or it raises an exception.
+ """
+ if not authorization:
+ return self.build_authentication()
+ (authmeth, auth) = authorization.split(" ",1)
+ if 'digest' != authmeth.lower():
+ return self.build_authentication()
+ amap = {}
+ for itm in auth.split(", "):
+ (k,v) = [s.strip() for s in itm.split("=",1)]
+ amap[k] = v.replace('"','')
+ try:
+ username = amap['username']
+ authpath = amap['uri']
+ nonce = amap['nonce']
+ realm = amap['realm']
+ response = amap['response']
+ assert authpath.split("?",1)[0] in path
+ 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 self.build_authentication()
+ ha1 = self.userfunc(realm,username)
+ return self.compute(ha1, username, response, method, authpath,
+ nonce, nc, cnonce, qop)
+
+ __call__ = authenticate
+
+def AuthDigestHandler(application, realm, userfunc):
+ """
+ This middleware implements HTTP Digest authentication (RFC 2617) on
+ the incoming request. There are several possible outcomes:
+
+ 0. If the REMOTE_USER environment variable is already populated;
+ then this middleware is a no-op, and the request is passed along
+ to the application.
+
+ 1. If the HTTP_AUTHORIZATION header was not provided, then a
+ HTTPUnauthorized exception is raised containing the challenge.
+
+ 2. If the HTTP_AUTHORIZATION header specifies anything other
+ than digest; the REMOTE_USER is left unset and application
+ processing continues.
+
+ 3. If the response is malformed or or if the user's credientials
+ do not pass muster, another HTTPUnauthorized is raised.
+
+ 4. IF all goes well, and the user's credintials pass; then
+ REMOTE_USER environment variable is filled in and the
+ AUTH_TYPE is listed as 'digest'.
+
+ Besides the application to delegate requests, this middleware
+ requires two additional arguments:
+
+ realm:
+ This is a globally unique identifier used to indicate the
+ authority that is performing the authentication. The taguri
+ such as tag:yourdomain.com,2006 is sufficient.
+
+ userfunc:
+ This is a callback function which performs the actual
+ authentication; the signature of this callback is:
+
+ userfunc(realm, username) -> hashcode
+
+ This module provides a 'digest_password' helper function which
+ can help construct the hashcode; it is recommended that the
+ hashcode is stored in a database, not the user's actual password.
+ """
+ authenticator = DigestAuthenticator(realm, userfunc)
+ def digest_application(environ, start_response):
+ username = environ.get('REMOTE_USER','')
+ if not username:
+ method = environ['REQUEST_METHOD']
+ fullpath = environ['SCRIPT_NAME'] + environ["PATH_INFO"]
+ authorization = environ.get('HTTP_AUTHORIZATION','')
+ result = authenticator(authorization, fullpath, method)
+ if isinstance(result, str):
+ environ['AUTH_TYPE'] = 'digest'
+ environ['REMOTE_USER'] = result
+ else:
+ return result.wsgi_application(environ, start_response)
+ return application(environ, start_response)
+ return digest_application
+
+middleware = AuthDigestHandler
+
+__all__ = ['digest_password', 'AuthDigestHandler' ]
+
+if '__main__' == __name__:
+ realm = 'tag:clarkevans.com,2005:digest'
+ def userfunc(realm, username):
+ return digest_password(username, realm, username)
+ from paste.wsgilib import dump_environ
+ from paste.util.baseserver import serve
+ from paste.httpexceptions import *
+ serve(HTTPExceptionHandler(
+ AuthDigestHandler(dump_environ, realm, userfunc)))