LTI 1.3 Advantage Tool implementation in Python
Project description
LTI 1.3 Advantage Tool implementation in Python
This project is a Python implementation of the similar PHP tool. Library contains adapters for usage from Django and Flask web frameworks but there is no difficulty to adapt it to other frameworks: you should just re-implement OIDCLogin and MessageLaunch classes as it is already done in existing adapters.
Examples of usage
Django: https://github.com/dmitry-viskov/pylti1.3-django-example
Flask: https://github.com/dmitry-viskov/pylti1.3-flask-example
Configuration
To configure your own tool you may use built-in adapters:
from pylti1p3.tool_config import ToolConfJsonFile
tool_conf = ToolConfJsonFile('path/to/json')
from pylti1p3.tool_config import ToolConfDict
settings = {
"<issuer_1>": { }, # one issuer ~ one client-id (outdated and not recommended way)
"<issuer_2>": [{ }, { }] # one issuer ~ many client-ids (preferable way)
}
private_key = '...'
public_key = '...'
tool_conf = ToolConfDict(settings)
client_id = '...' # must be set in case of "one issuer ~ many client-ids" concept
tool_conf.set_private_key(iss, private_key, client_id=client_id)
tool_conf.set_public_key(iss, public_key, client_id=client_id)
or create your own implementation. The pylti1p3.tool_config.ToolConfAbstract interface must be fully implemented for this to work. Concept of one issuer ~ many client-ids is a preferable way to organize configs and may be useful in case of integration with Canvas (https://canvas.instructure.com) or other Cloud LMS-es where platform doesn’t change iss for each customer.
In case of Django Framework you may use DjangoDbToolConf (see Configuration using Django Admin UI section below)
Example of JSON config:
{
"iss1": [{
"default": true,
"client_id": "client_id1",
"auth_login_url": "auth_login_url1",
"auth_token_url": "auth_token_url1",
"auth_audience": null,
"key_set_url": "key_set_url1",
"key_set": null,
"private_key_file": "private.key",
"public_key_file": "public.key",
"deployment_ids": ["deployment_id1", "deployment_id2"]
}, {
"default": false,
"client_id": "client_id2",
"auth_login_url": "auth_login_url2",
"auth_token_url": "auth_token_url2",
"auth_audience": null,
"key_set_url": "key_set_url2",
"key_set": null,
"private_key_file": "private.key",
"public_key_file": "public.key",
"deployment_ids": ["deployment_id3", "deployment_id4"]
}],
"iss2": [ ],
"iss3": { }
}
Usage with Django
Configuration using Django Admin UI
# settings.py
INSTALLED_APPS = [
'django.contrib.admin',
...
'pylti1p3.contrib.django.lti1p3_tool_config'
]
# urls.py
urlpatterns = [
...
path('admin/', admin.site.urls),
...
]
# views.py
from pylti1p3.contrib.django import DjangoDbToolConf
tool_conf = DjangoDbToolConf()
Open Id Connect Login Request
LTI 1.3 uses a modified version of the OpenId Connect third party initiate login flow. This means that to do an LTI 1.3 launch, you must first receive a login initialization request and return to the platform.
To handle this request, you must first create a new OIDCLogin (or DjangoOIDCLogin) object:
from pylti1p3.contrib.django import DjangoOIDCLogin
oidc_login = DjangoOIDCLogin(request, tool_conf)
Now you must configure your login request with a return url (this must be preconfigured and white-listed on the tool). If a redirect url is not given or the registration does not exist an pylti1p3.exception.OIDC_Exception will be thrown.
try:
oidc_login.redirect(get_launch_url(request))
except OIDC_Exception:
# display error page
log.error('Error doing OIDC login')
With the redirect, we can now redirect the user back to the tool. There are three ways to do this:
This will add a HTTP 302 location header:
oidc_login.redirect(get_launch_url(request))
This will display some javascript to do the redirect instead of using a HTTP 302:
oidc_login.redirect(get_launch_url(request), js_redirect=True)
You can also get the url you need to redirect to, with all the necessary query parameters (if you would prefer to redirect in a custom way):
redirect_obj = oidc_login.get_redirect_object()
redirect_url = redirect_obj.get_redirect_url()
Redirect is done, we can move onto the launch.
LTI Message Launches
Now that we have done the OIDC log the platform will launch back to the tool. To handle this request, first we need to create a new MessageLaunch (or DjangoMessageLaunch) object.
message_launch = DjangoMessageLaunch(request, tool_conf)
Once we have the message launch, we can validate it. Validation is transparent - it’s done once before you try to access the message body:
try:
launch_data = message_launch.get_launch_data()
except LtiException:
log.error('Launch validation failed')
You may do it more explicitly:
try:
launch_data = message_launch.set_auto_validation(enable=False).validate()
except LtiException:
log.error('Launch validation failed')
Now we know the launch is valid we can find out more information about the launch.
Check if we have a resource launch or a deep linking launch:
if message_launch.is_resource_launch():
# Resource Launch!
elif message_launch.is_deep_link_launch():
# Deep Linking Launch!
else:
# Unknown launch type
Check which services we have access to:
if message_launch.has_ags():
# Has Assignments and Grades Service
if message_launch.has_nrps():
# Has Names and Roles Service
Accessing Cached Launch Requests
It is likely that you will want to refer back to a launch later during subsequent requests. This is done using the launch id to identify a cached request. The launch id can be found using:
launch_id = message_launch.get_launch_id()
Once you have the launch id, you can link it to your session and pass it along as a query parameter.
Retrieving a launch using the launch id can be done using:
message_launch = DjangoMessageLaunch.from_cache(launch_id, request, tool_conf)
Once retrieved, you can call any of the methods on the launch object as normal, e.g.
if message_launch.has_ags():
# Has Assignments and Grades Service
Deep Linking Responses
If you receive a deep linking launch, it is very likely that you are going to want to respond to the deep linking request with resources for the platform.
To create a deep link response you will need to get the deep link for the current launch:
deep_link = message_launch.get_deep_link()
Now we need to create pylti1p3.deep_link_resource.DeepLinkResource to return:
resource = DeepLinkResource()
resource.set_url("https://my.tool/launch")\
.set_custom_params({'my_param': my_param})\
.set_title('My Resource')
Everything is set to return the resource to the platform. There are two methods of doing this.
The following method will output the html for an aut-posting form for you.
deep_link.output_response_form([resource1, resource2])
Alternatively you can just request the signed JWT that will need posting back to the platform by calling.
deep_link.get_response_jwt([resource1, resource2])
Names and Roles Service
Before using names and roles you should check that you have access to it:
if not message_launch.has_nrps():
raise Exception("Don't have names and roles!")
Once we know we can access it, we can get an instance of the service from the launch.
nrps = message_launch.get_nrps()
From the service we can get list of all members by calling:
members = nrps.get_members()
Assignments and Grades Service
Before using assignments and grades you should check that you have access to it:
if not launch.has_ags():
raise Exception("Don't have assignments and grades!")
Once we know we can access it, we can get an instance of the service from the launch:
ags = launch.get_ags()
To pass a grade back to the platform, you will need to create a pylti1p3.grade.Grade object and populate it with the necessary information:
gr = Grade()
gr.set_score_given(earned_score)\
.set_score_maximum(100)\
.set_timestamp(datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S+0000'))\
.set_activity_progress('Completed')\
.set_grading_progress('FullyGraded')\
.set_user_id(external_user_id)
To send the grade to the platform we can call:
ags.put_grade(gr)
This will put the grade into the default provided lineitem. If no default lineitem exists it will create one.
If you want to send multiple types of grade back, that can be done by specifying a pylti1p3.lineitem.LineItem:
line_item = LineItem()
line_item.set_tag('grade')\
.set_score_maximum(100)\
.set_label('Grade')
ags.put_grade(gr, line_item)
If a lineitem with the same tag exists, that lineitem will be used, otherwise a new lineitem will be created.
Check user’s role after LTI launch
user_is_staff = message_launch.check_staff_access()
user_is_student = message_launch.check_student_access())
user_is_teacher = message_launch.check_teacher_access()
user_is_teaching_assistant = message_launch.check_teaching_assistant_access()
user_is_designer = message_launch.check_designer_access()
user_is_observer = message_launch.check_observer_access()
user_is_transient = message_launch.check_transient()
Usage with Flask
Open Id Connect Login Request
This is draft of API endpoint. Wrap it in library of your choice.
Create FlaskRequest adapter. Then create instance of FlaskOIDCLogin. redirect method will return instance of werkzeug.wrappers.Response that points to LTI platform if login was successful. Handle exceptions.
from flask import request, session
from pylti1p3.flask_adapter import (FlaskRequest, FlaskOIDCLogin)
def login(request_params_dict):
tool_conf = ... # See Configuration chapter above
# FlaskRequest by default use flask.request and flask.session
# so in this case you may define request object without any arguments:
request = FlaskRequest()
# in case of using different request object (for example webargs or something like this)
# you may pass your own values:
request = FlaskRequest(
cookies=request.cookies,
session=session,
request_data=request_params_dict,
request_is_secure=request.is_secure
)
oidc_login = FlaskOIDCLogin(
request=request,
tool_config=tool_conf,
session_service=FlaskSessionService(request),
cookie_service=FlaskCookieService(request)
)
return oidc_login.redirect(request.get_param('target_link_uri'))
LTI Message Launches
This is draft of API endpoint. Wrap it in library of your choice.
Create FlaskRequest adapter. Then create instance of FlaskMessageLaunch. It lets you access data from LTI launch message if launch was successful. Handle exceptions.
from flask import request, session
from werkzeug.utils import redirect
from pylti1p3.flask_adapter import (FlaskRequest, FlaskMessageLaunch)
def launch(request_params_dict):
tool_conf = ... # See Configuration chapter above
request = FlaskRequest()
# or
request = FlaskRequest(
cookies=...,
session=...,
request_data=...,
request_is_secure=...
)
message_launch = FlaskMessageLaunch(
request=request,
tool_config=tool_conf
)
email = message_launch.get_launch_data().get('email')
# Place your user creation/update/login logic
# and redirect to tool content here
Cache for Public Key
Library try to fetch platform’s public key everytime on the message launch step. This public key may be stored in cache (memcache/redis) to speed-up launch process:
# Django cache storage:
launch_data_storage = DjangoCacheDataStorage()
# Flask cache storage:
launch_data_storage = FlaskCacheDataStorage(cache)
message_launch.set_public_key_caching(launch_data_storage, cache_lifetime=7200)
API to get JWKS
You may generate JWKS from Tool Config object:
tool_conf.set_public_key(iss, public_key, client_id=client_id)
jwks_dict = tool_conf.get_jwks() # {"keys": [{ ... }]}
# or you may specify iss and client_id:
jwks_dict = tool_conf.get_jwks(iss, client_id) # {"keys": [{ ... }]}
Don’t forget to set public key because without it JWKS can’t be generated. Also you may generate JWK for any public key using construction below:
from pylti1p3.registration import Registration
jwk_dict = Registration.get_jwk(public_key)
# {"e": ..., "kid": ..., "kty": ..., "n": ..., "alg": ..., "use": ...}
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file PyLTI1p3-1.8.3.tar.gz
.
File metadata
- Download URL: PyLTI1p3-1.8.3.tar.gz
- Upload date:
- Size: 38.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.1.1 pkginfo/1.5.0.1 requests/2.22.0 setuptools/45.2.0 requests-toolbelt/0.9.1 tqdm/4.43.0 CPython/3.7.4
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 427d6cf1fc288ee44f9e63b063d03a9d72116f4ca8f5bdc788b7652ceb4dd026 |
|
MD5 | fc27438b57d4daa02101c1bd5be01fb0 |
|
BLAKE2b-256 | ca1b713f7360f3b962604b76413f32c48c6f2c0075a57b53f08e6ca80cba9aaf |
Provenance
File details
Details for the file PyLTI1p3-1.8.3-py2.py3-none-any.whl
.
File metadata
- Download URL: PyLTI1p3-1.8.3-py2.py3-none-any.whl
- Upload date:
- Size: 55.8 kB
- Tags: Python 2, Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.1.1 pkginfo/1.5.0.1 requests/2.22.0 setuptools/45.2.0 requests-toolbelt/0.9.1 tqdm/4.43.0 CPython/3.7.4
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 542f3cf1b19f3f6eff3adb2145e974ee2f932889e0591fbfa55e6006d83df866 |
|
MD5 | 91c79726a1705d068384b127f0f42a26 |
|
BLAKE2b-256 | 0cbbf4953d42c0474190444c0d7f17b5180b19660abfd26e748d770161a86495 |