Adding user-authentication to your Twisted web-site (Another HowTo for undocumented features...)

Following along from the recent post describing how to make your Twisted web-server use SSL sockets, a slightly more involved HowTo describing how to setup HTTP authentication and Twisted's cred module. First, before you say anything, yes, this (using Nevow to guard regular twisted.web) is the "right" way according to the Nevow developers (Nevow is slated to become twisted.web2, the replacement for twisted.web). Apparently this just isn't a feature than anyone's been using, so it's not yet built in.
We're going to assume the following;
  • you have a twisted.web site
    • i.e. you've been following the Twisted web howto
  • you want to do form-based authentication
    • i.e. not a challenge-respose dialog, but a nicely formatted "please login" form in the main browser window
  • you're not using the Twisted "application" framework
    • since you're following along with the tutorial
  • you want to make the whole thing use SSL
    • since you're sending passwords via form values

The first thing we need is a login page. I'm going to shamelessly rip one out of the nevow sample code, and just strip down some of the unneeded features. We could have used any IResource-compatible web-page-producing mechanism for generating the page, the nevow code is just handy:

# loginform.py

from nevow import rend, tags, guard, loaders

class LoginForm(rend.Page):
"""Minimalist login page for Cinemon"""
addSlash = True
docFactory = loaders.stan(
tags.html[
tags.head[tags.title["Please log in to Cinemon"]],
tags.body[
tags.form(action=guard.LOGIN_AVATAR, method="post")[
tags.table[
tags.tr[
tags.td[ "Username:" ],
tags.td[ tags.input(type='text',name='username') ],
],
tags.tr[
tags.td[ "Password:" ],
tags.td[ tags.input(type='password',name='password') ],
]
],
tags.input(type='submit'),
]
]
])

def logout(*args, **named):
"""Null operation "logging out" the user"""

The next thing we need is a "Realm". A Realm is basically an object which, given an authenticated (which includes anonymous) user, produces an "agent" for that user. An agent is just a view of the application's resources which reflects the user's capabilities, an object with an IResource interface. That's wonderfully abstract, so let's get more concrete. Our agents are either going to be (for a user that hasn't logged in) a login form instance, or the actual web-site for those who have logged in.

Creating a Realm is fairly straightforward:

#realm.py
from twisted.cred import portal, checkers
from nevow import inevow
from cinemon.twistscan import loginform

class CinemonRealm(object):
"""Simplified model of a Realm

Constructed with a site, which must be an IResource-adaptable
object, the CinemonRealm produces a LoginForm for anyone who
isn't already logged in, or the site object for anyone who
has logged in.
"""
__implements__ = portal.IRealm
def __init__( self, site ):
"""Initialise, storing reference to our main site"""
self.site = site
def requestAvatar(self, avatarId, mind, *interfaces):
"""IRealm: Get the IResource appropriate for authenticated user

avaterId -- Can include checkers.ANONYMOUS, but what is it if
the user isn't anonymous?
mind -- ignored
interfaces -- Set of possible interfaces we could provide, we
need to provide at least one for any value we return
"""
for iface in interfaces:
if iface is inevow.IResource:
# do web stuff
if avatarId is checkers.ANONYMOUS:
resc = loginform.LoginForm()
resc.realm = self
return (inevow.IResource, resc, loginform.logout)
else:
resc = self.site
resc.realm = self
return (inevow.IResource, resc, loginform.logout)
raise NotImplementedError("Do not support any of the interfaces %s"%(interfaces,))

Okay, so now we have a Realm and a login page, what does it do for us? Well, actually, just about everything. The only significant piece missing from the authentication puzzle is that we need to wrap up our Realm with a twisted.cred credential checker into a twisted.cred "Portal". Since this is just for testing, we're going to use an in-memory password checker, which you shouldn't normally do:

#authorisation.py
from twisted.cred import portal, checkers, credentials
from nevow import inevow, appserver, guard
from cinemon.twistscan import loginform, realm

def wrapAuthorized( site ):
# force site to be nevow-compatible, using adapter for
# twisted.web sites...
site = inevow.IResource( site )
realmObject = realm.CinemonRealm( site )
portalObject = portal.Portal(realmObject)
myChecker = checkers.InMemoryUsernamePasswordDatabaseDontUse()
myChecker.addUser("user","password")
myChecker.addUser("fred", "flintstone")
# Allow anonymous access. Needed for access to loginform
portalObject.registerChecker(
checkers.AllowAnonymousAccess(), credentials.IAnonymous
)
# Allow users registered in the password file.
portalObject.registerChecker(myChecker)
site = appserver.NevowSite(
resource=guard.SessionWrapper(portalObject)
)
return site

Okay, so that code takes a "site", which is anything that can be adapted to a Nevow IResource, which includes bare twisted.web resources (there's an adapter in Nevow registered for them), and produces a NevowSite instance which has as it's resource a guard session wrapper around the portal, which wraps up our Realm object, which produces either the login page or the site we just passed in, depending on whether the user is logged in or not.

Only thing left to do is to actually run the code:

from twisted.application import internet
from twisted.web import resource
from twisted.internet import reactor, ssl
from cinemon.twistscan import authorisation

class Root( resource.Resource ):
"""Simplistic demo site for spike test"""
def getChild(self, name, request):
if name == '':
return self
return Resource.getChild( self, name, request )
def render_GET(self, request):
"""Render the root page for the demo"""
return """Hello World"""

if __name__ == "__main__":
def createAuthorized( doSSL = True, port=8080 ):
site = Root()
site = authorisation.wrapAuthorized( site )
if doSSL:
sslContext = ssl.DefaultOpenSSLContextFactory(
'/home/mcfletch/pylive/cinemon/privkey.pem',
'/home/mcfletch/pylive/cinemon/cacert.pem',
)
#serve = internet.TCPServer(
serve = internet.SSLServer(
port,
site,
contextFactory = sslContext
)
else:
serve = internet.TCPServer(
port,
site,
)
serve.startService()
reactor.callWhenRunning(
createAuthorized,
True, 8081
)
reactor.callWhenRunning(
createAuthorized,
False, 8080
)
reactor.run()

There you go, gentle reader, an encrypted, authenticated Twisted web-server.

Comments

Comments are closed.

Pingbacks

Pingbacks are closed.