3.4. IMS Basic Learning Tools Interoperability (version 1.0)

The IMS Basic Learning Tools Interoperability (BLTI) specification was released in 2010. The purpose of the specification is to provide a link between tool consumers (such as Learning Management Systems and portals) and Tools (such as specialist assessment management systems). Official information about the specification is available from the IMS GLC under the general name LTI:

This module implements the Basic LTI specification documented in the Best Practice Guide

This module requires the oauthlib module to be installed. The oauthlib module is available from PyPi.

This module is written from the point of view of the Tool Provider. There are a number of pre-defined classes to help you implement LTI in your own Python applications.

3.4.1. Classes

The ToolProviderApp class is a mini-web framework in itself which makes writing LTI tools much easier. The framework does not include a page templating language but it should be easy to integrate with your templating system of choice.

Instances of ToolProviderApp are callable objects that support the WSGI protocol for ease of integration into a wide variety of deployment scenarios.

The base class implementation takes care of many aspects of your LTI tool:

  1. Application settings are read from a JSON-encoded settings file.
  2. Data storage is configured using one of the concrete implementations of Pyslet’s own data access layer API. No SQL necessary in your code, minimising the risk of SQL injection vulnerabilities!
  3. Session handling: the base class handles the setting of a session cookie and an initial set of redirects to ensure that cookies are being supported properly by the browser. If session handling is broken a fail page method is called. The session logic contains special measures to prevent common session-related attacks (such as session fixation, hijacking and cross-site request forgery) and the redirection sequence is designed to overcome limitaions imposed by broswer restrictions on third party cookies or P3P-related policy issues by providing a user-actionable flow, opening your tool in a new window if necessary. End user messages are customisable.
  4. Launch authorisation is handled automatically, launch requests are checked using OAuth for validity and rejected automatically if invalid. Successful requests are automatically redirected to a resource-specific page.
  5. Each resource is given its own path in your application of the form /<script.wsgi>/resource/<id>/ allowing you to spread your tool application across multiple pages if necessary. A special method, ToolProviderApp.load_visit(), is provided to extract the resource ID from the URL path and load the corresponding entity from the data store. This method also loads the related entities for the the context, user and visit entities from the session according to the parameters passed in the original launch.
  6. An overridable tool permission model is provided with a default implementation that provides read/write/configure permissions to Instructors (and sub-roles) and read permissions to Learners (and sub-roles). This enables your tool to simply test a permission bit at runtime to determine whether or not to display certain page elements.
  7. Tools can be launched multiple times in the same browser session. Authorisations remain active allowing the user to interact with your tool in separate tabs or even in multiple iframes on the same page. Authorisations are automatically expired if a conflicting launch request is received. In other words, if a browser session receives a new launch from the same consumer but for a different user then all the previous user’s activity is automatically logged out.
  8. Consumer secrets can be encrypted when persisted in the data store using an application key. By default the application key is configured in the settings file. (The PyCrypto module is required for encryption.)

The ToolConsumer and ToolProvider classes are largely for internal use. You may want to use them if you are integrating the basic LTI functionality into a different web framework, they contain utility methods for reading information from the data store. You would use the ToolProvider.launch() method in your application when the user POSTs to your launch endpoint to check that the LTI launch has been authorised.

3.4.2. The Data Model

Implementing LTI requires some data storage to persist information between HTTP requests. This module is written using Pyslet’s own data access layer, based on the concepts of OData. For more information see The Open Data Protocol (OData).

A sample metadata file describing the required elements of the model is available as part of Pyslet itself. The entity sets (cf SQL Tables) it describes are as follows:

