For my third year project, I have hooked Twisted's
Perspective Broker authentication into Django, so that a networked
application can authenticate itself against Django's central user database
(django.contrib.auth
). The process is pretty quick and easy, thanks to
Twisted's pluggable nature, and Django's pure simplicity.
Firstly, we create a class which will actually do the "checking", to allow access (or not) given a username/password combination. That looks something like this:
from zope.interface import implements
from twisted.python import failure, log
from twisted.cred import portal, checkers, error, credentials
from twisted.internet import defer
from django.contrib.auth.models import User, check_password
class DjangoAuthChecker:
implements(checkers.ICredentialsChecker)
credentialInterfaces = (credentials.IUsernamePassword,
credentials.IUsernameHashedPassword)
def _passwordMatch(self, matched, user):
if matched:
return user
else:
return failure.Failure(error.UnauthorizedLogin())
def requestAvatarId(self, credentials):
try:
user = User.objects.get(username=credentials.username)
return defer.maybeDeferred(
check_password,
credentials.password,
user.password).addCallback(self._passwordMatch, user)
except User.DoesNotExist:
return defer.fail(error.UnauthorizedLogin())
This code is relatively self-explanatory, once you understand Twisted's way of
doing things. An authenticated "user" in Twisted (though this might not
actually be a person) is called an Avatar
. The requestAvatarId
method
therefore takes a set of credentials, and returns the Django user object that
corresponds to those credentials (or a failure if necessary). This return value
is wrapped up inside a Deferred, which is something that I won't go into detail
about in this post. For more information on Deferred objects, this is one area
where the Twisted documentation is quite detailed.
Edit: Fixed a bug in the above code on line 24, thanks to Tom Leys of GridSpy for pointing it out.
The next thing we need to do is create a "realm", which will essentially convert the Django user object into an "avatar" - which is the object that the server will use to communicate with the client:
class MyRealm(object):
implements(portal.IRealm)
def requestAvatar(self, user, mind, *interfaces):
assert pb.IPerspective in interfaces
avatar = MyAvatar(user)
avatar.attached(mind)
return pb.IPerspective, avatar, lambda a=avatar:a.detached(mind)
class MyAvatar(pb.Avatar):
def __init__(self, user):
self.user = user
def attached(self, mind):
self.remote = mind
print 'User %s connected' % (self.user,)
def detached(self, mind):
self.remote = None
print 'User %s disconnected' % (self.user,)
As you can probably tell, the realm has no responsibility for authentication. It simply accepts a user object (usually a username in normal Twisted apps), and returns an avatar. Here, I've written a very simple avatar that will print a message when each user connects and disconnects. The "mind" object allows the server to call remote methods on the client, and is documented (though not in great detail) in the Twisted Perspective Broker Authentication documentation.
That's pretty much it for the authentication code. All that's left now is to actually link this into a Perspective Broker:
checker = DjangoAuthChecker()
realm = MyRealm()
p = portal.Portal(realm, [checker])
reactor.listenTCP(self.port, PBServerFactory(p))
reactor.run()
To login to a Perspective Broker with authentication enabled is, you'll be glad to know, very simple:
factory = pb.PBClientFactory()
reactor.connectTCP('127.0.0.1', 8123, factory)
d = factory.login(
cred.credentials.UsernamePassword('username', 'password'),
client=self)
d.addCallback(connected)
d.addErrback(error)
reactor.run()
Hopefully it's obvious what is going on here. Once we "login" to the factory
providing the connection, a pair of callbacks are added to the deferred - the
connected
function will be called if we are successfully logged in, and the
error
function will be called if there is some kind of error (like an
UnauthorizedLogin
, for example). The success callback will be given the
perspective as the only argument, so it can begin to call methods on the server
in the usual way.
If you've solved this particular puzzle in some other way, or if it's just been of use - then I'd love to hear from you. In any case, this is an excellent example of how simple integration can be when you're using well documented open source systems. Cheers!