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
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!