This entity set is used to store information about encryption keys used to encrypt the consumer secrets in the data store. For more information see pyslet.wsgi.WSGIDataApp
This entity set is the root of the information space for each tool. LTI tools tend to be multi-tenanted, that is, the same tool application can be used by multiple consumers with complete isolation between each consumer’s data. The Silo provides this level of protection. Normally, each Silo will link to a single consumer but there may be cases where two or more consumers should share some data, in these cases a single Silo may link to multiple consumers.
This entity set contains information about the tool consumers. Each consumer is identified by a consumer key and access is protected using a consumer secret (which can be stored in an encrypted form in the data store).
LTI tools are launched from the consumer using OAuth. The protocol requires the use of a nonce (number used once only) to prevent the launch request being ‘replayed’ by an unauthorised person. This entity set is used to record which nonces have been used and when.
The primary launch concept in LTI is the resource. Every launch must have a resource_link_id which identifies the specific ‘place’ or ‘page’ in which the tool has been placed.
LTI defines a context as an optional course or group-like organisation that provides context for a launch request. The context provides another potential scope for sharing data across launches.
An LTI launch is typically identifed with a specific user of the Tool Consumer (though this isn’t required). Information about the users is recorded in the data store so that they can be associated with any data generated by the tool using simple extensions to the data model.
Each time someone launches your tool a visit entity is created with information about the resource, the context and the user.
Used to store information about the browser session, see pyslet.wsgi.SessionApp for details. The basic session entity is extended to link to the visits that are active (i.e., currently authorised) for this session.

These entities are related using navigation properties enabling you to determine, for example, which Consumer a Resource belongs to, which Visits are active in a Session, and so on.

You can extend the core model by adding additional data properties (which should be nullable) or by adding optional navigation properties. For example, you might create an entity set to store information created by users of the tool and add a navigation property from the User entity to your new entity to indicate ownership. The sample Noticeboard application uses this technique and can be used as a guide. Hello LTI

Writing your first LTI tool is easy:

from optparse import OptionParser
import pyslet.imsbltiv1p0 as lti

if __name__ == '__main__':
    parser = OptionParser()
    (options, args) = parser.parse_args()
    lti.ToolProviderApp.setup(options, args)
    app = lti.ToolProviderApp()

Save this script as mytool.py and run it from the command line like this:

$ python mytool.py --help

Built-in to the WSGI base classes is support for running your tool from the command line during development. The script above just uses Python’s builtin options parsing feature to set up the tool class before creating an instance (the WSGI callable object) and running a basic WSGI server using Python’s builtin wsgiref module.

Try running your application with the -m and –create_silo options to use an in-memory SQLite data store and a default consumer.

$ python mytool.py -m

The script may print a warning message to the console warning you that the in-memory database does not support multiple connections, it then just sits waiting for connections on the default port, 8080. The default consumer has key ‘12345’ and secret ‘secret’ (these can be changed using a configuration file!). The launch URL for your running tool is:


If you try it in the IMS test consumer at: http://www.imsglobal.org/developers/LTI/test/v1p1/lms.php you should get something that looks a bit like this:


For a more complete example see the NoticeBoard Sample LTI Tool.

3.4.3. Reference

class pyslet.imsbltiv1p0.ToolProviderApp(**kwargs)

Bases: pyslet.wsgi.SessionApp

Represents WSGI applications that provide LTI Tools

The key ‘ToolProviderApp’ is reserved for settings defined by this class in the settings file. The defined settings are:

silo (‘testing’)
The name of a default silo to create when the –create_silo option is used.
key (‘12345’)
The default consumer key created when –create_silo is used.
secret (‘secret’)
The consumer secret of the default consumer created when –create_silo is used.

We have our own context class

alias of ToolProviderContext

classmethod add_options(parser)

Adds the following options:

--create_silo create default silo and consumer

Provides ToolProviderApp specific bindings.

This method adds bindings for /launch as the launch URL for the tool and all paths within /resource as the resource pages themselves.


Sets the group in the context from the launch parameters


Sets the resource in the context from the launch parameters


Sets the user in the context from the launch parameters


Sets the permissions in the context from the launch params


Permission bit mask representing ‘read’ permission


Permission bit mask representing ‘write’ permission


Permission bit mask representing ‘configure’ permission

classmethod get_permissions(role)

Returns the permissions that apply to a single role

A single URN instance

Specific LTI tools can override this method to provide more complex permission models. Each permission type is represented by an integer bit mask, permissions can be combined with binary or ‘|’ to make an overal permissions integer. The default implementation uses the READ_PERMISSION, WRITE_PERMISSION and CONFIGURE_PERMISSION bit masks but you are free to use any values you wish.

In this implementation, Instructors (and all sub-roles) are granted read, write and configure whereas Learners (and all subroles) are granted read only. Any other role returns 0 (no permissions).

