A very basic description of authentication opportunities in WSGI¶
- Title:
Simple Authentication
- Author:
Ian Bicking <ianb@colorstudy.com>
- Discussions-To:
Python Web-SIG <web-sig@python.org>
- Status:
Proposed
- Created:
13-Nov-2006
Abstract¶
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.
Rationale¶
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.
Specification¶
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
REMOTE_USER
Respond with
401 Unauthorized
Note
Please do not confused 401 Unauthorized
with “permission
denied”. Permission denied should be indicated with 403
Forbidden
.
REMOTE_USER
:This should be the string username of the user, nothing more.
401 Unauthorized
:Because middleware is handling the authentication, additional information is not required. You do not (and should not) include a
WWW-Authenticate
header. The middleware may include that header, or may change the response in some other way to handle the login.
Example¶
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[0].lower() == name.lower():
headers.remove(header)
break
Problems¶
Strictly speaking, it is illegal to send a
401 Unauthorized
response without theWWW-Authenticate header
. If no middleware is installed, most browsers will treat it like a200 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.)
Other Possibilities¶
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.
Open Issues¶
See Problems.