diff options
| author | ianb <devnull@localhost> | 2005-12-13 07:00:20 +0000 |
|---|---|---|
| committer | ianb <devnull@localhost> | 2005-12-13 07:00:20 +0000 |
| commit | 4e73bff9da87e35c7154ab1cc923bb4f9d40711d (patch) | |
| tree | d2e4c92965398700457280d5829dfaa5cdf5b4fb /paste/auth/cookie.py | |
| parent | 55b404e53bc834daf3852069af6de9b1fca4c742 (diff) | |
| download | paste-4e73bff9da87e35c7154ab1cc923bb4f9d40711d.tar.gz | |
Merged changes from cce branch (r3727:HEAD/4008); the branch is now in sync with trunk
Diffstat (limited to 'paste/auth/cookie.py')
| -rw-r--r-- | paste/auth/cookie.py | 229 |
1 files changed, 229 insertions, 0 deletions
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)))) |
