pylti1p3: LTI 1.3 Advantage Tool
pylti1p3 is a Python library implementing the LTI 1.3 Advantage Tool specification, enabling seamless integration with LTI 1.3 platforms. It handles OAuth 2.0, JWT validation, deep linking, and various LTI services. The current version is 2.0.0, with an active release cadence addressing new LTI features, bug fixes, and Python compatibility.
Common errors
-
from pylti1p3.message_launch import MessageLaunch ImportError: cannot import name 'MessageLaunch' from 'pylti1p3.message_launch'
cause This usually indicates an incorrect import path or a corrupted installation.fixVerify the exact casing and module name. Reinstall the library using `pip uninstall pylti1p3 && pip install pylti1p3`. If using an older version, consult its specific documentation for import paths. -
jwt.exceptions.InvalidSignatureError: Signature verification failed
cause The JWT received from the LTI platform could not be verified. This often means your public key URL (JWKS URL) or the platform's JWKS is incorrect/unavailable, or the token was signed with a different key.fixEnsure the `key_set_url` in your `ToolConfig` is correct and accessible. Verify that the platform's public key (JWKS) has not changed and that your tool is correctly registered with the platform. -
TypeError: 'ToolConfig' object is not subscriptable
cause You are trying to access attributes of `ToolConfig` using dictionary-style access (e.g., `tool_config['key_set_url']`) instead of object-style access.fixAccess `ToolConfig` attributes directly, e.g., `tool_config.get('key_set_url')` or `tool_config.key_set_url` if the property is exposed directly. The `ToolConfig` constructor expects a dictionary, but once instantiated, it's an object. -
AttributeError: 'MessageLaunch' object has no attribute 'get_target_link_uri'
cause You are trying to access a method or attribute that either does not exist in your installed version or is not directly exposed by the `MessageLaunch` object. Specifically `target_link_uri` is a claim within the launch data.fixAccess claims via `message_launch.get_launch_data()` and then parse the dictionary, or use specific helper methods like `message_launch.get_target_link_uri()` if they exist in your version. For `target_link_uri`, ensure your `id_token` contains this claim. For earlier versions, you might need to manually extract from `launch_data['https://purl.imsglobal.org/spec/lti/claim/target_link_uri']`.
Warnings
- breaking Version 2.0.0 dropped support for Python 2.7 and 3.5. Applications running on these Python versions will break.
- breaking In version 1.12.0, the `AssignmentsGradesService.put_grade` and `AssignmentsGradesService.get_grades` methods no longer automatically create new line items if they don't exist. You must explicitly create line items before attempting to put grades or retrieve grades for them.
- gotcha Incorrect configuration of `ToolConfig` parameters, especially `private_key` and `public_key`, `key_set_url`, `auth_login_url`, and `auth_token_url`, is a common source of LTI launch failures.
Install
-
pip install pylti1p3
Imports
- ToolConfig
from pylti1p3.tool_config import ToolConfig
- MessageLaunch
from pylti1p3.message_launch import MessageLaunch
- ServiceConnector
from pylti1p3.service_connector import ServiceConnector
- AssignmentsGradesService
from pylti1p3.grade import AssignmentsGradesService
- NamesRolesProvisioningService
from pylti1p3.grade import NamesRolesProvisioningService
Quickstart
import os
import json
from pylti1p3.tool_config import ToolConfig
from pylti1p3.message_launch import MessageLaunch
# Mock a minimal request object for demonstration
# In a real application, this would come from your web framework (e.g., Flask, Django)
class MockRequest:
def __init__(self, method='POST', headers=None, form=None, data=None):
self.method = method
self.headers = headers or {}
self.form = form or {}
self.data = data # raw body for content_type application/json, etc.
def get_json(self):
return json.loads(self.data) if self.data and 'application/json' in self.headers.get('Content-Type', '') else None
def get_param(self, key, default=None):
return self.form.get(key, default)
# 1. Configure the LTI Tool
# These values would typically come from environment variables, database, or a configuration file
iss = os.environ.get('LTI_ISS', 'https://example.com')
client_id = os.environ.get('LTI_CLIENT_ID', 'your-client-id')
jwks_url = os.environ.get('LTI_JWKS_URL', 'https://example.com/platform/.well-known/jwks.json')
auth_login_url = os.environ.get('LTI_AUTH_LOGIN_URL', 'https://example.com/platform/login_initiations')
auth_token_url = os.environ.get('LTI_AUTH_TOKEN_URL', 'https://example.com/platform/access_token')
deployment_id = os.environ.get('LTI_DEPLOYMENT_ID', '1') # Example deployment ID
# Your tool's private key (for signing messages sent *to* the platform)
# In a real app, this would be loaded from a file or secure store
private_key = os.environ.get('LTI_TOOL_PRIVATE_KEY', '-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----')
# Your tool's public key (to be registered with the platform)
# In a real app, this would be loaded from a file or secure store
public_key = os.environ.get('LTI_TOOL_PUBLIC_KEY', '-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----')
tool_config = ToolConfig({
'key_set_url': jwks_url,
'iss': iss,
'client_id': client_id,
'deployment_ids': [deployment_id],
'auth_login_url': auth_login_url,
'auth_token_url': auth_token_url,
'private_key': private_key,
'public_key': public_key
})
# 2. Simulate an LTI 1.3 Message Launch request
# This is a highly simplified mock. A real LTI launch involves a POST request
# with a 'id_token' parameter (a signed JWT) and possibly 'state' for CSRF protection.
# For a basic example, we'll just demonstrate setting up the MessageLaunch object.
# In a real scenario, the 'id_token' would be extracted from the incoming request's form data.
# Here, we'll use a placeholder JWT string that would normally be generated by the Platform.
# Note: This placeholder JWT will NOT be valid for actual verification.
# A valid JWT needs to be signed by the platform's private key and match the JWKS.
example_id_token = os.environ.get('LTI_EXAMPLE_ID_TOKEN', 'eyJhbGciOiJSUzI1NiIsImtpZCI6IkExMjMifQ.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoiMTIzNDUifQ.S0meS1gnedT0ken')
mock_form_data = {
'id_token': example_id_token,
'state': 'a_random_state_string'
}
mock_request = MockRequest(method='POST', form=mock_form_data)
# 3. Process the LTI Message Launch
# The MessageLaunch object requires a 'request' (your web framework's request object)
# and the 'ToolConfig' you just created.
try:
message_launch = MessageLaunch(mock_request, tool_config)
is_valid_launch = message_launch.validate()
if is_valid_launch:
print("LTI 1.3 Message Launch validated successfully!")
launch_data = message_launch.get_launch_data()
print("Launch Data (Payload):\n", json.dumps(launch_data, indent=2))
# Example: Accessing specific claims
print("User ID:", message_launch.get_sub()) # 'sub' is the user ID
print("Context Title:", message_launch.get_context_title()) # Course title
# Access LTI 1.3 services (e.g., Assignment and Grades Service)
# This requires the launch to contain the appropriate service context and claims
# if message_launch.has_ags():
# print("Assignments and Grades Service available!")
# ags = message_launch.get_ags()
# # Example: Get line items
# # line_items = ags.get_lineitems()
# # print("Line items:", line_items)
else:
print("LTI 1.3 Message Launch validation failed.")
except Exception as e:
print(f"An error occurred during LTI launch processing: {e}")
# In a real app, handle authentication/authorization errors gracefully