Skip to content

Commit

Permalink
Add basic LTI 1.3 support - #636
Browse files Browse the repository at this point in the history
Preliminary launch steps are working, using updated code in our
django-lti-provider-example library.
  • Loading branch information
nikolas committed Mar 8, 2024
1 parent 7b02d44 commit 0c6f37c
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 10 deletions.
7 changes: 5 additions & 2 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
1.0.0
1.1.0
==================
* Add LTI 1.3 support

1.0.0 (2023-10-26)
==================
* Django 3 compatability
* No longer testing against Django 2.2


0.4.7
==================
* Provide a default value for allow_ta_access setting.
Expand Down
17 changes: 14 additions & 3 deletions lti_provider/urls.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from django.urls import re_path

from lti_provider.views import LTIConfigView, LTILandingPage, LTIRoutingView, \
LTICourseEnableView, LTIPostGrade, LTIFailAuthorization, LTICourseConfigure
from lti_provider.views import (
LTIConfigView, LTILandingPage, LTIRoutingView,
LTICourseEnableView, LTIPostGrade, LTIFailAuthorization,
LTICourseConfigure,
login, launch, get_jwks, configure
)


urlpatterns = [
Expand All @@ -17,5 +21,12 @@
re_path(r'^assignment/(?P<assignment_name>.*)/(?P<pk>\d+)/$',
LTIRoutingView.as_view(), {}, 'lti-assignment-view'),
re_path(r'^assignment/(?P<assignment_name>.*)/$',
LTIRoutingView.as_view(), {}, 'lti-assignment-view')
LTIRoutingView.as_view(), {}, 'lti-assignment-view'),

# New pylti1.3 routes
re_path(r'^login/$', login, name='game-login'),
re_path(r'^launch/$', launch, name='game-launch'),
re_path(r'^jwks/$', get_jwks, name='jwks'),
re_path(r'^configure/(?P<launch_id>[\w-]+)/$', configure,
name='lti-configure')
]
123 changes: 118 additions & 5 deletions lti_provider/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import time
import os
import pprint

from django.conf import settings
from django.contrib import messages
Expand All @@ -15,11 +17,16 @@
from lti_provider.models import LTICourseContext
from pylti.common import LTIPostMessageException, post_message


try:
from django.urls import reverse
except ImportError:
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseForbidden, JsonResponse
from django.shortcuts import render
from django.views.decorators.http import require_POST
from django.urls import reverse
from pylti1p3.contrib.django import (
DjangoOIDCLogin, DjangoMessageLaunch, DjangoCacheDataStorage
)
from pylti1p3.deep_link_resource import DeepLinkResource
from pylti1p3.tool_config import ToolConfJsonFile
from pylti1p3.registration import Registration


class LTIConfigView(TemplateView):
Expand Down Expand Up @@ -222,3 +229,109 @@ def post(self, request, *args, **kwargs):
messages.add_message(request, messages.INFO, msg)

return HttpResponseRedirect(redirect_url)


#
# New pylti1p3 funtionality below, adapted from pylti1.3-django-example
#
# https://github.com/dmitry-viskov/pylti1.3-django-example
#
class ExtendedDjangoMessageLaunch(DjangoMessageLaunch):

def validate_nonce(self):
"""
Probably it is bug on "https://lti-ri.imsglobal.org":
site passes invalid "nonce" value during deep links launch.
Because of this in case of iss == http://imsglobal.org just
skip nonce validation.
"""
iss = self.get_iss()
deep_link_launch = self.is_deep_link_launch()
if iss == "http://imsglobal.org" and deep_link_launch:
return self
return super().validate_nonce()


def get_lti_config_path():
return os.path.join(settings.BASE_DIR, 'configs', 'config.json')


def get_tool_conf():
tool_conf = ToolConfJsonFile(get_lti_config_path())
return tool_conf


def get_jwk_from_public_key(key_name):
key_path = os.path.join(settings.BASE_DIR, 'configs', key_name)
f = open(key_path, 'r')
key_content = f.read()
jwk = Registration.get_jwk(key_content)
f.close()
return jwk


def get_launch_data_storage():
return DjangoCacheDataStorage()


def get_launch_url(request):
target_link_uri = request.POST.get(
'target_link_uri', request.GET.get('target_link_uri'))
if not target_link_uri:
raise Exception('Missing "target_link_uri" param')
return target_link_uri


def login(request):
tool_conf = get_tool_conf()
launch_data_storage = get_launch_data_storage()

oidc_login = DjangoOIDCLogin(
request, tool_conf, launch_data_storage=launch_data_storage)
target_link_uri = get_launch_url(request)
return oidc_login\
.enable_check_cookies()\
.redirect(target_link_uri)


@require_POST
def launch(request):
tool_conf = get_tool_conf()
launch_data_storage = get_launch_data_storage()
message_launch = ExtendedDjangoMessageLaunch(
request, tool_conf, launch_data_storage=launch_data_storage)
message_launch_data = message_launch.get_launch_data()
pprint.pprint(message_launch_data)

return render(request, 'game.html', {
'page_title': 'Page Title',
'is_deep_link_launch': message_launch.is_deep_link_launch(),
'launch_data': message_launch.get_launch_data(),
'launch_id': message_launch.get_launch_id(),
'curr_user_name': message_launch_data.get('name', ''),
})


def get_jwks(request):
tool_conf = get_tool_conf()
return JsonResponse(tool_conf.get_jwks(), safe=False)


def configure(request, launch_id):
tool_conf = get_tool_conf()
launch_data_storage = get_launch_data_storage()
message_launch = ExtendedDjangoMessageLaunch.from_cache(
launch_id, request, tool_conf,
launch_data_storage=launch_data_storage)

if not message_launch.is_deep_link_launch():
return HttpResponseForbidden('Must be a deep link!')

launch_url = request.build_absolute_uri(reverse('game-launch'))

resource = DeepLinkResource()
resource.set_url(launch_url).set_title('Custom title!')

html = message_launch.get_deep_link().output_response_form([resource])
return HttpResponse(html)
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"oauth2",
"oauthlib",
"pylti",
"pylti1p3",
],
scripts=[],
license="BSD",
Expand Down
9 changes: 9 additions & 0 deletions test_reqs.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,12 @@ importlib-metadata<7.1 # for flake8
entrypoints==0.4
typing_extensions==4.10.0
pyparsing==3.1.2

certifi==2024.2.2 # requests
idna==3.6 # requests
charset_normalizer==3.3.2 # requests
urllib3==2.2.1 # requests
requests==2.31.0 # pylti1p3
pyjwt==2.8.0 # pylti1p3
jwcrypto==1.5.6 # pylti1p3
pylti1p3==2.0.0

0 comments on commit 0c6f37c

Please sign in to comment.