254 lines
9.1 KiB
Python
254 lines
9.1 KiB
Python
|
# (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
|
||
|
"""
|
||
|
Digest HTTP/1.1 Authentication
|
||
|
|
||
|
This module implements ``Digest`` authentication as described by
|
||
|
RFC 2617 [1]_ .
|
||
|
|
||
|
Basically, you just put this module before your application, and it
|
||
|
takes care of requesting and handling authentication requests. This
|
||
|
module has been tested with several common browsers "out-in-the-wild".
|
||
|
|
||
|
>>> from paste.wsgilib import dump_environ
|
||
|
>>> from paste.httpserver import serve
|
||
|
>>> # from paste.auth.digest import digest_password, AuthDigestHandler
|
||
|
>>> realm = 'Test Realm'
|
||
|
>>> def authfunc(environ, realm, username):
|
||
|
... return digest_password(realm, username, username)
|
||
|
>>> serve(AuthDigestHandler(dump_environ, realm, authfunc))
|
||
|
serving on...
|
||
|
|
||
|
This code 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.
|
||
|
|
||
|
.. [1] http://www.faqs.org/rfcs/rfc2617.html
|
||
|
"""
|
||
|
from paste.httpexceptions import HTTPUnauthorized
|
||
|
from paste.httpheaders import *
|
||
|
try:
|
||
|
from hashlib import md5
|
||
|
except ImportError:
|
||
|
from md5 import md5
|
||
|
import time, random
|
||
|
from six.moves.urllib.parse import quote as url_quote
|
||
|
import six
|
||
|
|
||
|
def _split_auth_string(auth_string):
|
||
|
""" split a digest auth string into individual key=value strings """
|
||
|
prev = None
|
||
|
for item in auth_string.split(","):
|
||
|
try:
|
||
|
if prev.count('"') == 1:
|
||
|
prev = "%s,%s" % (prev, item)
|
||
|
continue
|
||
|
except AttributeError:
|
||
|
if prev == None:
|
||
|
prev = item
|
||
|
continue
|
||
|
else:
|
||
|
return
|
||
|
yield prev.strip()
|
||
|
prev = item
|
||
|
|
||
|
yield prev.strip()
|
||
|
|
||
|
def _auth_to_kv_pairs(auth_string):
|
||
|
""" split a digest auth string into key, value pairs """
|
||
|
for item in _split_auth_string(auth_string):
|
||
|
(k, v) = item.split("=", 1)
|
||
|
if v.startswith('"') and len(v) > 1 and v.endswith('"'):
|
||
|
v = v[1:-1]
|
||
|
yield (k, v)
|
||
|
|
||
|
def digest_password(realm, username, password):
|
||
|
""" construct the appropriate hashcode needed for HTTP digest """
|
||
|
content = "%s:%s:%s" % (username, realm, password)
|
||
|
if six.PY3:
|
||
|
content = content.encode('utf8')
|
||
|
return md5(content).hexdigest()
|
||
|
|
||
|
class AuthDigestAuthenticator(object):
|
||
|
""" implementation of RFC 2617 - HTTP Digest Authentication """
|
||
|
def __init__(self, realm, authfunc):
|
||
|
self.nonce = {} # list to prevent replay attacks
|
||
|
self.authfunc = authfunc
|
||
|
self.realm = realm
|
||
|
|
||
|
def build_authentication(self, stale = ''):
|
||
|
""" builds the authentication error """
|
||
|
content = "%s:%s" % (time.time(), random.random())
|
||
|
if six.PY3:
|
||
|
content = content.encode('utf-8')
|
||
|
nonce = md5(content).hexdigest()
|
||
|
|
||
|
content = "%s:%s" % (time.time(), random.random())
|
||
|
if six.PY3:
|
||
|
content = content.encode('utf-8')
|
||
|
opaque = md5(content).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()
|
||
|
content = '%s:%s' % (method, path)
|
||
|
if six.PY3:
|
||
|
content = content.encode('utf8')
|
||
|
ha2 = md5(content).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 six.PY3:
|
||
|
chk = chk.encode('utf8')
|
||
|
if response != md5(chk).hexdigest():
|
||
|
if nonce in self.nonce:
|
||
|
del self.nonce[nonce]
|
||
|
return self.build_authentication()
|
||
|
pnc = self.nonce.get(nonce,'00000000')
|
||
|
if pnc is not None and 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, environ):
|
||
|
""" This function takes a WSGI environment and authenticates
|
||
|
the request returning authenticated user or error.
|
||
|
"""
|
||
|
method = REQUEST_METHOD(environ)
|
||
|
fullpath = url_quote(SCRIPT_NAME(environ)) + url_quote(PATH_INFO(environ))
|
||
|
authorization = AUTHORIZATION(environ)
|
||
|
if not authorization:
|
||
|
return self.build_authentication()
|
||
|
(authmeth, auth) = authorization.split(" ", 1)
|
||
|
if 'digest' != authmeth.lower():
|
||
|
return self.build_authentication()
|
||
|
amap = dict(_auth_to_kv_pairs(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 self.build_authentication()
|
||
|
ha1 = self.authfunc(environ, realm, username)
|
||
|
return self.compute(ha1, username, response, method, authpath,
|
||
|
nonce, nc, cnonce, qop)
|
||
|
|
||
|
__call__ = authenticate
|
||
|
|
||
|
class AuthDigestHandler(object):
|
||
|
"""
|
||
|
middleware for HTTP Digest authentication (RFC 2617)
|
||
|
|
||
|
This component follows the procedure below:
|
||
|
|
||
|
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 or specifies
|
||
|
an algorithem other than ``digest``, then a HTTPUnauthorized
|
||
|
response is generated with the challenge.
|
||
|
|
||
|
2. If the response is malformed or or if the user's credientials
|
||
|
do not pass muster, another HTTPUnauthorized is raised.
|
||
|
|
||
|
3. 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'.
|
||
|
|
||
|
Parameters:
|
||
|
|
||
|
``application``
|
||
|
|
||
|
The application object is called only upon successful
|
||
|
authentication, and can assume ``environ['REMOTE_USER']``
|
||
|
is set. If the ``REMOTE_USER`` is already set, this
|
||
|
middleware is simply pass-through.
|
||
|
|
||
|
``realm``
|
||
|
|
||
|
This is a identifier for the authority that is requesting
|
||
|
authorization. It is shown to the user and should be unique
|
||
|
within the domain it is being used.
|
||
|
|
||
|
``authfunc``
|
||
|
|
||
|
This is a callback function which performs the actual
|
||
|
authentication; the signature of this callback is:
|
||
|
|
||
|
authfunc(environ, 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 (since you only need the hashcode).
|
||
|
"""
|
||
|
def __init__(self, application, realm, authfunc):
|
||
|
self.authenticate = AuthDigestAuthenticator(realm, authfunc)
|
||
|
self.application = application
|
||
|
|
||
|
def __call__(self, environ, start_response):
|
||
|
username = REMOTE_USER(environ)
|
||
|
if not username:
|
||
|
result = self.authenticate(environ)
|
||
|
if isinstance(result, str):
|
||
|
AUTH_TYPE.update(environ,'digest')
|
||
|
REMOTE_USER.update(environ, result)
|
||
|
else:
|
||
|
return result.wsgi_application(environ, start_response)
|
||
|
return self.application(environ, start_response)
|
||
|
|
||
|
middleware = AuthDigestHandler
|
||
|
|
||
|
__all__ = ['digest_password', 'AuthDigestHandler' ]
|
||
|
|
||
|
def make_digest(app, global_conf, realm, authfunc, **kw):
|
||
|
"""
|
||
|
Grant access via digest authentication
|
||
|
|
||
|
Config looks like this::
|
||
|
|
||
|
[filter:grant]
|
||
|
use = egg:Paste#auth_digest
|
||
|
realm=myrealm
|
||
|
authfunc=somepackage.somemodule:somefunction
|
||
|
|
||
|
"""
|
||
|
from paste.util.import_string import eval_import
|
||
|
import types
|
||
|
authfunc = eval_import(authfunc)
|
||
|
assert isinstance(authfunc, types.FunctionType), "authfunc must resolve to a function"
|
||
|
return AuthDigestHandler(app, realm, authfunc)
|
||
|
|
||
|
if "__main__" == __name__:
|
||
|
import doctest
|
||
|
doctest.testmod(optionflags=doctest.ELLIPSIS)
|