An LTI consumer can specify multiple roles on launch, this method is called for each role and the resulting permissions integers are combined to provide an overall permissions integer.

get_user_display_name(context, user=None)

Given a user entity, returns a display name

If user is None then the user from the context is used instead.


Given a resource entity, returns a display title


Called during launch to create a new visit entity

A new visit entity is created and bound to the resource entity referred to in the launch. The visit entity stores the permissions and a link to the (optional) user entity.

If a visit to the same resource is already associated with the session it is replaced. This ensures that information about the resource, the user, roles and permissions always corresponds to the most recent launch.

Any visits from the same consumer but with a different user are also removed. This handles the case where a previous user of the browser session needs to be logged out of the tool.

find_visit(context, resource_id)

Finds a visit that matches this resource_id


Overridden to update the Session ID in the visit

merge_session(context, merge_session)

Overridden to update the Session ID in any associated visits


Loads an existing LTI visit into the context

You’ll normally call this method from each session decorated method of your tool provider that applies to a protected resource.

This method sets the following attributes of the context…

The resource record is identified from the resource id given in the URL path.
The session is searched for a visit record matching the resource.
Set from the visit record
The optional user is loaded from the visit.
The context record identified from the resource id given in the URL path. This may be None if the resource link was not created in any context.
The consumer object is looked up from the visit entity.

If the visit can’t be set then an exception is raised, an unknown resource raises pyslet.wsgi.PageNotFound whereas the absence of a valid visit for a known resource raises pyslet.wsgi.PageNotAuthorized. These are caught automatically by the WSGI handlers and return 404 and 403 errors respectively.


Redirects to the resource identified on launch

A POST request should pretty much always redirect to a GET page and our tool launches are no different. This allows you to reload a tool page straight away if desired without the risk of double-POST issues.

class pyslet.imsbltiv1p0.ToolProviderContext(environ, start_response, canonical_root=None)

Bases: pyslet.wsgi.SessionContext

consumer = None

a ToolConsumer instance identified from the launch

parameters = None

a dictionary of non-oauth parameters from the launch

visit = None

the effective visit entity

resource = None

the effective resource entity

user = None

the effective user entity

group = None

the effective group (context) entity

permissions = None

the effective permissions (an integer for bitwise testing)

class pyslet.imsbltiv1p0.ToolConsumer(entity, cipher)

Bases: object

An LTI consumer object

An Entity instance.
An AppCipher instance.

This class is a light wrapper for the entity object that is used to persist information on the server about the consumer. The consumer is persisted in a data store using a single entity passed on construction which must have the following required properties:

ID: Int64
A database key for the consumer.
Handle: String
A convenient handle for referring to this consumer in the user interface of the silo’s owner. This handle is never exposed to users launching the tool through the LTI protocol. For example, you might use handles like “LMS Prod” and “LMS Staging” as handles to help distinguish different consumers.
Key: String
The consumer key
Secret: String
The consumer secret (encrypted using cipher).
Silo: Entity
Required navigation property to the Silo this consumer is associated with.
Contexts: Entity Collection
Navigation property to the associated contexts from which this tool has been launched.
Resources: Entity Collection
Navigation property to the associated resources from which this tool has been launched.
Users: Entity Collection
Navigation property to the associated users that have launched the tool.
entity = None

the entity that persists this consumer

cipher = None

the cipher used to

key = None

the consumer key

secret = None

the consumer secret

classmethod new_from_values(entity, cipher, handle, key=None, secret=None)

Create an instance from an new entity

An Entity instance from a suitable entity set.
An AppCipher instance, used to encrypt the secret before storing it.
A string
key (optional)
A text string, defaults to a string generated with generate_key()
secret (optional)
A text string, defaults to a string generated with generate_key()

The fields of the entity are set from the passed in parameters (or the defaults) and then a new instance of cls is constructed from the entity and cipher and returned as a the result.

update_from_values(handle, secret)

Updates an instance from new values

A string used to update the consumer’s handle
A string used to update the consumer’s secret

It is not possible to update the consumer key as this is used to set the ID of the consumer itself.


Returns a key into the nonce table

A string received as a nonce during an LTI launch.

This method hashes the nonce, along with the consumer entity’s Key, to return a hex digest string that can be used as a key for comparing against the nonces used in previous launches.

