From a330a53206e01bf3ac74ebfe3b7b9b6c5de91473 Mon Sep 17 00:00:00 2001 From: Jim Laney Date: Wed, 17 Oct 2018 12:10:47 -0700 Subject: [PATCH 1/2] adds oauthlib for lti request validation --- blti/tests.py | 39 +++++++++++++++------- blti/validators.py | 80 ++++++++++++++++++++++++++-------------------- blti/views.py | 12 ++----- setup.py | 2 +- 4 files changed, 77 insertions(+), 56 deletions(-) diff --git a/blti/tests.py b/blti/tests.py index 923db91..9152d88 100644 --- a/blti/tests.py +++ b/blti/tests.py @@ -1,26 +1,43 @@ from django.conf import settings -from django.test import TestCase +from django.test import RequestFactory, TestCase from django.core.exceptions import ImproperlyConfigured -from blti.validators import BLTIOauth, Roles +from blti.validators import BLTIRequestValidator, Roles from blti.crypto import aes128cbc from blti.models import BLTIData from blti import BLTI, BLTIException class BLTIOAuthTest(TestCase): - def test_no_config(self): - self.assertRaises(ImproperlyConfigured, BLTIOauth) + def setUp(self): + self.request = RequestFactory().post( + '/test', data=getattr(settings, 'CANVAS_LTI_V1_LAUNCH_PARAMS', {}), + secure=True) - def test_no_consumer(self): + def test_validate_client_key(self): with self.settings(LTI_CONSUMERS={}): - self.assertRaises(BLTIException, BLTIOauth().get_consumer, 'XYZ') + self.assertFalse( + BLTIRequestValidator().validate_client_key('X', self.request)) - with self.settings(LTI_CONSUMERS={'ABC': '12345'}): - self.assertRaises(BLTIException, BLTIOauth().get_consumer, 'XYZ') + with self.settings(LTI_CONSUMERS={'A': '12345'}): + self.assertTrue( + BLTIRequestValidator().validate_client_key('A', self.request)) - def test_get_consumer(self): - with self.settings(LTI_CONSUMERS={'ABC': '12345'}): - self.assertEquals(BLTIOauth().get_consumer('ABC').secret, '12345') + def test_get_client_secret(self): + with self.settings(LTI_CONSUMERS={}): + self.assertEquals( + BLTIRequestValidator().get_client_secret('X', self.request), + 'dummy') + + with self.settings(LTI_CONSUMERS={'A': '12345'}): + self.assertEquals( + BLTIRequestValidator().get_client_secret('A', self.request), + '12345') + + def test_validate_timestamp_and_nonce(self): + with self.settings(LTI_CONSUMERS={'A': '12345'}): + self.assertTrue( + BLTIRequestValidator().validate_timestamp_and_nonce( + 'X', '123456789', 'ABCDEFG', self.request)) class BLTIDataTest(TestCase): diff --git a/blti/validators.py b/blti/validators.py index 8ad2f1f..81976e9 100644 --- a/blti/validators.py +++ b/blti/validators.py @@ -1,50 +1,62 @@ from django.conf import settings from django.core.exceptions import ImproperlyConfigured +from oauthlib.oauth1.rfc5849.request_validator import RequestValidator +from oauthlib.oauth1.rfc5849.endpoints.signature_only import ( + SignatureOnlyEndpoint) from blti.models import BLTIKeyStore from blti import BLTIException -import oauth2 as oauth import re -class BLTIOauth(object): +class BLTIRequestValidator(RequestValidator): def __init__(self): - if not hasattr(settings, 'LTI_CONSUMERS'): - raise ImproperlyConfigured('Missing setting LTI_CONSUMERS') - - def validate(self, request, params={}): - oauth_server = oauth.Server() - oauth_server.add_signature_method( - oauth.SignatureMethod_HMAC_SHA1()) - - oauth_request = oauth.Request.from_request( - request.method, - request.build_absolute_uri(), - headers=request.META, - parameters=params - ) - - if oauth_request: + self._dummy = {'key': 'dummy', 'secret': 'dummy'} + self._client_secret = None + + @property + def dummy_client(self): + return self._dummy['key'] + + def validate_client_key(self, client_key, request): + client_secret = self.get_client_secret(client_key, request) + if client_secret == self._dummy['secret']: + return False + return True + + def get_client_secret(self, client_key, request): + if self._client_secret is None: try: - key = oauth_request.get_parameter('oauth_consumer_key') - consumer = self.get_consumer(key) - oauth_server._check_signature(oauth_request, consumer, None) - return oauth_request.get_nonoauth_parameters() - except oauth.Error as err: - raise BLTIException(str(err)) + self._client_secret = BLTIKeyStore.objects.get( + consumer_key=client_key).shared_secret + except BLTIKeyStore.DoesNotExist: + try: + self._client_secret = getattr( + settings, 'LTI_CONSUMERS', {})[client_key] + except KeyError: + self._client_secret = self._dummy['secret'] + return self._client_secret + + def validate_timestamp_and_nonce(self, client_key, timestamp, nonce, + request, request_token=None, + access_token=None): + return True - raise BLTIException('Invalid OAuth Request') - def get_consumer(self, key): +class BLTIOauth(object): + def validate(uri, http_method='POST', body=None, headers=None): + request_validator = BLTIRequestValidator() + + endpoint = SignatureOnlyEndpoint(request_validator) try: - model = BLTIKeyStore.objects.get(consumer_key=key) - return oauth.Consumer(key, str(model.shared_secret)) + valid, request = endpoint.validate_request( + uri, http_method, body, headers) + except AttributeError as ex: + raise BLTIException(ex) - except BLTIKeyStore.DoesNotExist: - try: - consumers = getattr(settings, 'LTI_CONSUMERS', {}) - return oauth.Consumer(key, consumers[key]) - except KeyError: - raise BLTIException('No Matching Consumer') + if not valid: + raise BLTIException('Invalid OAuth Request') + + return request.params class Roles(object): diff --git a/blti/views.py b/blti/views.py index 52d4a22..81b47b2 100644 --- a/blti/views.py +++ b/blti/views.py @@ -53,16 +53,8 @@ def dispatch(self, request, *args, **kwargs): return super(BLTILaunchView, self).dispatch(request, *args, **kwargs) def validate(self, request): - params = {} - body = request.read() - try: - params = dict((k, v) for k, v in [tuple( - map(unquote_plus, kv.split('=')) - ) for kv in body.split('&')]) - except Exception: - raise BLTIException('Missing or malformed parameter or value') - - blti_params = BLTIOauth().validate(request, params=params) + blti_params = BLTIOauth().validate( + request.build_absolute_uri(), body=request.read()) self.blti = BLTIData(**blti_params) self.authorize(self.authorized_role) self.set_session(request, **blti_params) diff --git a/setup.py b/setup.py index a3352da..20acf7b 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ include_package_data=True, install_requires=[ 'Django>2.1,<3.0', - 'oauth2', + 'oauthlib', 'PyCrypto', ], license='Apache License, Version 2.0', From 36ea4800ef74b721e65fd5c07afd152be0048905 Mon Sep 17 00:00:00 2001 From: Jim Laney Date: Fri, 19 Oct 2018 11:06:54 -0700 Subject: [PATCH 2/2] implement oauthlib request validation --- blti/__init__.py | 6 +++- blti/tests.py | 72 +++++++++++++++++++++++++++++++------------ blti/validators.py | 47 ++++++++++++++-------------- blti/views.py | 27 +++++++++++----- travis-ci/settings.py | 42 +------------------------ 5 files changed, 101 insertions(+), 93 deletions(-) diff --git a/blti/__init__.py b/blti/__init__.py index 94c10d9..a7c1833 100644 --- a/blti/__init__.py +++ b/blti/__init__.py @@ -21,7 +21,8 @@ def set_session(self, request, **kwargs): request.session.create() kwargs['_blti_session_id'] = request.session.session_key - request.session['blti'] = self._encrypt_session(kwargs) + request.session['blti'] = self._encrypt_session( + self.filter_oauth_params(kwargs)) def get_session(self, request): if 'blti' not in request.session: @@ -38,6 +39,9 @@ def pop_session(self, request): if 'blti' in request.session: request.session.pop('blti', None) + def filter_oauth_params(self, params): + return {k: v for k, v in params.items() if not k.startswith('oauth_')} + def _encrypt_session(self, data): aes = aes128cbc(settings.BLTI_AES_KEY, settings.BLTI_AES_IV) return aes.encrypt(json.dumps(data)) diff --git a/blti/tests.py b/blti/tests.py index 9152d88..31578b9 100644 --- a/blti/tests.py +++ b/blti/tests.py @@ -1,18 +1,36 @@ from django.conf import settings from django.test import RequestFactory, TestCase -from django.core.exceptions import ImproperlyConfigured from blti.validators import BLTIRequestValidator, Roles from blti.crypto import aes128cbc from blti.models import BLTIData from blti import BLTI, BLTIException +import time -class BLTIOAuthTest(TestCase): +class RequestValidatorTest(TestCase): def setUp(self): self.request = RequestFactory().post( '/test', data=getattr(settings, 'CANVAS_LTI_V1_LAUNCH_PARAMS', {}), secure=True) + def test_check_client_key(self): + self.assertTrue(BLTIRequestValidator().check_client_key('x' * 12)) + self.assertTrue(BLTIRequestValidator().check_client_key('5' * 30)) + self.assertTrue(BLTIRequestValidator().check_client_key('-' * 20)) + self.assertTrue(BLTIRequestValidator().check_client_key('_' * 20)) + self.assertFalse(BLTIRequestValidator().check_client_key('x' * 11)) + self.assertFalse(BLTIRequestValidator().check_client_key('x' * 31)) + self.assertFalse(BLTIRequestValidator().check_client_key('*' * 40)) + + def test_check_nonce(self): + self.assertTrue(BLTIRequestValidator().check_nonce('x' * 20)) + self.assertTrue(BLTIRequestValidator().check_nonce('5' * 50)) + self.assertTrue(BLTIRequestValidator().check_nonce('-' * 20)) + self.assertTrue(BLTIRequestValidator().check_nonce('_' * 20)) + self.assertFalse(BLTIRequestValidator().check_nonce('*' * 20)) + self.assertFalse(BLTIRequestValidator().check_nonce('x' * 19)) + self.assertFalse(BLTIRequestValidator().check_nonce('x' * 51)) + def test_validate_client_key(self): with self.settings(LTI_CONSUMERS={}): self.assertFalse( @@ -34,10 +52,17 @@ def test_get_client_secret(self): '12345') def test_validate_timestamp_and_nonce(self): - with self.settings(LTI_CONSUMERS={'A': '12345'}): - self.assertTrue( - BLTIRequestValidator().validate_timestamp_and_nonce( - 'X', '123456789', 'ABCDEFG', self.request)) + self.assertTrue( + BLTIRequestValidator().validate_timestamp_and_nonce( + 'X', time.time(), '', self.request)) + + self.assertFalse( + BLTIRequestValidator().validate_timestamp_and_nonce( + 'X', time.time() - 65, '', self.request)) + + self.assertFalse( + BLTIRequestValidator().validate_timestamp_and_nonce( + 'X', time.time() + 65, '', self.request)) class BLTIDataTest(TestCase): @@ -150,18 +175,27 @@ def test_authorize_specific(self): BLTIException, Roles().authorize, self.blti, role='Manager') -class CryptoTest(TestCase): - test_key = 'DUMMY_KEY_FOR_TESTING_1234567890' - test_iv = 'DUMMY_IV_TESTING' - msgs = [ - ('LTI provides a framework through which an LMS can send some ' - 'verifiable information about a user to a third party.'), - "'abc': {'key': value}" - ] +class BLTISessionTest(TestCase): + def test_encrypt_decrypt_session(self): + with self.settings(BLTI_AES_KEY='DUMMY_KEY_FOR_TESTING_1234567890', + BLTI_AES_IV='DUMMY_IV_TESTING'): + + data = {'abc': {'key': 123}, + 'xyz': ('LTI provides a framework through which an LMS ' + 'can send some verifiable information about a ' + 'user to a third party.')} + + enc = BLTI()._encrypt_session(data) + self.assertEquals(BLTI()._decrypt_session(enc), data) + + def test_filter_oauth_params(self): + with self.settings(BLTI_AES_KEY='DUMMY_KEY_FOR_TESTING_1234567890', + BLTI_AES_IV='DUMMY_IV_TESTING'): + data = getattr(settings, 'CANVAS_LTI_V1_LAUNCH_PARAMS', {}) - def test_encrypt_decrypt(self): - aes = aes128cbc(self.test_key, self.test_iv) + self.assertEquals(len(data), 43) + self.assertEquals(data['oauth_consumer_key'], 'XXXXXXXXXXXXXX') - for msg in self.msgs: - enc = aes.encrypt(msg) - self.assertEquals(aes.decrypt(enc), msg) + data = BLTI().filter_oauth_params(data) + self.assertEquals(len(data), 36) + self.assertRaises(KeyError, lambda: data['oauth_consumer_key']) diff --git a/blti/validators.py b/blti/validators.py index 81976e9..8ca892c 100644 --- a/blti/validators.py +++ b/blti/validators.py @@ -1,25 +1,40 @@ from django.conf import settings from django.core.exceptions import ImproperlyConfigured from oauthlib.oauth1.rfc5849.request_validator import RequestValidator -from oauthlib.oauth1.rfc5849.endpoints.signature_only import ( - SignatureOnlyEndpoint) +from oauthlib.oauth1.rfc5849.utils import UNICODE_ASCII_CHARACTER_SET from blti.models import BLTIKeyStore from blti import BLTIException +import time import re class BLTIRequestValidator(RequestValidator): def __init__(self): - self._dummy = {'key': 'dummy', 'secret': 'dummy'} self._client_secret = None + @property + def allowed_signature_methods(self): + return ['HMAC-SHA1'] + @property def dummy_client(self): - return self._dummy['key'] + return 'dummy' + + @property + def client_key_length(self): + return 12, 30 + + @property + def nonce_length(self): + return 20, 50 + + @property + def safe_characters(self): + return set(UNICODE_ASCII_CHARACTER_SET) | set('-_') def validate_client_key(self, client_key, request): client_secret = self.get_client_secret(client_key, request) - if client_secret == self._dummy['secret']: + if client_secret == self.dummy_client: return False return True @@ -33,30 +48,14 @@ def get_client_secret(self, client_key, request): self._client_secret = getattr( settings, 'LTI_CONSUMERS', {})[client_key] except KeyError: - self._client_secret = self._dummy['secret'] + self._client_secret = self.dummy_client return self._client_secret def validate_timestamp_and_nonce(self, client_key, timestamp, nonce, request, request_token=None, access_token=None): - return True - - -class BLTIOauth(object): - def validate(uri, http_method='POST', body=None, headers=None): - request_validator = BLTIRequestValidator() - - endpoint = SignatureOnlyEndpoint(request_validator) - try: - valid, request = endpoint.validate_request( - uri, http_method, body, headers) - except AttributeError as ex: - raise BLTIException(ex) - - if not valid: - raise BLTIException('Invalid OAuth Request') - - return request.params + now = int(time.time()) + return (now - 60) <= timestamp <= (now + 60) class Roles(object): diff --git a/blti/views.py b/blti/views.py index 81b47b2..a41d403 100644 --- a/blti/views.py +++ b/blti/views.py @@ -5,8 +5,10 @@ from django.views.decorators.csrf import csrf_exempt from blti import BLTI, BLTIException from blti.models import BLTIData -from blti.validators import BLTIOauth, Roles +from blti.validators import BLTIRequestValidator, Roles from blti.performance import log_response_time +from oauthlib.oauth1.rfc5849.endpoints.signature_only import ( + SignatureOnlyEndpoint) class BLTIView(TemplateView): @@ -52,17 +54,26 @@ class BLTILaunchView(BLTIView): def dispatch(self, request, *args, **kwargs): return super(BLTILaunchView, self).dispatch(request, *args, **kwargs) - def validate(self, request): - blti_params = BLTIOauth().validate( - request.build_absolute_uri(), body=request.read()) - self.blti = BLTIData(**blti_params) - self.authorize(self.authorized_role) - self.set_session(request, **blti_params) - def post(self, request, *args, **kwargs): context = self.get_context_data(**kwargs) return self.render_to_response(context) + def validate(self, request): + request_validator = BLTIRequestValidator() + endpoint = SignatureOnlyEndpoint(request_validator) + + valid, oauth_req = endpoint.validate_request( + request.build_absolute_uri(), request.method, body=request.read(), + headers={'Content-Type': 'application/x-www-form-urlencoded'}) + + if not valid: + raise BLTIException('Invalid OAuth Request') + + blti_params = dict(oauth_req.params) + self.set_session(request, **blti_params) + + super(BLTILaunchView, self).validate(request) + class RawBLTIView(BLTILaunchView): template_name = 'blti/raw.html' diff --git a/travis-ci/settings.py b/travis-ci/settings.py index 0f8a069..a7d9e0c 100644 --- a/travis-ci/settings.py +++ b/travis-ci/settings.py @@ -1,43 +1,14 @@ -""" -Django settings for project project. - -Generated by 'django-admin startproject' using Django 2.1.1. - -For more information on this file, see -https://docs.djangoproject.com/en/2.1/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/2.1/ref/settings/ -""" - - import os - BASE_DIR = os.path.dirname(os.path.dirname(__file__)) -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' -# SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -TEMPLATE_DEBUG = True - -ALLOWED_HOSTS = [] - - -# Application definition - INSTALLED_APPS = [ - 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', 'blti', ] @@ -47,24 +18,16 @@ 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.RemoteUserMiddleware', - 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] AUTHENTICATION_BACKENDS = [ 'django.contrib.auth.backends.RemoteUserBackend', -# 'django.contrib.auth.backends.ModelBackend', ] ROOT_URLCONF = 'travis-ci.urls' WSGI_APPLICATION = 'travis-ci.wsgi.application' - -# Database -# https://docs.djangoproject.com/en/1.7/ref/settings/#databases - DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', @@ -72,9 +35,6 @@ } } -# Internationalization -# https://docs.djangoproject.com/en/1.7/topics/i18n/ - LANGUAGE_CODE = 'en-us' TIME_ZONE = 'America/Los_Angeles' @@ -87,4 +47,4 @@ STATIC_URL = '/static/' -CANVAS_LTI_V1_LAUNCH_PARAMS = {'launch_presentation_height': '400', 'user_image': 'https://example.instructure.com/images/thumbnails/123456/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 'context_id': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 'tool_consumer_info_version': 'cloud', 'ext_roles': 'urn:lti:instrole:ims/lis/Administrator,urn:lti:instrole:ims/lis/Instructor,urn:lti:instrole:ims/lis/Student,urn:lti:role:ims/lis/Instructor,urn:lti:role:ims/lis/Learner/NonCreditLearner,urn:lti:role:ims/lis/Mentor,urn:lti:sysrole:ims/lis/User', 'tool_consumer_instance_guid': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.example.instructure.com', 'context_label': 'ABC 101 A', 'lti_message_type': 'basic-lti-launch-request', 'custom_canvas_workflow_state': 'claimed', 'lis_person_name_full': 'James Average', 'context_title': 'ABC 101 A: Example Course', 'user_id': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 'custom_canvas_user_id': '123456', 'launch_presentation_locale': 'en', 'custom_canvas_api_domain': 'example.instructure.com', 'custom_canvas_enrollment_state': 'active', 'tool_consumer_instance_contact_email': 'example@example.instructure.com', 'tool_consumer_info_product_family_code': 'canvas', 'custom_application_type': 'ExampleApp', 'lis_person_name_family': 'Average', 'lis_course_offering_sourcedid': '2018-spring-ABC-101-A', 'launch_presentation_width': '800', 'resource_link_title': 'Example App', 'custom_canvas_account_sis_id': 'example:account', 'lis_person_sourcedid': 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', 'tool_consumer_instance_name': 'Example University', 'resource_link_id': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 'lis_person_contact_email_primary': 'javerage@example.edu', 'roles': 'Instructor,urn:lti:instrole:ims/lis/Administrator', 'custom_canvas_course_id': '123456', 'lti_version': 'LTI-1p0', 'lis_person_name_given': 'James', 'launch_presentation_return_url': 'https://example.instructure.com/courses/123456', 'launch_presentation_document_target': 'iframe', 'custom_canvas_account_id': '12345', 'custom_canvas_user_login_id': 'javerage'} +CANVAS_LTI_V1_LAUNCH_PARAMS = {'oauth_consumer_key': 'XXXXXXXXXXXXXX', 'oauth_signature_method': 'HMAC-SHA1', 'oauth_timestamp': '1234567890', 'oauth_nonce': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 'oauth_signature': 'XXXXXXXXXXXXXXXXXXXXXXXXXXX=', 'oauth_callback': 'about:blank', 'oauth_version': '1.0', 'launch_presentation_height': '400', 'user_image': 'https://example.instructure.com/images/thumbnails/123456/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 'context_id': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 'tool_consumer_info_version': 'cloud', 'ext_roles': 'urn:lti:instrole:ims/lis/Administrator,urn:lti:instrole:ims/lis/Instructor,urn:lti:instrole:ims/lis/Student,urn:lti:role:ims/lis/Instructor,urn:lti:role:ims/lis/Learner/NonCreditLearner,urn:lti:role:ims/lis/Mentor,urn:lti:sysrole:ims/lis/User', 'tool_consumer_instance_guid': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.example.instructure.com', 'context_label': 'ABC 101 A', 'lti_message_type': 'basic-lti-launch-request', 'custom_canvas_workflow_state': 'claimed', 'lis_person_name_full': 'James Average', 'context_title': 'ABC 101 A: Example Course', 'user_id': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 'custom_canvas_user_id': '123456', 'launch_presentation_locale': 'en', 'custom_canvas_api_domain': 'example.instructure.com', 'custom_canvas_enrollment_state': 'active', 'tool_consumer_instance_contact_email': 'example@example.instructure.com', 'tool_consumer_info_product_family_code': 'canvas', 'custom_application_type': 'ExampleApp', 'lis_person_name_family': 'Average', 'lis_course_offering_sourcedid': '2018-spring-ABC-101-A', 'launch_presentation_width': '800', 'resource_link_title': 'Example App', 'custom_canvas_account_sis_id': 'example:account', 'lis_person_sourcedid': 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', 'tool_consumer_instance_name': 'Example University', 'resource_link_id': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 'lis_person_contact_email_primary': 'javerage@example.edu', 'roles': 'Instructor,urn:lti:instrole:ims/lis/Administrator', 'custom_canvas_course_id': '123456', 'lti_version': 'LTI-1p0', 'lis_person_name_given': 'James', 'launch_presentation_return_url': 'https://example.instructure.com/courses/123456', 'launch_presentation_document_target': 'iframe', 'custom_canvas_account_id': '12345', 'custom_canvas_user_login_id': 'javerage'}