?

Log in

Previous Entry | Next Entry

I'm writing this up here because I didn't find any detailed documentation on the web, and it's not very intuitive. The situation:

1. I have a web service that I'm delivering using the python tornado web server.
2. It needs to be authenticated.
3. The authentication backend I have to use is kerberos.

I am not a big fan of http basic authentication. In order for it to be at all safe, it has to be done over SSL, because it sends the password in the clear. Also, Kerberos isn't intended to be used this way--one of Kerberos' strengths is that it doesn't require you to send passwords to the agent you are authenticating with, meaning that your password can't be compromised simply because that agent has been compromised. Using Kerberos the way I'm using it in this example throws out that advantage, and I'm hoping to fix that at some point. But for now, I needed to get something working. Expedience is the mother of insecurity, or something.

So the way that I'm doing the authentication is that the user visits a URL that's password protected. The browser doesn't send any authentication header, because it doesn't know it has to. The tornado server needs to send back an HTTP 401 response, which says "authenticate, please." It also needs to send an auth header that specifies the authentication realm.

At this point the browser throws up a dialog box prompting the user for a username and password. They type in their kerberos username, and their kerberos password. The browser sends the username and password to the tornado server.

The tornado server now contacts the kerberos server and acquires a ticket for my service (we'll call it tor in the example code I'm going to provide). The tornado server then checks to see that the ticket actually works to authenticate to the tor service. If it does, the user's in; otherwise, tornado sends another 401 error.

In order to make this work, the first thing I needed was a kerberos instance for my service. The Kerberos principal name for a server instance generally looks like this:

service/hostname.example.com@EXAMPLE.COM

service is the name of the service. hostname.example.com is the name of the host on which the service is running. @EXAMPLE.COM is the kerberos realm (I always wanted a kerberos realm called STRAUMLI, but that's another story).

As I said, we're going to call the service tor and we'll call the hostname tornado.example.com. So we need to create a principal called tor/tornado.example.com@EXAMPLE.COM on the Kerberos master for the EXAMPLE.COM realm. Once this is created, we need to extract a keytab file containing the Kerberos key for tor/tornado.example.com@EXAMPLE.COM.

I'm not going to tell you how to do this, because I actually didn't do it myself—I asked one of our nice sysadmins to do it, and they just did it and gave me the keytab file. If you are in a situation where you need to do this, you probably have similar resources, so go use them. If you don't, it's not too painful, but I haven't done it myself in years, so you'll have to RTFM.

Beware: the keytab file is an actual password. If it gets out, an attacker could use it to spoof your service. So keep it safe—it should be owner-readable, with no other permission bits set.

The second step that's necessary here is to set up pykpass. Pykpass actually does the whole kerberos authentication process and authenticator validation process for you, so you don't need to think very hard—you just have to pass in the right information. You can find pykpass on the python.org web site, and presumably you know how to install python packages, right?

So here's the code:

#!/usr/bin/env python

from kpass import kpass, KpassError
import tornado.httpserver
import tornado.ioloop
import tornado.web
import base64

class torRequestHandler(tornado.web.RequestHandler):
    def get(self, line):
        auth_hdr = self.request.headers.get('Authorization')
        
        if auth_hdr == None:
            return self.request_basic_auth()
        if not auth_hdr.startswith('Basic '):
            return self.request_basic_auth()
        
        auth_decoded = base64.decodestring(auth_hdr[6:])
        username, password = auth_decoded.split(':', 2)

        try:
            if kpass(unicode(username + "@EXAMPLE"),
                     password, "tor", "tornado.example.com",
                     "FILE:/etc/tor.keytab") != 1:
                return self.request_basic_auth()
        except KpassError, diag:
            return self.request_basic_auth()

        # put the rest of your get handler here.
    
    def request_basic_auth(self):
        if self._headers_written: 
            raise Exception('headers have already been written')
           
        self.set_status(401)
        self.set_header('WWW-Authenticate','Basic realm="%s"' % "EXAMPLE.COM")
        self.finish()
            
        return False

urls = [        
    (r"/tor/(.*)", torRequestHandler),
]


application = tornado.web.Application(urls)

# Don't even *think* about not using SSL.
ssl_options = { "certfile": "/etc/torcert.crt",
                "keyfile": "/etc/torcert.key" };

if __name__ == "__main__":
    http_server = tornado.httpserver.HTTPServer(application,
                                                ssl_options=ssl_options)
    http_server.listen(8888)
    tornado.ioloop.IOLoop.instance().start()


As I stated earlier, I don't particularly recommend this as a long-term solution. If nothing else, putting the authentication where I've done is inconvenient. But if you need to do this for testing or prototyping purposes, hopefully this will be of some use to you.

By the way, I shamelessly copied some of the above code from an article on using python decorators to do authentication with tornado. The reason I didn't just use the example from that blog entry is that it doesn't actually work—it throws an exception because the authenticator gets called after the http response has been sent. I wasn't able to figure out why this was happening, and frankly I hate code that's complicated an opaque, which python decorators are, so I just got rid of the decorator and the interceptor and hard-coded my solution.