Mixing the consumer entity’s Key into the hash reduces the chance of a collision between two nonces from separate consumers.

get_context(context_id, title=None, label=None, ctypes=None)

Returns a context entity

The context_id string passed on launch
title (optional)
The title string passed on launch
label (optional)
The label string passed on launch
ctypes (optional)
An array of URI instances representing the context types of this context. See CONTEXT_TYPE_HANDLES for more information.

Returns the context entity.

If this context has never been seen before then a new entity is created and bound to the consumer. Otherwise, the additional information (if supplied) is compared and updated as necessary.

get_resource(resource_link_id, title=None, description=None, context=None)

Returns a resource entity

The resource_link_id string passed on launch (required).
title (optional)
The title string passed on launch, or None.
description (optional)
The description string passed on launch, or None.
context (optional)
The context entity referred to in the launch, or None.

If this resource has never been seen before then a new entity is created and bound to the consumer and (if specified) the context. Otherwise, the additional information (if supplied) is compared and updated as necessary, with the proviso that a resource can never change context, as per the following quote from the specification:

[resource_link_id] will also change if the item is exported from one system or context and imported into another system or context.
get_user(user_id, name_given=None, name_family=None, name_full=None, email=None)

Returns a user entity

The user_id string passed on launch
The user’s given name (or None)
The user’s family name (or None)
The user’s full name (or None)
The user’s email (or None)

If this user has never been seen before then a new entity is created and bound to the consumer, otherwise the

class pyslet.imsbltiv1p0.ToolProvider(consumers, nonces, cipher)

Bases: pyslet.imsbltiv1p0.OAuthMissing, pyslet.pep8.MigratedClass

An LTI tool provider object

The EntitySet containing the tool Consumers.
The EntitySet containing the used Nonces.
An AppCipher instance. Used to decrypt the consumer secret from the database.

Implements the RequestValidator object required by the oauthlib package. Internally creates an instance of SignatureOnlyEndpoint

consumers = None

The entity set containing Silos

nonces = None

The entity set containing Nonces

cipher = None

The cipher object used for encrypting consumer secrets


Implements the required method for consumer lookup

Returns a ToolConsumer instance or raises a KeyError if key is not the key of any known consumer.

launch(command, url, headers, body_string)

Checks a launch request for authorization

The HTTP method, as an upper-case string. Should be POST for LTI.
The full URL of the page requested as part of the launch. This will be the launch URL specified in the LTI protocol and configured in the consumer.
A dictionary of headers, must include the Authorization header but other values are ignored.
The query string (in the LTI case, this is the content of the POST request).

Returns a ToolConsumer instance and a dictionary of parameters on success. If the incoming request is not authorized then LTIAuthenticationError is raised.

This method also checks the LTI message type and protocol version and will raise LTIProtcolError if this is not a recognized launch request.

Launch(*args, **kwargs)

Deprecated equivalent to launch() Metadata


Loads the default metadata document

Returns a pyslet.odata2.metadata.Document instance. The schema is loaded from a bundled metadata document which contains the minimum schema required for an LTI tool provider. Constants and Data

pyslet.imsbltiv1p0.LTI_VERSION = 'LTI-1p0'

The version of LTI we support

pyslet.imsbltiv1p0.LTI_MESSAGE_TYPE = 'basic-lti-launch-request'

The message type we support

pyslet.imsbltiv1p0.SYSROLE_HANDLES = {'Administrator': <pyslet.urn.URN object>, 'Creator': <pyslet.urn.URN object>, 'None': <pyslet.urn.URN object>, 'SysAdmin': <pyslet.urn.URN object>, 'User': <pyslet.urn.URN object>, 'AccountAdmin': <pyslet.urn.URN object>, 'SysSupport': <pyslet.urn.URN object>}

A mapping from a system role handle to the full URN for the role as a URI instance.

