diff options
| author | cce <devnull@localhost> | 2005-12-31 06:57:42 +0000 |
|---|---|---|
| committer | cce <devnull@localhost> | 2005-12-31 06:57:42 +0000 |
| commit | 98f2eef2897f1670bf5540e6199724aaf56ea5b4 (patch) | |
| tree | 9543b1dad43f0772829ed8a2748ca6b2f8f6530e /paste/auth | |
| parent | 59ba5ee40b7da9f406a29f558ec21878dbf291b3 (diff) | |
| download | paste-98f2eef2897f1670bf5540e6199724aaf56ea5b4.tar.gz | |
- cleaned up documentation for paste.auth.cookie
- cleaned up documentation for paste.auth.form
- cleaned up documentation for paste.auth.basic
- converted InternalServerError into its own class (httpexceptions)
- converted BadRequest into its own class (httpexceptions)
- a few minor cleanups
Diffstat (limited to 'paste/auth')
| -rw-r--r-- | paste/auth/__init__.py | 4 | ||||
| -rw-r--r-- | paste/auth/basic.py | 92 | ||||
| -rw-r--r-- | paste/auth/cas.py | 19 | ||||
| -rw-r--r-- | paste/auth/cookie.py | 179 | ||||
| -rw-r--r-- | paste/auth/form.py | 115 | ||||
| -rw-r--r-- | paste/auth/open_id.py | 2 |
6 files changed, 285 insertions, 126 deletions
diff --git a/paste/auth/__init__.py b/paste/auth/__init__.py index 76835b0..ad914d6 100644 --- a/paste/auth/__init__.py +++ b/paste/auth/__init__.py @@ -1,3 +1,7 @@ """ Package for authentication/identification of requests. + +The objective of this package is to provide single-focused middleware +components that implement a particular specification. Integration of +the components into a usable system is up to a higher-level framework. """ diff --git a/paste/auth/basic.py b/paste/auth/basic.py index 39764a9..b3e8f36 100644 --- a/paste/auth/basic.py +++ b/paste/auth/basic.py @@ -3,21 +3,34 @@ # the MIT License: http://www.opensource.org/licenses/mit-license.php # This code was written with funding by http://prometheusresearch.com """ -Basic Authentication +Basic HTTP/1.0 Authentication +This module implements ``Basic`` authentication as described in HTTP/1.0 +specification [1]_ . Do not use this module unless you need to work +with very out-dated clients, instead use ``digest`` authentication. +Basically, you just put this module before your application, and it +takes care of requesting and handling authentication requests. + +>>> from paste.wsgilib import dump_environ +>>> from paste.util.httpserver import serve +>>> realm = 'Test Realm' +>>> def authfunc(username, password): +... return username == password +>>> serve(AuthBasicHandler(dump_environ, realm, authfunc)) +serving on... + +.. [1] http://www.w3.org/Protocols/HTTP/1.0/draft-ietf-http-spec.html#BasicAA """ 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 - """ + +class AuthBasicAuthenticator: + """ + implements ``Basic`` authentication details + """ + type = 'basic' + def __init__(self, realm, authfunc): self.realm = realm - self.userfunc = userfunc + self.authfunc = authfunc def build_authentication(self): head = [('WWW-Authenticate','Basic realm="%s"' % self.realm)] @@ -31,37 +44,60 @@ class BasicAuthenticator: return self.build_authentication() auth = auth.strip().decode('base64') username, password = auth.split(':') - if self.userfunc(username, password): + if self.authfunc(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): +class AuthBasicHandler: + """ + HTTP/1.0 ``Basic`` authentication middleware + + 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 mandatory user-defined function which takes a + ``username`` and ``password`` for its first and second + arguments respectively. It should return ``True`` if + the user is authenticated. + + """ + def __init__(self, application, realm, authfunc): + self.application = application + self.authenticate = AuthBasicAuthenticator(realm, authfunc) + + def __call__(self, environ, start_response): username = environ.get('REMOTE_USER','') if not username: authorization = environ.get('HTTP_AUTHORIZATION','') - result = authenticator(authorization) - if isinstance(result,str): + result = self.authenticate(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 + return self.application(environ, start_response) 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.httpserver import serve - from paste.httpexceptions import * - serve(HTTPExceptionHandler( - AuthBasicHandler(dump_environ, realm, userfunc))) + +if "__main__" == __name__: + import doctest + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/paste/auth/cas.py b/paste/auth/cas.py index 613044e..92c7996 100644 --- a/paste/auth/cas.py +++ b/paste/auth/cas.py @@ -3,7 +3,7 @@ # 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 +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 @@ -30,8 +30,9 @@ class CASAuthenticate(HTTPSeeOther): def AuthCASHandler(application, authority): """ - This middleware implements CAS 1.0 Authentication There are several - possible outcomes: + middleware to implement 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 @@ -48,10 +49,14 @@ def AuthCASHandler(application, authority): 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. + Parameters: + + ``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): diff --git a/paste/auth/cookie.py b/paste/auth/cookie.py index 1282cfb..b2f93f0 100644 --- a/paste/auth/cookie.py +++ b/paste/auth/cookie.py @@ -5,30 +5,44 @@ """ 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 +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. It is recommended via the -HMAC specification (RFC 2104) that the secret key be 64 bytes since -this is the block size of the hashing. +Following is a very simple example where a form is presented asking +for a user name (no actual checking), and dummy session identifier +(perhaps corresponding to a database session id) is stored in the +cookie. + +>>> from paste.util.httpserver import serve +>>> from paste.fileapp import DataApp +>>> from paste.httpexceptions import * +>>> from paste.wsgilib import parse_querystring +>>> def testapp(environ, start_response): +... user = dict(parse_querystring(environ)).get('user','') +... if user: +... environ['REMOTE_USER'] = user +... environ['MY_SESSION'] = '1234' # save this too +... environ['paste.auth.cookie'].append('MY_SESSION') +... if environ.get('REMOTE_USER'): +... page = '<html><body>Welcome %s (%s)</body></html>' +... page %= (environ['REMOTE_USER'], environ['MY_SESSION']) +... else: +... page = ('<html><body><form><input name="user" />' +... '<input type="submit" /></form></body></html>') +... return DataApp(page, content_type="text/html")( +... environ, start_response) +>>> serve(AuthCookieHandler(testapp)) +serving on... + """ + import sha, hmac, base64, random, time, string, warnings from paste.request 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(hmac.new('x','x',sha).digest()) _header_size = _signature_size + len(make_time(time.time())) @@ -52,10 +66,12 @@ def new_secret(): """ returns a 64 byte secret """ return ''.join(random.sample(_all_chars,64)) -class CookieSigner: +class AuthCookieSigner: """ + save/restore ``environ`` entries via digially signed cookie + This class converts content into a timed and digitally signed - cookie, as well as having the facility to reverse this procedure. + 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. @@ -65,6 +81,36 @@ class CookieSigner: 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. + + Constructor Arguments: + + ``secret`` + + This is a secret key if you want to syncronize your keys so + that the cookie will be good across a cluster of computers. + It is recommended via the HMAC specification (RFC 2104) that + the secret key be 64 bytes since this is the block size of + the hashing. If you do not provide a secret key, a random + one is generated each time you create the handler; this + should be sufficient for most cases. + + ``timeout`` + + This is the time (in minutes) from which the cookie is set + to expire. Note that on each request a new (replacement) + cookie is sent, hence this is effectively a session timeout + parameter for your entire cluster. If you do not provide a + timeout, it is set at 30 minutes. + + ``maxlen`` + + This is the maximum size of the *signed* cookie; hence the + actual content signed will be somewhat less. If the cookie + goes over this size, a ``CookieTooLarge`` exception is + raised so that unexpected handling of cookies on the client + side are avoided. By default this is set at 4k (4096 bytes), + which is the standard cookie size limit. + """ def __init__(self, secret = None, timeout = None, maxlen = None): self.timeout = timeout or 30 @@ -86,7 +132,7 @@ class CookieSigner: return cookie def auth(self,cookie): - """ + """ Authenticate the cooke using the signature, verify that it has not expired; and return the cookie's content """ @@ -111,17 +157,12 @@ class CookieSigner: 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 + a list of environment keys to be saved via cookie + An instance of this object, found at ``environ['paste.auth.cookie']`` + lists the `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. """ def __init__(self, handler, scanlist): list.__init__(self, scanlist) @@ -133,7 +174,9 @@ class AuthCookieEnviron(list): class AuthCookieHandler: """ - This middleware uses cookies to stash-away a previously authenticated + the actual handler that should be put in your middleware stack + + 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 @@ -144,23 +187,55 @@ class AuthCookieHandler: `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['paste.auth.cookie'].append('your.environ.variable')`` + + + Constructor Arguments: + + ``application`` + + This is the wrapped application which will have access to + the ``environ['REMOTE_USER']`` restored by this middleware. + + ``cookie_name`` + + The name of the cookie used to store this content, by default + it is ``PASTE_AUTH_COOKIE``. + + ``scanlist`` + + This is the initial set of ``environ`` keys to save/restore + to the signed cookie. By default is consists only of + ``REMOTE_USER``; any tuple or list of environment keys + will work. However, be careful, as the total saved size is + limited to around 3k. + + ``signer`` + + This is the signer object used to create the actual cookie + values, by default, it is ``AuthCookieSigner`` and is passed + the remaining arguments to this function: ``secret``, + ``timeout``, and ``maxlen``. + + At this time, each cookie is individually signed. To store more + than the 4k of data; it is possible to sub-class this object to + provide different ``environ_name`` and ``cookie_name`` """ environ_name = 'paste.auth.cookie' - signer_class = CookieSigner + cookie_name = 'PASTE_AUTH_COOKIE' + signer_class = AuthCookieSigner environ_class = AuthCookieEnviron - def __init__(self, application, cookie_name=None, secret=None, - timeout=None, maxlen=None, signer=None, scanlist = None): + def __init__(self, application, cookie_name=None, scanlist=None, + signer=None, secret=None, timeout=None, maxlen=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' - + self.cookie_name = cookie_name or self.cookie_name + def __call__(self, environ, start_response): if self.environ_name in environ: raise AssertionError("AuthCookie already installed!") @@ -188,7 +263,7 @@ class AuthCookieHandler: def response_hook(status, response_headers, exc_info=None): """ - Scan the environment for keys specified in the scanlist, + 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) @@ -210,26 +285,10 @@ class AuthCookieHandler: 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.httpserver import serve - from paste.httpexceptions import * - serve(AuthCookieHandler( - HTTPExceptionHandler( - AuthStupidHandler(dump_environ)))) +__all__ = ['AuthCookieHandler', 'AuthCookieSigner', 'AuthCookieEnviron'] + + +if "__main__" == __name__: + import doctest + doctest.testmod(optionflags=doctest.ELLIPSIS) + diff --git a/paste/auth/form.py b/paste/auth/form.py index d32af2e..3dbe375 100644 --- a/paste/auth/form.py +++ b/paste/auth/form.py @@ -3,14 +3,31 @@ # the MIT License: http://www.opensource.org/licenses/mit-license.php # This code was written with funding by http://prometheusresearch.com """ -HTTP Form Authentication +Authentication via HTML Form + +This is a very simple HTML form login screen that asks for the username +and password. This middleware component requires that an authorization +function taking the name and passsword and that it be placed in your +application stack. This class does not include any session management +code or way to save the user's authorization; however, it is easy enough +to put ``paste.auth.cookie`` in your application stack. + +>>> from paste.wsgilib import dump_environ +>>> from paste.util.httpserver import serve +>>> from paste.auth.cookie import AuthCookieHandler +>>> from paste.auth.form import AuthFormHandler +>>> def authfunc(username, password): +... return username == password +>>> serve(AuthCookieHandler( +... AuthFormHandler(dump_environ, authfunc))) +serving on... """ -from paste.wsgilib import parse_formvars, construct_url +from paste.request import construct_url, parse_formvars -template = """\ +TEMPLATE ="""\ <html> - <head><title>Please Login</title></head> + <head><title>Please Login!</title></head> <body> <h1>Please Login</h1> <form action="%s" method="post"> @@ -27,47 +44,83 @@ template = """\ </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. +class AuthFormHandler: + """ + HTML-based login middleware + + This causes a HTML form to be returned if ``REMOTE_USER`` is + not found in the ``environ``. If the form is returned, the + ``username`` and ``password`` combination are given to a + user-supplied authentication function, ``authfunc``. If this + is successful, then application processing continues. + + 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. + + ``authfunc`` + + This is a mandatory user-defined function which takes a + ``username`` and ``password`` for its first and second + arguments respectively. It should return ``True`` if + the user is authenticated. + + ``template`` + + This is an optional (a default is provided) HTML + fragment that takes exactly one ``%s`` substution + argument; which *must* be used for the form's ``action`` + to ensure that this middleware component does not alter + the current path. The HTML form must use ``POST`` and + have two input names: ``username`` and ``password``. + + Since the authentication form is submitted (via ``POST``) + neither the ``PATH_INFO`` nor the ``QUERY_STRING`` are accessed, + and hence the current path remains _unaltered_ through the + entire authentication process. If authentication succeeds, the + ``REQUEST_METHOD`` is converted from a ``POST`` to a ``GET``, + so that a redirect is unnecessary (unlike most form auth + implementations) """ - login_page = login_page or template - def form_application(environ, start_response): + + def __init__(self, application, authfunc, template=None): + self.application = application + self.authfunc = authfunc + self.template = template or TEMPLATE + + def __call__(self, environ, start_response): username = environ.get('REMOTE_USER','') if username: - return application(environ, start_response) + return self.application(environ, start_response) + if 'POST' == environ['REQUEST_METHOD']: - formvars = parse_formvars(environ) + formvars = parse_formvars(environ, include_get_vars=False) username = formvars.get('username') password = formvars.get('password') if username and password: - if userfunc(username,password): + if self.authfunc(username,password): environ['AUTH_TYPE'] = 'form' environ['REMOTE_USER'] = username environ['REQUEST_METHOD'] = 'GET' + environ['CONTENT_LENGTH'] = '' + environ['CONTENT_TYPE'] = '' del environ['paste.parsed_formvars'] - return application(environ, start_response) + return self.application(environ, start_response) + + content = self.template % construct_url(environ) 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 + ('Content-Length', len(content)))) + return [content] middleware = AuthFormHandler __all__ = ['AuthFormHandler'] -if '__main__' == __name__: - def userfunc(username, password): - return username == password - from paste.wsgilib import dump_environ - from paste.util.httpserver import serve - from paste.httpexceptions import * - from cookie import AuthCookieHandler - serve(HTTPExceptionHandler( - AuthCookieHandler( - AuthFormHandler(dump_environ, userfunc)))) +if "__main__" == __name__: + import doctest + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/paste/auth/open_id.py b/paste/auth/open_id.py index 9eade11..ad3a665 100644 --- a/paste/auth/open_id.py +++ b/paste/auth/open_id.py @@ -53,6 +53,8 @@ should be redirected to wherever they would normally go after a successful login. """ +__all__ = ['AuthOpenIDHandler'] + import cgi import urlparse import cgitb |
