2011-03-14

OAuth, Python and Basic LTI

On a recent long flight I was working on a Python script to act as a bridge between an IMS Basic LTI consumer and Questionmark Perception motivated by a rash claim that this was achievable given a suitably long flight away from other distractions.

The first part of the job (undertaken at Heathrow's Terminal 3) was to download the tools I would need.  The moodle on my laptop was still on 1.9.4 so I needed to upgrade before I could install the Basic LTI module for Moodle 1.9 and 2.  Despite the size of the downloads the 3G reception is great at Heathrow.

Basic LTI uses OAuth to establish trust between the Tool Consumer (Moodle in my case) and the Tool Provider (my script) so I needed to get a library to jump start support for OAuth 1.0 in Python.  Consensus on the web seems to be that the best modules are available from the Google Code project called, simply, 'oauth'.  The python module listed there is straightforward to use, even without a copy of the OAuth specification to hand.

Of course, these things never go quite as smoothly as you would like (and I'm not just talking about turbulence over Northern Canada).  I put together my BLTI module and hooked it up to Moodle but there were two critical problems to solve before I could make it work.

Firstly, BLTI uses tokenless authentication and the Python module has no method for verifying the validity of a tokenless request.  As a result, I had to dive in a bit deeper than I'd hoped.  Instead of calling the intended method: oauth_server.verify_request(oauth_request) I'm having to unpick that method and make a low-level call instead: oauth_server._check_signature(oauth_request, consumer, None) - the leading underscore is a hint that I might get into trouble with future updates to the oauth module.

Once I'd overcome that problem, I was disappointed to find that my tool provider still failed with a checksum validation error.  The tool consumer in Moodle was signing a request in a way that my module was unable to reproduce.  The BLTI launch call can take quite a few extra parameters and all of these variables need to put into the hash.  It's not quite a needle in a haystack but I looked nervously at my remaining battery power and wondered if I'd find the culprit in time.

The problem turns out to be a small bug in the server example distributed with the python oauth module.  The problem relates to the way the URL has to be incorporated into the hash.  (Section 9.1.2 of the OAuth spec)  The example server assumes that the path used by the HTTP client will be the full URL.  In other words, they assume an HTTP request like this:

POST http://tool.example.com/bltiprovider/lms.example.com HTTP/1.1
Host: tool.example.com
....other headers follow

In the example code, the oauth request is constructed by a sub-class of BaseHTTPRequestHandler like this:

oauth_request = oauth.OAuthRequest.from_request(self.command,
  self.path, headers=self.headers, query_string=postdata)


When I was testing with Moodle and Chrome my request was looking more like this:


POST /bltiprovider/lms.example.com HTTP/1.1
Host: tool.example.com

This resulted in a URL of "///bltiprovider/lms.example.com" being added to the hash.  Once the problem is identified it is fairly straight forward to use the urlparse module to identify the shorter form of request and recombine the host header and scheme to make the canonical URL.  I guess a real application is unlikely to use BaseHTTPRequestHandler so this probably isn't a big deal but I thought I'd blog the issue anyway because I was pleased that I found and fixed it before I had to sleep my MacBook.

3 comments:

  1. Hello I want to know if you could solve your problems. I want to integrate OSQA (based on django) with Basic LTI

    Thanks in advance!

    ReplyDelete
  2. Yes - I did get the Basic LTI demo working. The Python code to wrap the OAuth library is here:

    http://code.google.com/p/qtimigration/source/browse/trunk/pyslet/pyslet/imsbltiv1p0.py

    The idea is that you sub-class BLTIToolProvider which includes a few methods to help you manage a list of tool consumers and also an all important Launch method which you can use for checking an incoming HTTP request. (This is demoware rather than production code.)

    My code goes something like this:

    import pyslet.imsbltiv1p0 as blti

    tp=blti.BLTIToolProvider()
    # Load keys from simple list of key/secret pairs
    tp.LoadFromFile(open('consumers.txt'))

    # And then in an LTI-protected handler derived from BaseHTTPRequestHandler...

    consumer,params=tp.Launch(self.command, url, headers=self.headers, query_string=postdata)

    The tricky bit was to realize that url must include the scheme and 'authority' part of the URL, not just the path... something like this worked for me. (Wish I knew how to auto detect https in the handler.)

    parts=urlparse.urlsplit(self.path)
    if not parts.scheme:
    scheme='http'
    if not parts.netloc:
    netloc=self.headers['Host']
    else:
    netloc=parts.netloc
    url=urlparse.urlunsplit([scheme,netloc]+list(parts[2:]))
    else:
    url=self.path

    Hope that helps.

    ReplyDelete