pyslet.imsbltiv1p0.INSTROLE_HANDLES = {'None': <pyslet.urn.URN object>, 'Guest': <pyslet.urn.URN object>, 'Learner': <pyslet.urn.URN object>, 'Alumni': <pyslet.urn.URN object>, 'Member': <pyslet.urn.URN object>, 'Other': <pyslet.urn.URN object>, 'Mentor': <pyslet.urn.URN object>, 'ProspectiveStudent': <pyslet.urn.URN object>, 'Administrator': <pyslet.urn.URN object>, 'Observer': <pyslet.urn.URN object>, 'Student': <pyslet.urn.URN object>, 'Faculty': <pyslet.urn.URN object>, 'Instructor': <pyslet.urn.URN object>, 'Staff': <pyslet.urn.URN object>}

A mapping from a institution role handle to the full URN for the role as a URI instance.

pyslet.imsbltiv1p0.ROLE_HANDLES = {'Manager/CourseCoordinator': <pyslet.urn.URN object>, 'Mentor/Auditor': <pyslet.urn.URN object>, 'Instructor/PrimaryInstructor': <pyslet.urn.URN object>, 'TeachingAssistant/TeachingAssistant': <pyslet.urn.URN object>, 'Administrator/Developer': <pyslet.urn.URN object>, 'Member': <pyslet.urn.URN object>, 'ContentDeveloper/ContentExpert': <pyslet.urn.URN object>, 'Mentor/ExternalReviewer': <pyslet.urn.URN object>, 'TeachingAssistant/Grader': <pyslet.urn.URN object>, 'Mentor/Mentor': <pyslet.urn.URN object>, 'ContentDeveloper/Librarian': <pyslet.urn.URN object>, 'Mentor/ExternalTutor': <pyslet.urn.URN object>, 'Instructor/Lecturer': <pyslet.urn.URN object>, 'TeachingAssistant/TeachingAssistantTemplate': <pyslet.urn.URN object>, 'Administrator/Administrator': <pyslet.urn.URN object>, 'Instructor/ExternalInstructor': <pyslet.urn.URN object>, 'TeachingAssistant/TeachingAssistantSection': <pyslet.urn.URN object>, 'Administrator/Support': <pyslet.urn.URN object>, 'Mentor/Advisor': <pyslet.urn.URN object>, 'Mentor': <pyslet.urn.URN object>, 'TeachingAssistant/TeachingAssistantGroup': <pyslet.urn.URN object>, 'Manager/AreaManager': <pyslet.urn.URN object>, 'TeachingAssistant/TeachingAssistantOffering': <pyslet.urn.URN object>, 'Mentor/LearningFacilitator': <pyslet.urn.URN object>, 'Learner/GuestLearner': <pyslet.urn.URN object>, 'Learner': <pyslet.urn.URN object>, 'Learner/Instructor': <pyslet.urn.URN object>, 'Manager/Observer': <pyslet.urn.URN object>, 'Administrator/ExternalDeveloper': <pyslet.urn.URN object>, 'Learner/Learner': <pyslet.urn.URN object>, 'Administrator': <pyslet.urn.URN object>, 'Administrator/ExternalSupport': <pyslet.urn.URN object>, 'Mentor/Tutor': <pyslet.urn.URN object>, 'Mentor/ExternalAuditor': <pyslet.urn.URN object>, 'TeachingAssistant': <pyslet.urn.URN object>, 'Instructor': <pyslet.urn.URN object>, 'Mentor/ExternalMentor': <pyslet.urn.URN object>, 'Administrator/ExternalSystemAdministrator': <pyslet.urn.URN object>, 'Manager/ExternalObserver': <pyslet.urn.URN object>, 'Learner/ExternalLearner': <pyslet.urn.URN object>, 'Mentor/ExternalAdvisor': <pyslet.urn.URN object>, 'ContentDeveloper/ContentDeveloper': <pyslet.urn.URN object>, 'TeachingAssistant/TeachingAssistantSectionAssociation': <pyslet.urn.URN object>, 'ContentDeveloper/ExternalContentExpert': <pyslet.urn.URN object>, 'Mentor/ExternalLearningFacilitator': <pyslet.urn.URN object>, 'ContentDeveloper': <pyslet.urn.URN object>, 'Learner/NonCreditLearner': <pyslet.urn.URN object>, 'Member/Member': <pyslet.urn.URN object>, 'Mentor/Reviewer': <pyslet.urn.URN object>, 'Manager': <pyslet.urn.URN object>, 'Administrator/SystemAdministrator': <pyslet.urn.URN object>, 'Instructor/GuestInstructor': <pyslet.urn.URN object>}

