diff options
| author | ianb <devnull@localhost> | 2005-04-22 03:18:20 +0000 |
|---|---|---|
| committer | ianb <devnull@localhost> | 2005-04-22 03:18:20 +0000 |
| commit | 36593a8d40cd1417d2927a78cdbe621090b6e8a5 (patch) | |
| tree | 74dc42b93bb512593869fd57f8be99c198d8c108 /paste/login.py | |
| parent | 7752a3102fc8aa68ee7e349495bc2e9345c77c00 (diff) | |
| download | paste-36593a8d40cd1417d2927a78cdbe621090b6e8a5.tar.gz | |
Renamed package itself
Diffstat (limited to 'paste/login.py')
| -rw-r--r-- | paste/login.py | 251 |
1 files changed, 251 insertions, 0 deletions
diff --git a/paste/login.py b/paste/login.py new file mode 100644 index 0000000..7e6fcac --- /dev/null +++ b/paste/login.py @@ -0,0 +1,251 @@ +""" +Login/authentication middleware + +NOT YET FINISHED +""" + +import wsgilib +import sha + +def middleware( + application, + http_login=False, + http_realm='Secure Website', + http_overwrite_realm=True, + http_and_cookie=True, + cookie_prefix='', + login_page='_login/login_form', + logout_page='_login/logout_form', + secret=None, + authenticator=None, + ): + """ + Configuration: + + http_login: + If true, then we'll prefer HTTP Basic logins, passing a 401 to + the user. If false, we'll use form logins with Cookie + authentication. + http_realm: + The realm to use. If http_overwrite_realm is true then we will + force this to be the realm (even if the application supplies + its own realm). + http_and_cookie: + If true, we'll give the user a login cookie even if they use + HTTP. Then we don't have to throw a 401 on every page to get + them to re-login. + cookie_prefix: + Used before all cookie names; like a domain. + login_page: + If using cookie login and we get a 401, we'll turn it into a + 200 and do an internal redirect to this page (using recursive). + logout_page: + Ditto the logout (logout will at some point be triggered with + another key we add to the environment). + secret: + We use this for signing cookies. We'll generate it automatically + if it's not provided explicitly (set it explicitly to be sure + it is stable). + authenticator: + When we do HTTP logins we need to tell if they are using the + correct login immediately. See the Authenticator object for + the framework of an implementation. + + When you require a login, return a 401 error. When a login has + occurred, the logged-in username will be in REMOTE_USER. When the + user is logged in, but denied access, use a 403 error (not a 401). + It might be useful to have another middleware that wraps an application + and returns a 401 error, based on parsing the URL. + + Currently, the login form, if used, is rendered at the URL requested + by the user, instead of issuing an HTTP redirect. This will require + some attention to caching issues, but allows forms to be POSTed without + losing data after the login (as long as the login page contains the + appropriate hidden fields.) + + Also, the cookie is not deleted on an unsuccessful login attempt. + + The cookie is issued with path '/' and no expiration date. This + should probably be overridable. + + Environment variables used: + paste.login.signer: + signer, created from UsernameSigner class + paste.login._dologin: + user name to be logged in, either from HTTP auth + or from form submission (XXX form not implement) + paste.login._doredirect: + login page to which to redirect + paste.login._loginredirect: + set to True iff _doredirect set and login_page is + relative, else undefined. Used where? + """ + + if http_login: + assert authenticator, ( + "You must provide an authenticator argument if you " + "are using http_login") + if secret is None: + secret = create_secret() + cookie_name = cookie_prefix + '_login_auth' + + signer = UsernameSigner(secret) + + def login_application(environ, start_response): + orig_script_name = environ['SCRIPT_NAME'] + orig_path_info = environ['PATH_INFO'] + cookies = wsgilib.get_cookies(environ) + cookie = cookies.get(cookie_name) + username = None + environ['paste.login.signer'] = signer + if cookie and cookie.value: + username = signer.check_signature( + cookie.value, environ['wsgi.errors']) + authenticatee = ( + environ.get('HTTP_AUTHORIZATION') or + environ.get('HTTP_CGI_AUTHORIZATION')) + if (not username + and authenticator + and authenticatee): + username = authenticator().check_basic_auth(authenticatee) + if http_and_cookie: + environ['paste.login._dologin'] = username + if username: + environ['REMOTE_USER'] = username + + def login_start_response(status, headers): + if environ.get('paste.login._dologin'): + cookie = SimpleCookie(cookie_name, + signer.make_signature(username), + '/') + headers.append(('Set-Cookie', str(cookie))) + del environ['paste.login._dologin'] + status_int = int(status.split(None, 1)[0].strip()) + if status_int == 401 and http_login: + if (http_overwrite_realm + or not wsgilib.has_header(headers, 'www-authenticate')): + headers.append(('WWW-Authenticate', 'Basic realm="%s"' % http_realm)) + elif status_int == 401: + status = '200 OK' + if login_page.startswith('/'): + assert environ.has_key('paste.recursive.include'), ( + "You must use the recursive middleware to " + "use a non-relative page for the login_page") + environ['paste.login._doredirect'] = login_page + return garbage_writer + return start_response(status, headers) + + app_iter = application(environ, login_start_response) + + if environ.get('paste.login._doredirect'): + page_name = environ['paste.login._doredirect'] + del environ['paste.login._doredirect'] + eat_app_iter(app_iter) + if login_page.startswith('/'): + app_iter = environ['paste.recursive.forward']( + login_page[1:]) + else: + # Don't use recursive, since login page is + # internal to + new_environ = environ.copy() + new_environ['SCRIPT_NAME'] = orig_script_name + new_environ['PATH_INFO'] = '/' + login_page + new_environ['paste.login._loginredirect'] = True + app_iter = login_application(new_environ, start_response) + return app_iter + + return login_application + + +def encodestrip (s): + return s.encode('base64').strip ('\n') + +class UsernameSigner(object): + + def __init__(self, secret): + self.secret = secret + def digest (self, username): + return sha.new(self.secret+username).digest() + + def __call__(self, username): + return encodestrip (self.digest(username)) + + def check_signature(self, b64value, errors): + value = b64value.decode ('base64') + if ' ' not in value: + errors.write('Badly formatted cookie: %r\n' % value) + return None + signature, username = value.split(' ', 1) + sig_hash = self.digest(username) + if sig_hash == signature: + return username + errors.write('Bad signature: %r\n' % value) + return None + + def make_signature (self, username): + return encodestrip (self.digest(username) + " " + username) + + def login_user(self, username, environ): + """ + Adds a username so that the login middleware will later set + the user to be logged in (with a cookie). + """ + environ['paste.login._dologin'] = username + +class SimpleCookie(object): + def __init__ (self, cookie_name, signed_val, path): + self.cookie_name = cookie_name + self.signed_val = signed_val + self.path = '/' + def __str__ (self): + return "%s=%s; Path=%s" % (self.cookie_name, + self.signed_val, self.path) + +class Authenticator(object): + + """ + This is the basic framework for an authenticating object. + """ + + def check_basic_auth(self, auth): + """Returns either the authenticated username or, if unauthorized, + None.""" + assert auth.lower().startswith('basic ') + type, auth = auth.split() + auth = auth.strip().decode('base64') + username, password = auth.split(':') + if self.check_auth (username, password): + return username + return None + + def check_auth(self, username, password): + raise NotImplementedError + + +######################################## +## Utility functions +######################################## + +def create_secret(): + # @@: obviously not a good secret generator: should be randomized + # somehow, and maybe store the secret somewhere for later use. + return 'secret' + +def garbage_writer(s): + """ + When we don't care about the written output. + """ + pass + +def eat_app_iter(app_iter): + """ + When we don't care about the iterated output. + """ + try: + for s in app_iter: + pass + finally: + if hasattr(app_iter, 'close'): + app_iter.close() + + |
