#!/usr/bin/env python """auth_basic.py: HTTP basic authentication for web.py (http://webpy.org) __version__ = "0.1" __license__ = "public domain" __author__ = "Leonardo Boiko (leoboiko@gmail.com)" The elementary way of using this module is to forbid access to a resource (URL) unless the user authenticates first. Sample code: auth_basic.users['username'] = 'password' class admin: def GET(self): if auth_basic.auth('Admin area'): web.render('admin') else: auth_basic.unauthorized() web.render('unauthorized') After you call `unauthorized()`, the user will be asked to provide username and password and try the request again. Whatever you output is considered to be diagnosis information. A more advanced scheme is to present a different view depending on whether the user is anonymous or authenticated. You can do this by tackling an extra token to the request URL --- say '/admin' --- when the user wants to be identified. Functions `auth_urls` and `if_auth` might be convenient if you like to save typing: auth_basic.users['username'] = 'password' urls = [ '/', 'index', ] + auth_basic.auth_urls([ '/list', 'list' ], suffix='admin') # adds ['/list/(admin)', 'list'] to urls class list: realm='Admin area' def auth_fail(self): auth_basic.unauthorized(list.realm) web.render('unauthorized') def auth_ok(self, admin): if admin: # do stuff for admins web.render('list') # `admin` is useful in the template def GET(self, want_admin=False): if_auth(want_admin, self.auth_ok, self.auth_fail, list.realm) On this example, web.py will call: - `list.auth_ok(False)` if the request was '/list' - `list.auth_ok(True)` if the request was '/list/admin' and auth is valid - `list.auth_fail()` if the request was '/list/admin' and auth is invalid """ import web from base64 import b64decode users = web.storage() users.__doc__ = """ A `storage` object mapping authorized users to passwords. """ def unauthorized(realm): """Set a 401 Unauthorized status with the given realm. This is the status code you should set to ask the user to try authentication. As per RFC 2616, the response entity (web.ctx.output, anything you output) will only be shown if the first authentication attemp fails, so you should use it only to include diagnostic information. """ web.ctx.status = '401 Unauthorized' web.header('WWW-Authenticate', 'Basic realm="%s"' % realm) def auth(realm, hdr=None): """Authenticate user. Return username if credentials are valid, False otherwise. `realm`: The authentication realm; a opaque string uniquely describing the resources for which the credentials are valid. `hdr`: The client HTTP Authorization header. Defaults to the current one as available in `web.ctx`, if any. """ if not hdr: if 'HTTP_AUTHORIZATION' in web.ctx.env.keys(): hdr = web.ctx.env['HTTP_AUTHORIZATION'] if hdr: if (hdr[0:5] != 'Basic'): web.badrequest() username, password = b64decode(hdr[6:].strip()).split(':') if (username in users.keys()) and (password == users[username]): return username return False def auth_urls(urls, suffix='admin'): """Helps mapping URLs accessible to both auth users and anonymous users. The idea is that you want to render templates differently depending on whether the user authenticated himself or not. `urls`: A list of URL regexpes and class mappings, like the web.run argument. `suffix`: A URL suffix to be appended to authenticated resources. Default to `admin`. For example, if you call auth_urls([ '/index', 'index', '/list/([0-9]+)', 'list' ]) return value will be: ['/index', index, '/index/+(admin)', index, '/list/([0-9]+)', list, '/list/([0-9]+)/+(admin)', list] Then you can define the classes like this: class index: def GET(self, want_admin=False): #... class list: def GET(self, item, want_admin=False): """ res = [] for i, v in enumerate(urls): if i % 2 == 0: s = '(' + suffix + ')' url = urls[i] cl = urls[i+1] if (url[len(url)-1] != '/'): s = '/+' + s res.append(url + s) res.append(cl) res.append(url) res.append(cl) return res def if_auth(want_auth, fn_ok, fn_fail, realm): """Convenience function for pages that can be seen authenticated or not. `want_auth`: Boolean indicating whether the user wants to be authenticated. Typically you'll get this from the request path. `fn_ok(auth)`: Function called if no error occurred. If the user was authenticated, its single argument `auth` is set to username, and if he's anonymous, to False. `fn_fail()`: Function called if the user requested authentication but failed to provide valid credentials. Typically you'll call unauthorized(realm) and render an error page here. `realm`: Authentication realm. """ if want_auth: if auth(realm): fn_ok(True) else: fn_fail() else: fn_ok(False)