A mapping from LTI role handles to the full URN for the role as a URI instance.


Splits an LTI role into vocab, type and sub-type

A URN instance containing the full definition of the role.

Returns a triple of:

One of ‘role’, ‘sysrole’, ‘instrole’ or some future vocab extension.
The role type, e.g., ‘Learner’, ‘Instructor’
The role sub-type , e.g., ‘NonCreditLearner’, ‘Lecturer’. Will be None if there is no sub-type.

If this is not an LTI defined role, or the role descriptor does not start with the path ims/lis then ValueError is raised.

pyslet.imsbltiv1p0.is_subrole(role, parent_role)

True if role is a sub-role of parent_role

A URN instance containing the full definition of the role to be tested.
A URN instance containing the full definition of the parent role. It must not define a subrole of ValueError is raised.

In the special case that role does not have subrole then it is simply matched against parent_role. This ensures that:

is_subrole(role, ROLE_HANDLES['Learner'])

will return True in all cases where role is a Learner role.

pyslet.imsbltiv1p0.CONTEXT_TYPE_HANDLES = {'CourseSection': <pyslet.urn.URN object>, 'CourseOffering': <pyslet.urn.URN object>, 'Group': <pyslet.urn.URN object>, 'CourseTemplate': <pyslet.urn.URN object>}

A mapping from LTI context type handles to the full URN for the context type as a URI instance. Exceptions

class pyslet.imsbltiv1p0.LTIError

Bases: exceptions.Exception

Base class for all LTI errors

class pyslet.imsbltiv1p0.LTIAuthenticationError

Bases: pyslet.imsbltiv1p0.LTIError

Indicates an authentication error (on launch)

class pyslet.imsbltiv1p0.LTIProtocolError

Bases: pyslet.imsbltiv1p0.LTIError

Indicates a protocol violoation

This may be raised if the message type or protocol version in a launch request do not match the expected values or if a required parameter is missing. Legacy Classes

Earlier Pyslet versions contained a very rudimentary memory based LTI tool provider implementation based on the older oauth module. These classes have been superceded but the main BLTIToolProvider class has been refactored as a derived class of ToolProvider using a SQLite ‘:memory:’ database (instead of a Python dictionary) and the existing method signatures should continue to work as before.

The only change you’ll need to make is to install the newer oauthlib. Bear in mind that these classes are now deprecated and you should refactor to use the base ToolProvider class directly for future compatibility. Please raise an issue on GitHub if you anticipate problems.

class pyslet.imsbltiv1p0.BLTIToolProvider

Bases: pyslet.imsbltiv1p0.ToolProvider

Legacy class for tool provider.

Refactored to build directly on the newer ToolProvider. A single Silo entity is created containing all defined consumers. An in-memory SQLite database is used as the data store. Consumer keys are not encrypted (a plaintext cipher is used) as they will not be persisted.


Generates a new key

Also available as GenerateKey. This method is deprecated, it has been replaced by the similarly named function pyslet.wsgi.generate_key().

The minimum key length in bits. Defaults to 128.

The key is returned as a sequence of 16 bit hexadecimal strings separated by ‘.’ to make them easier to read and transcribe into other systems.

new_consumer(key=None, secret=None)

Creates a new BLTIConsumer instance

Also available as NewConsumer

The new instance is added to the database of consumers authorized to use this tool. The consumer key and secret are automatically generated using generate_key() but key and secret can be passed as optional arguments instead.


Loads the list of trusted consumers

Also available as LoadFromFile

The consumers are loaded from a simple file of key, secret pairs formatted as:

<consumer key> [SPACE]+ <consumer secret>

Lines starting with a ‘#’ are ignored as comments.

GenerateKey(*args, **kwargs)

Deprecated equivalent to generate_key()

LoadFromFile(*args, **kwargs)

Deprecated equivalent to load_from_file()

NewConsumer(*args, **kwargs)

Deprecated equivalent to new_consumer()

SaveToFile(*args, **kwargs)

Deprecated equivalent to save_to_file()


Saves the list of trusted consumers

Also available as SaveToFile

The consumers are saved in a simple file suitable for reading with load_from_file().