[Twisted-Python] htpasswd / HTTP basic auth example

Andrew Bennetts andrew-twisted at puzzling.org
Sun Jul 20 21:27:16 EDT 2003

On Sun, Jul 20, 2003 at 08:39:27PM -0000, Moshe Zadka wrote:
> As many of you know, using guard (the webby interface to authentication)
> has been a thorny issue with Twisted. Christopher Armstrong and I wrote
> a module which hides many of the details of using guard, and supplies
> a much more usable interface.

This reminds me -- I wrote a simple resource wrapper recently that provides
HTTP basic auth, reading from a htpasswd(1) file [currently it only supports
crypt'ed passwords, not MD5].  This doesn't use guard (or even newcred) at
all, although it probably should.

I keep meaning to post it to the list; so here it is!

---- htpasswdauth.py ----

from crypt import crypt

from twisted.web import static
from twisted.web.resource import Resource
from twisted.protocols import http

__all__ = ['HtPasswdWrapper']

class UnauthorizedResource(Resource):
    isLeaf = 1
    def __init__(self, realm, errorPage):
        self.realm = realm
        self.errorPage = errorPage

    def render(self, request):
        # FIXME: Does realm need to be quoted?
        request.setHeader('WWW-authenticate', 'basic realm="%s"' % self.realm)
        return self.errorPage.render(request)

class HtPasswdWrapper(Resource):
    """Apache-style htpasswd protection for a resource.

    Requires a client to authenticate (using HTTP basic auth) to access a
    resource.  If they fail to authenticate, or their username and password
    aren't accepted, they receive an error page.

    The username and password are checked against a htpasswd(1) file using
    crypt.  The file is re-read for every request.

        - Integrate this into twisted.web.woven.guard / newcred?
        - Support MD5 password hashes in the htpasswd file, as well as crypt.

    @cvar unauthorizedPage: L{Resource} that will be used to render the error
        page given when a user is unauthorized.

    unauthorizedPage = static.Data(
        '<html><body>Access Denied.</body></html>', 'text/html'

    def __init__(self, resource, htpasswdFilename, realm):
        @param resource: resource to protect with authentication.
        @param htpasswdFilename: filename of an htpasswd file to authenticate
            with.  Currently only crypt(3)-format passwords are supported.
        @param realm: HTTP auth realm.
        self.resource = resource
        self.filename = htpasswdFilename
        self.realm = realm

    def getChildWithDefault(self, path, request):
        if self.authenticateUser(request):
            return self.resource.getChildWithDefault(path, request)
            return self.unauthorized()

    def render(self, request):
        if self.authenticateUser(request):
            return self.resource.render(request)
            return self.unauthorized().render(request)

    def authenticateUser(self, request):
        username, password = request.getUser(), request.getPassword()
        lines = [l.rstrip().split(':', 1) for l in file(self.filename).readlines()]
        lines = [l for l in lines if l[0] == username]
        if not lines:
            return 0
        hashedPassword = lines[0][1]
        return hashedPassword == crypt(password, hashedPassword[:2])

    def unauthorized(self):
        return UnauthorizedResource(self.realm, self.unauthorizedPage)

if __name__ == '__main__':
    # Quick & dirty testing...
    # create the intarweb
    from twisted.web.server import Site
    root = Resource()
    sit = Site(HtPasswdWrapper(root, '/tmp/htpasswdtest', 'test site'))
    #sit = Site(root)

    root.putChild('', static.Data('If you can see this, you are authorized!  Congrats!', 'text/plain'))
    root.putChild('blah', static.Data('Bring me a child!!', 'text/plain'))

    # and finally talk to the internat
    from twisted.internet import reactor
    reactor.listenTCP(18080, sit)

---- end ----


More information about the Twisted-Python mailing list