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:

  1. Indicating when a request is authenticated, and by who
  2. Responding that authentication is necessary

There are already two conventions for this:

  1. Put the username in REMOTE_USER
  2. 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 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.)

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.