summaryrefslogtreecommitdiff
path: root/paste/auth
diff options
context:
space:
mode:
authorianb <devnull@localhost>2005-12-13 07:00:20 +0000
committerianb <devnull@localhost>2005-12-13 07:00:20 +0000
commit4e73bff9da87e35c7154ab1cc923bb4f9d40711d (patch)
treed2e4c92965398700457280d5829dfaa5cdf5b4fb /paste/auth
parent55b404e53bc834daf3852069af6de9b1fca4c742 (diff)
downloadpaste-4e73bff9da87e35c7154ab1cc923bb4f9d40711d.tar.gz
Merged changes from cce branch (r3727:HEAD/4008); the branch is now in sync with trunk
Diffstat (limited to 'paste/auth')
-rw-r--r--paste/auth/__init__.py1
-rw-r--r--paste/auth/basic.py67
-rw-r--r--paste/auth/cas.py94
-rw-r--r--paste/auth/cookie.py229
-rw-r--r--paste/auth/digest.py193
-rw-r--r--paste/auth/form.py73
-rw-r--r--paste/auth/multi.py81
7 files changed, 738 insertions, 0 deletions
diff --git a/paste/auth/__init__.py b/paste/auth/__init__.py
new file mode 100644
index 0000000..792d600
--- /dev/null
+++ b/paste/auth/__init__.py
@@ -0,0 +1 @@
+#
diff --git a/paste/auth/basic.py b/paste/auth/basic.py
new file mode 100644
index 0000000..8a7e787
--- /dev/null
+++ b/paste/auth/basic.py
@@ -0,0 +1,67 @@
+# (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
+"""
+Basic Authentication
+
+"""
+from paste.httpexceptions import HTTPUnauthorized
+
+class BasicAuthenticator:
+ """ Implementation of only 'Basic' authentication in 2617 """
+ def __init__(self, realm, userfunc):
+ """
+ realm is a globally unique URI like tag:clarkevans.com,2005:basic
+ that represents the authenticating authority
+ userfunc(username, password) -> boolean
+ """
+ self.realm = realm
+ self.userfunc = userfunc
+
+ def build_authentication(self):
+ head = [('WWW-Authenticate','Basic realm="%s"' % self.realm)]
+ return HTTPUnauthorized(headers=head)
+
+ def authenticate(self, authorization):
+ if not authorization:
+ return self.build_authentication()
+ (authmeth, auth) = authorization.split(" ",1)
+ if 'basic' != authmeth.lower():
+ return self.build_authentication()
+ auth = auth.strip().decode('base64')
+ username, password = auth.split(':')
+ if self.userfunc(username, password):
+ return username
+ return self.build_authentication()
+
+ __call__ = authenticate
+
+def AuthBasicHandler(application, realm, userfunc):
+ authenticator = BasicAuthenticator(realm, userfunc)
+ def basic_application(environ, start_response):
+ username = environ.get('REMOTE_USER','')
+ if not username:
+ authorization = environ.get('HTTP_AUTHORIZATION','')
+ result = authenticator(authorization)
+ if isinstance(result,str):
+ environ['AUTH_TYPE'] = 'basic'
+ environ['REMOTE_USER'] = result
+ else:
+ return result.wsgi_application(environ, start_response)
+ return application(environ, start_response)
+ return basic_application
+
+middleware = AuthBasicHandler
+
+__all__ = ['AuthBasicHandler']
+
+if '__main__' == __name__:
+ realm = 'tag:clarkevans.com,2005:basic'
+ def userfunc(username, password):
+ return username == password
+ from paste.wsgilib import dump_environ
+ from paste.util.baseserver import serve
+ from paste.httpexceptions import *
+ serve(HTTPExceptionHandler(
+ AuthBasicHandler(dump_environ, realm, userfunc)))
diff --git a/paste/auth/cas.py b/paste/auth/cas.py
new file mode 100644
index 0000000..193b79a
--- /dev/null
+++ b/paste/auth/cas.py
@@ -0,0 +1,94 @@
+# (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
+"""
+CAS 1.0 Authentication
+
+The Central Authentication System is a straight-forward single sign-on
+mechanism developed by Yale University's ITS department. It has since
+enjoyed widespread success and is deployed at many major universities
+and some corporations.
+
+ https://clearinghouse.ja-sig.org/wiki/display/CAS/Home
+ http://www.yale.edu/tp/auth/usingcasatyale.html
+
+This implementation has the goal of maintaining current path arguments
+passed to the system so that it can be used as middleware at any stage
+of processing. It has the secondary goal of allowing for other
+authentication methods to be used concurrently.
+"""
+import urllib
+from paste.wsgilib import construct_url
+from paste.httpexceptions import HTTPSeeOther, HTTPForbidden
+
+class CASLoginFailure(HTTPForbidden):
+ """ The exception raised if the authority returns 'no' """
+
+class CASAuthenticate(HTTPSeeOther):
+ """ The exception raised to authenticate the user """
+
+def AuthCASHandler(application, authority):
+ """
+ This middleware implements CAS 1.0 Authentication 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 a query argument 'ticket' is found, then an attempt to
+ validate said ticket /w the authentication service done. If the
+ ticket is not validated; an 403 'Forbidden' exception is raised.
+ Otherwise, the REMOTE_USER variable is set with the NetID that
+ was validated and AUTH_TYPE is set to "cas".
+
+ 2. Otherwise, a 303 'See Other' is returned to the client directing
+ them to login using the CAS service. After logon, the service
+ will send them back to this same URL, only with a 'ticket' query
+ argument.
+
+ authority:
+ This is a fully-qualified URL to a CAS 1.0 service. The URL
+ should end with a '/' and have the 'login' and 'validate'
+ sub-paths as described in the CAS 1.0 documentation.
+ """
+ assert authority.endswith("/") and authority.startswith("http")
+ def cas_application(environ, start_response):
+ username = environ.get('REMOTE_USER','')
+ if username:
+ return application(environ, start_response)
+ qs = environ.get('QUERY_STRING','').split("&")
+ if qs and qs[-1].startswith("ticket="):
+ # assume a response from the authority
+ ticket = qs.pop().split("=",1)[1]
+ environ['QUERY_STRING'] = "&".join(qs)
+ service = construct_url(environ)
+ args = urllib.urlencode(
+ {'service': service,'ticket': ticket})
+ requrl = authority + "validate?" + args
+ result = urllib.urlopen(requrl).read().split("\n")
+ if 'yes' == result[0]:
+ environ['REMOTE_USER'] = result[1]
+ environ['AUTH_TYPE'] = 'cas'
+ return application(environ, start_response)
+ exce = CASLoginFailure()
+ else:
+ service = construct_url(environ)
+ args = urllib.urlencode({'service': service})
+ location = authority + "login?" + args
+ exce = CASAuthenticate(location)
+ return exce.wsgi_application(environ, start_response)
+ return cas_application
+
+middleware = AuthCASHandler
+
+__all__ = ['CASLoginFailure', 'CASAuthenticate', 'AuthCASHandler' ]
+
+if '__main__' == __name__:
+ authority = "https://secure.its.yale.edu/cas/servlet/"
+ from paste.wsgilib import dump_environ
+ from paste.util.baseserver import serve
+ from paste.httpexceptions import *
+ serve(HTTPExceptionHandler(
+ AuthCASHandler(dump_environ, authority)))
diff --git a/paste/auth/cookie.py b/paste/auth/cookie.py
new file mode 100644
index 0000000..96071d7
--- /dev/null
+++ b/paste/auth/cookie.py
@@ -0,0 +1,229 @@
+# (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
+"""
+Cookie "Saved" Authentication
+
+This Authentication middleware saves the current REMOTE_USER, and any
+other environment variables specified, in a cookie so that it can be
+retrieved during the next request without requiring re-authentication.
+This uses a session cookie on the client side (so it goes away when the
+user closes their window) and does server-side expiration.
+
+ NOTE: If you use HTTPFound or other redirections; it is likely that
+ this module will not work unless it is _before_ the middleware
+ that converts the exception into a response. Therefore, in your
+ component stack, put this component darn near the top (before
+ the exception handler).
+
+According to the cookie specifications, RFC2068 and RFC2109, browsers
+should allow each domain at least 20 cookies; each one with a content
+size of at least 4k (4096 bytes). This is rather small; so one should
+be parsimonious in your cookie name/sizes.
+"""
+import sha, base64, random, time, string, warnings
+from paste.wsgilib import get_cookies
+
+def make_time(value):
+ """ return a human readable timestmp """
+ return time.strftime("%Y%m%d%H%M",time.gmtime(value))
+_signature_size = len(sha.sha("").digest())
+_header_size = _signature_size + len(make_time(time.time()))
+
+# build encode/decode functions to safely pack away values
+_encode = [('\\','\\x5c'),('"','\\x22'),('=','\\x3d'),(';','\\x3b')]
+_decode = [(v,k) for (k,v) in _encode]
+_decode.reverse()
+def encode(s, sublist = _encode):
+ return reduce((lambda a,(b,c): string.replace(a,b,c)), sublist, str(s))
+decode = lambda s: encode(s,_decode)
+
+class CookieTooLarge(RuntimeError):
+ def __init__(self, content, cookie):
+ RuntimeError.__init__("Signed cookie exceeds maximum size of 4096")
+ self.content = content
+ self.cookie = cookie
+
+class CookieSigner:
+ """
+ This class converts content into a timed and digitally signed
+ cookie, as well as having the facility to reverse this procedure.
+ If the cookie, after the content is encoded and signed exceeds the
+ maximum length (4096), then CookieTooLarge exception is raised.
+
+ The timeout of the cookie is handled on the server side for a few
+ reasons. First, if a 'Expires' directive is added to a cookie, then
+ the cookie becomes persistent (lasting even after the browser window
+ has closed). Second, the user's clock may be wrong (perhaps
+ intentionally). The timeout is specified in minutes; and expiration
+ date returned is rounded to one second.
+ """
+ def __init__(self, secret = None, timeout = None, maxlen = None):
+ self.timeout = timeout or 30
+ self.maxlen = maxlen or 4096
+ self.secret = secret or sha.sha(str(random.random()) +
+ str(time.time())).digest()
+
+ def sign(self, content):
+ """
+ Sign the content returning a valid cookie (that does not
+ need to be escaped and quoted). The expiration of this
+ cookie is handled server-side in the auth() function.
+ """
+ cookie = base64.b64encode(
+ sha.sha(content+self.secret).digest() +
+ make_time(time.time()+60*self.timeout) +
+ content).replace("/","_").replace("=","~")
+ if len(cookie) > self.maxlen:
+ raise CookieTooLarge(content,cookie)
+ return cookie
+
+ def auth(self,cookie):
+ """
+ Authenticate the cooke using the signature, verify that it
+ has not expired; and return the cookie's content
+ """
+ decode = base64.b64decode(
+ cookie.replace("_","/").replace("~","="))
+ signature = decode[:_signature_size]
+ expires = decode[_signature_size:_header_size]
+ content = decode[_header_size:]
+ if signature == sha.sha(content+self.secret).digest():
+ if int(expires) > int(make_time(time.time())):
+ return content
+ else:
+ # This is the normal case of an expired cookie; just
+ # don't bother doing anything here.
+ pass
+ else:
+ # This case can happen if the server is restarted with a
+ # different secret; or if the user's IP address changed
+ # due to a proxy. However, it could also be a break-in
+ # attempt -- so should it be reported?
+ pass
+
+class AuthCookieEnviron(list):
+ """
+ This object is a list of `environ` keys that were restored from or
+ will be added to the digially signed cookie. This object can be
+ accessed from an `environ` variable by using this module's name.
+
+ environ['paste.auth.cookie'].append('your.environ.variable')
+
+ This environment-specific object can also be used to access/configure
+ the base handler for all requests by using:
+
+ environ['paste.auth.cookie'].handler
+
+ """
+ def __init__(self, handler, scanlist):
+ list.__init__(self, scanlist)
+ self.handler = handler
+ def append(self, value):
+ if value in self:
+ return
+ list.append(self,str(value))
+
+class AuthCookieHandler:
+ """
+ This middleware uses cookies to stash-away a previously authenticated
+ user (and perhaps other variables) so that re-authentication is not
+ needed. This does not implement sessions; and therefore N servers
+ can be syncronized to accept the same saved authentication if they
+ all use the same cookie_name and secret.
+
+ By default, this handler scans the `environ` for the REMOTE_USER
+ key; if found, it is stored. It can be configured to scan other
+ `environ` keys as well -- but be careful not to exceed 2-3k (so that
+ the encoded and signed cookie does not exceed 4k). You can ask it
+ to handle other environment variables by doing:
+
+ environ['paste.auth.cookie'].append('your.environ.variable')
+
+ """
+ environ_name = 'paste.auth.cookie'
+ signer_class = CookieSigner
+ environ_class = AuthCookieEnviron
+
+ def __init__(self, application, cookie_name=None, secret=None,
+ timeout=None, maxlen=None, signer=None, scanlist = None):
+ if not signer:
+ signer = self.signer_class(secret,timeout,maxlen)
+ self.signer = signer
+ self.scanlist = scanlist or ('REMOTE_USER',)
+ self.application = application
+ self.cookie_name = cookie_name or 'PASTE_AUTH_COOKIE'
+
+ def __call__(self, environ, start_response):
+ if self.environ_name in environ:
+ raise AssertionError("AuthCookie already installed!")
+ scanlist = self.environ_class(self,self.scanlist)
+ jar = get_cookies(environ)
+ if jar.has_key(self.cookie_name):
+ content = self.signer.auth(jar[self.cookie_name].value)
+ if content:
+ for pair in content.split(";"):
+ (k,v) = pair.split("=")
+ k = decode(k)
+ if k not in scanlist:
+ scanlist.append(k)
+ if k in environ:
+ continue
+ environ[k] = decode(v)
+ if 'REMOTE_USER' == k:
+ environ['AUTH_TYPE'] = 'cookie'
+ environ[self.environ_name] = scanlist
+ if "paste.httpexceptions" in environ:
+ warnings.warn("Since paste.httpexceptions is hooked in your "
+ "processing chain before paste.auth.cookie, if an "
+ "HTTPRedirection is raised, the cookies this module sets "
+ "will not be included in your response.\n")
+
+ def response_hook(status, response_headers, exc_info=None):
+ """
+ Scan the environment for keys specified in the scanlist,
+ pack up their values, signs the content and issues a cookie.
+ """
+ scanlist = environ.get(self.environ_name)
+ assert scanlist and isinstance(scanlist,self.environ_class)
+ content = []
+ for k in scanlist:
+ v = environ.get(k,None)
+ if v is not None:
+ content.append("%s=%s" % (encode(k),encode(v)))
+ if content:
+ content = ";".join(content)
+ content = self.signer.sign(content)
+ cookie = '%s=%s; Path=/;' % (self.cookie_name, content)
+ if 'https' == environ['wsgi.url_scheme']:
+ cookie += ' secure;'
+ response_headers.append(('Set-Cookie',cookie))
+ return start_response(status, response_headers, exc_info)
+ return self.application(environ, response_hook)
+
+middleware = AuthCookieHandler
+
+__all__ = ['AuthCookieHandler']
+
+if '__main__' == __name__:
+ from paste.wsgilib import parse_querystring
+ def AuthStupidHandler(application):
+ def authstupid_application(environ, start_response):
+ args = dict(parse_querystring(environ))
+ user = args.get('user','')
+ if user:
+ environ['REMOTE_USER'] = user
+ environ['AUTH_TYPE'] = 'stupid'
+ test = args.get('test','')
+ if test:
+ environ['paste.auth.cookie.test'] = test
+ environ['paste.auth.cookie'].append('paste.auth.cookie.test')
+ return application(environ, start_response)
+ return authstupid_application
+ from paste.wsgilib import dump_environ
+ from paste.util.baseserver import serve
+ from paste.httpexceptions import *
+ serve(AuthCookieHandler(
+ HTTPExceptionHandler(
+ AuthStupidHandler(dump_environ))))
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)))
diff --git a/paste/auth/form.py b/paste/auth/form.py
new file mode 100644
index 0000000..b53952f
--- /dev/null
+++ b/paste/auth/form.py
@@ -0,0 +1,73 @@
+# (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 Form Authentication
+
+"""
+from paste.wsgilib import parse_formvars, construct_url
+
+template = """\
+<html>
+ <head><title>Please Login</title></head>
+ <body>
+ <h1>Please Login</h1>
+ <form action="%s" method="post">
+ <dl>
+ <dt>Username:</dt>
+ <dd><input type="text" name="username"></dd>
+ <dt>Password:</dt>
+ <dd><input type="password" name="password"></dd>
+ </dl>
+ <input type="submit" name="authform" />
+ <hr />
+ </form>
+ </body>
+</html>
+"""
+
+def AuthFormHandler(application, userfunc, login_page = None):
+ """ This causes a HTML form to be returned if REMOTE_USER has not
+ been provided. This is a really simple implementation, it
+ requires that the query arguments returned from the form have two
+ variables "username" and "password". These are then passed to
+ the userfunc; which should return True if authentication is granted.
+ """
+ login_page = login_page or template
+ def form_application(environ, start_response):
+ username = environ.get('REMOTE_USER','')
+ if username:
+ return application(environ, start_response)
+ if 'POST' == environ['REQUEST_METHOD']:
+ formvars = parse_formvars(environ)
+ username = formvars.get('username')
+ password = formvars.get('password')
+ if username and password:
+ if userfunc(username,password):
+ environ['AUTH_TYPE'] = 'form'
+ environ['REMOTE_USER'] = username
+ environ['REQUEST_METHOD'] = 'GET'
+ del environ['paste.parsed_formvars']
+ return application(environ, start_response)
+ start_response("200 OK",(('Content-Type', 'text/html'),
+ ('Content-Length', len(login_page))))
+ if "%s" in login_page:
+ return [login_page % construct_url(environ) ]
+ return [login_page]
+ return form_application
+
+middleware = AuthFormHandler
+
+__all__ = ['AuthFormHandler']
+
+if '__main__' == __name__:
+ def userfunc(username, password):
+ return username == password
+ from paste.wsgilib import dump_environ
+ from paste.util.baseserver import serve
+ from paste.httpexceptions import *
+ from cookie import AuthCookieHandler
+ serve(HTTPExceptionHandler(
+ AuthCookieHandler(
+ AuthFormHandler(dump_environ, userfunc))))
diff --git a/paste/auth/multi.py b/paste/auth/multi.py
new file mode 100644
index 0000000..1b593ae
--- /dev/null
+++ b/paste/auth/multi.py
@@ -0,0 +1,81 @@
+# (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
+"""
+Multi Authentication
+
+In some environments, the choice of authentication method to be used
+depends upon the environment and is not "fixed". This middleware
+allows N authentication methods to be registered along with a goodness
+function which determines which method should be used.
+
+Strictly speaking this is not limited to authentication, but it is a
+common requirement in that domain; this is why it isn't named
+AuthMultiHandler (for now).
+"""
+
+class MultiHandler:
+ """ This middleware provides two othogonal facilities:
+ (a) a way to register any number of middlewares
+ (b) a way to register predicates which cause one of
+ the registered middlewares to be used
+ If none of the predicates returns True, then the
+ application is invoked directly without middleware
+ """
+ def __init__(self, application):
+ self.application = application
+ self.default = application
+ self.binding = {}
+ self.predicate = []
+ def add_method(self, name, factory, *args, **kwargs):
+ self.binding[name] = factory(self.application, *args, **kwargs)
+ def add_predicate(self, name, checker):
+ self.predicate.append((checker,self.binding[name]))
+ def set_default(self, name):
+ """
+ This method sets the default middleware to be executed,
+ if none of the rules apply.
+ """
+ self.default = self.binding[name]
+ def set_query_argument(self, name, key = '*authmeth', value = None):
+ """
+ This method indicates that the named middleware component should
+ be executed if the given key/value pair occurs in the query args.
+ """
+ lookfor = "%s=%s" % (key, value or name)
+ self.add_predicate(name,
+ lambda environ: lookfor in environ.get('QUERY_STRING',''))
+ def __call__(self, environ, start_response):
+ for (checker,binding) in self.predicate:
+ if checker(environ):
+ return binding(environ, start_response)
+ return self.default(environ, start_response)
+
+middleware = MultiHandler
+
+__all__ = ['MultiHandler']
+
+if '__main__' == __name__:
+ import basic, digest, cas, cookie, form
+ from paste.httpexceptions import *
+ from paste.wsgilib import dump_environ
+ from paste.util.baseserver import serve
+ multi = MultiHandler(dump_environ)
+ multi.add_method('basic',basic.middleware,
+ 'tag:clarkevans.com,2005:basic',
+ lambda n,p: n == p )
+ multi.set_query_argument('basic')
+ multi.add_method('digest',digest.middleware,
+ 'tag:clarkevans.com,2005:digest',
+ lambda r,u: digest.digest_password(u,r,u))
+ multi.set_query_argument('digest')
+ multi.add_method('form',lambda ap: cookie.middleware(
+ form.middleware(ap,
+ lambda n,p: n == p)))
+ multi.set_query_argument('form')
+ #authority = "https://secure.its.yale.edu/cas/servlet/"
+ #multi.add_method('cas',lambda ap: cookie.middleware(
+ # cas.middleware(ap,authority)))
+ #multi.set_default('cas')
+ serve(HTTPExceptionHandler(multi))