|Author:||Ian Bicking <firstname.lastname@example.org>|
|Discussions-To:||Python Web-SIG <email@example.com>|
This describes a simple pattern for implementing authentication in WSGI middleware. This does not propose any new features or environment keys; it only describes a baseline recommended practice.
Authentication is probably the most common detail that should be abstracted away from an application, as it is a concern most often bound to a deployment.
There are two components to authentication:
- Indicating when a request is authenticated, and by who
- Responding that authentication is necessary
There are already two conventions for this:
- Put the username in
- Respond with
Please do not confused
401 Unauthorized with “permission
denied”. Permission denied should be indicated with
- This should be the string username of the user, nothing more.
- Because middleware is handling the authentication, additional
information is not required. You do not (and should not) include
WWW-Authenticateheader. The middleware may include that header, or may change the response in some other way to handle the login.
The first example implements simple HTTP Basic authentication:
class HTTPBasic(object): def __init__(self, app, user_database, realm='Website'): self.app = app self.user_database = user_database self.realm = realm def __call__(self, environ, start_response): def repl_start_response(status, headers, exc_info=None): if status.startswith('401'): remove_header(headers, 'WWW-Authenticate') headers.append(('WWW-Authenticate', 'Basic realm="%s"' % self.realm)) return start_response(status, headers) auth = environ.get('HTTP_AUTHORIZATION') if auth: scheme, data = auth.split(None, 1) assert scheme.lower() == 'basic' username, password = data.decode('base64').split(':', 1) if self.user_database.get(username) != password: return self.bad_auth(environ, start_response) environ['REMOTE_USER'] = username del environ['HTTP_AUTHORIZATION'] return self.app(environ, repl_start_response) def bad_auth(self, environ, start_response): body = 'Please authenticate' headers = [ ('content-type', 'text/plain'), ('content-length', str(len(body))), ('WWW-Authenticate', 'Basic realm="%s"' % self.realm)] start_response('401 Unauthorized', headers) return [body] def remove_header(headers, name): for header in headers: if header.lower() == name.lower(): headers.remove(header) break
- Strictly speaking, it is illegal to send a
401 Unauthorizedresponse without the
WWW-Authenticate header. If no middleware is installed, most browsers will treat it like a
200 OK. There is also no way to detect if an appropriate middleware is installed.
- This doesn’t give any other information about the user. That information can go in other keys, but that is not addressed in this specification currently.
- Some login methods will redirect the user, and any POST request data will possibly be lost. (Note that a specification like A specification for how to process POST form requests helps address this problem.)
- While you can add to this specification, I think it’s the most logical and useful way to do authentication and better efforts can build on this base.