LTI Authentication

Last post I wrote a short tutorial on how to build an LTI app, with a YouTube search app as the example. The thing I left out of that example was signature validation. Validating the request's signature is how you can make sure that app launches are happening only from trusted sources, and how you can use LTI for automatically logging in app users. Authentication is important and if you're building a production-worthy LTI app you should definitely include this.


Graphology is the analysis of signatures and handwriting. It tells us that John Hancock had trouble sleeping and liked ponies.

LTI authentication is based on OAuth 1.0's signature validation. LTI is primarily a one-way identity assertion, where the learning platform tells the app details about the current user's identity and context. The learning platform and the app are responsible to come up with a consumer key and shared secret out-of-band (more on that in a minute). The attributes come across as form POST parameters, and the signature is sent across as an additional parameter.

Side note: Wherever possible you should serve your app over https instead of http. Some learning platforms like Canvas render all pages over SSL and users will get security warnings when trying to launch non-SSL apps.

The signature is generated, as with OAuth 1.0, by sorting all the request parameters, adding the consumer key, a timestamp and a nonce, and hashing them together using the shared secret. There are either LTI or OAuth libraries for many common languages, so hopefully you don't have to write this code yourself, but if you do here's a handy page for checking your signature generation. In practice your code will look something like the following (read my last post if this doesn't look familiar):

# Handle POST requests to the endpoint "/lti_launch"
post "/lti_launch" do
  config = find_config(params['oauth_consumer_key'])
  if !config
    return "Invalid launch - no configuration found for that key"
  end
  provider = IMS::LTI::ToolProvider.new(config.key, config.secret, params)
  if !provider.valid_request?(request)
    return "Invalid launch - signature does not match"
  end
  
  # do magical YouTube-y stuff 
end

Side note: there are a few (optional) cases where the app may also pass messages back to the learning platform (grade passback, for example). In these cases the app will use the same signing process as the learning platform.

User data comes across as POST parameters. Some parameters are required, many are optional and can be configured within the learning platform. Here's a list of the expected parameters, though additional custom parameters can also be sent. If your app has its own login system you can use this data to match users up with their existing logins, or to auto-create a user account in your system matching the data provided (some tools ask new users to create a login on first launch before continuing, I encourage people to make that an optional step after-the-fact to make things easier on the end-user). Our YouTube app doesn't care about identity at all, so we can ignore identity for now.

The only trick left is generating a consumer key and shared secret, and sharing them between the app and the learning platform. In practice, the app typically generates the key and secret, and it's the end-user's responsibility to copy the key and secret from a page within the app and paste them into a form within the learning platform. On the learning platformside the app will be configured by an institution admin, a department admin, or possibly an individual instructor, and the same key will be used to launch the app for any learners or instructors within that unit. Sometimes the key is the user's login or some known identifier, but often it's a large unique string just like the shared secret.

Example of key and secret being provided by the app.

Example of pasting the key and secret within the learning platform's app configuration tool.

Remember: the key is fine to share publicly, but the secret should be protected, since anyone who knows the secret (and key) can impersonate the learning platform to the app. If you're going to do something like hashing the key to generate the secret, it's a very good idea to add some randomness.

Shared secrets should be kept secret. They should be kept safe.

For our YouTube app we'll just add a small command-line script that can be used to generate a new key and secret by hand:

config = generate_config()
print "== New Configuration =="
print "consumer key:  " + config.key
print "shared secret: " + config.secret

The reason you need authentication for LTI should be clear, it's how the app can verify that users are coming from a trusted source into the app, and can be considered an alternative to a username and passwordstyle login. Even for a simple YouTube app like our example, which completely ignores all user information, it's important because each app placement within the learning platform is going to end up persisting information to your datastore once a video is selected, and without signature validation it would be easy for a malicious party to launch and run your app millions of times and stuff your datastore with junk placements. There's a more elegant approach that gets around this issue using an extension to LTI that I built for Canvas, but I'll cover that in another post.

Sinatra app source code available here.

Comments

Wen said…
This comment has been removed by the author.

Popular Posts