def _make_lti11_basic_launch_args( oauth_consumer_key: str = "my_consumer_key", oauth_consumer_secret: str = "my_shared_secret", ): oauth_timestamp = str(int(time.time())) oauth_nonce = secrets.token_urlsafe(32) args = { "lti_message_type": "basic-lti-launch-request", "lti_version": "LTI-1p0".encode(), "resource_link_id": "88391-e1919-bb3456", "oauth_consumer_key": oauth_consumer_key, "oauth_timestamp": str(int(oauth_timestamp)), "oauth_nonce": str(oauth_nonce), "oauth_signature_method": "HMAC-SHA1", "oauth_callback": "about:blank", "oauth_version": "1.0", "user_id": "123123123", } extra_args = {"my_key": "this_value"} headers = {"Content-Type": "application/x-www-form-urlencoded"} launch_url = "http://jupyterhub/hub/lti/launch" args.update(extra_args) base_string = signature.signature_base_string( "POST", signature.base_string_uri(launch_url), signature.normalize_parameters( signature.collect_parameters(body=args, headers=headers)), ) args["oauth_signature"] = signature.sign_hmac_sha1( base_string, oauth_consumer_secret, None) return args
def sign_data(self, method, path, data): initial_data = { "oauth_consumer_key": settings.LTI_KEY, "oauth_signature_method": "HMAC-SHA1", "oauth_nonce": "nonce", "oauth_timestamp": str(math.floor(time.time())), "oauth_version": "1.0" } data = {**initial_data, **data} params = oauth1.collect_parameters(body=data, exclude_oauth_signature=True, with_realm=False) norm_params = oauth1.normalize_parameters(params) base_string = oauth1.signature_base_string(method, 'http://testserver' + path, norm_params) signature = oauth1.sign_hmac_sha1( base_string, settings.LTI_SECRET, '' # resource_owner_secret - not used ) data["oauth_signature"] = signature return urlencode(data)
def _make_lti11_basic_launch_args( oauth_consumer_key: str = 'my_consumer_key', oauth_consumer_secret: str = 'my_shared_secret', ): oauth_timestamp = str(int(time.time())) oauth_nonce = secrets.token_urlsafe(32) args = { 'lti_message_type': 'basic-lti-launch-request', 'lti_version': 'LTI-1p0'.encode(), 'resource_link_id': '88391-e1919-bb3456', 'oauth_consumer_key': oauth_consumer_key, 'oauth_timestamp': str(int(oauth_timestamp)), 'oauth_nonce': str(oauth_nonce), 'oauth_signature_method': 'HMAC-SHA1', 'oauth_callback': 'about:blank', 'oauth_version': '1.0', 'user_id': '123123123', } extra_args = {'my_key': 'this_value'} headers = {'Content-Type': 'application/x-www-form-urlencoded'} launch_url = 'http://jupyterhub/hub/lti/launch' args.update(extra_args) base_string = signature.signature_base_string( 'POST', signature.base_string_uri(launch_url), signature.normalize_parameters( signature.collect_parameters(body=args, headers=headers)), ) args['oauth_signature'] = signature.sign_hmac_sha1( base_string, oauth_consumer_secret, None) return args
def build_signature(request, consumer_secret): """Uses the request object and consumer secret to build a signature This is an internal function and it's highly unlikely than an end user would ever need to call this :param request : a twolegged.Request subclass :param consumer_secret : string :rtype : string """ headers = request.headers() auth_headers = {k: v for k, v in headers.iteritems() if k == 'Authorization'} # we are only interested in the POST data if it's passed as form # parameters while raw POST body is ignored body = request.form_data() params = [(k, v) for k, v in request.params() if k != 'oauth_signature'] qs = RequestEncodingMixin._encode_params(params) collected_params = collect_parameters(qs, body, auth_headers) normalized_params = normalize_parameters(collected_params) host = headers.get('Host', None) normalized_uri = normalize_base_string_uri(request.base_url(), host) base_string = construct_base_string(unicode(request.method()), normalized_uri, normalized_params) return sign_hmac_sha1(base_string, consumer_secret, None)
def _get_validated_lti_params_from_values(cls, request, current_time, lti_consumer_valid, lti_consumer_secret, lti_max_timestamp_age): """ Validates LTI signature and returns LTI parameters """ # Taking a cue from oauthlib, to avoid leaking information through a timing attack, # we proceed through the entire validation before rejecting any request for any reason. # However, as noted there, the value of doing this is dubious. try: base_uri = normalize_base_string_uri(request.uri) parameters = collect_parameters(uri_query=request.uri_query, body=request.body) parameters_string = normalize_parameters(parameters) base_string = construct_base_string(request.http_method, base_uri, parameters_string) computed_signature = sign_hmac_sha1(base_string, unicode(lti_consumer_secret), '') submitted_signature = request.oauth_signature data = { parameter_value_pair[0]: parameter_value_pair[1] for parameter_value_pair in parameters } def safe_int(value): """ Interprets parameter as an int or returns 0 if not possible """ try: return int(value) except (ValueError, TypeError): return 0 oauth_timestamp = safe_int(request.oauth_timestamp) # As this must take constant time, do not use shortcutting operators such as 'and'. # Instead, use constant time operators such as '&', which is the bitwise and. valid = (lti_consumer_valid) valid = valid & (submitted_signature == computed_signature) valid = valid & (request.oauth_version == '1.0') valid = valid & (request.oauth_signature_method == 'HMAC-SHA1') valid = valid & ( 'user_id' in data ) # Not required by LTI but can't log in without one valid = valid & (oauth_timestamp >= current_time - lti_max_timestamp_age) valid = valid & (oauth_timestamp <= current_time) if valid: return data except AttributeError as error: log.error("'{}' not found.".format(error.message)) return None
def test_sign_hmac_sha1(self): """Verifying HMAC-SHA1 signature against one created by OpenSSL.""" self.assertRaises(ValueError, sign_hmac_sha1, self.control_base_string, self.client_secret, self.resource_owner_secret) sign = sign_hmac_sha1(self.control_base_string, self.client_secret.decode('utf-8'), b'') self.assertEqual(len(sign), 28) self.assertEqual(sign, self.control_signature)
def test_sign_hmac_sha1(self): """Verifying HMAC-SHA1 signature against one created by OpenSSL.""" self.assertRaises(ValueError, sign_hmac_sha1, self.control_base_string, self.client_secret, self.resource_owner_secret) sign = sign_hmac_sha1(self.control_base_string, self.client_secret.decode('utf-8'), self.resource_owner_secret.decode('utf-8')) self.assertEqual(len(sign), 28) self.assertEqual(sign, self.control_signature)
def test_sign_hmac_sha1(self): """Verifying HMAC-SHA1 signature against one created by OpenSSL.""" # Control signature created using openssl: # echo -n $(cat <message>) | openssl dgst -binary -hmac <key> | base64 control_signature = "Uau4O9Kpd2k6rvh7UZN/RN+RG7Y=" self.assertRaises(ValueError, sign_hmac_sha1, self.control_base_string, self.client_secret, self.resource_owner_secret) sign = sign_hmac_sha1(self.control_base_string, self.client_secret.decode('utf-8'), self.resource_owner_secret.decode('utf-8')) self.assertEquals(len(sign), 28) self.assertEquals(sign, control_signature)
def _get_validated_lti_params_from_values(cls, request, current_time, lti_consumer_valid, lti_consumer_secret, lti_max_timestamp_age): """ Validates LTI signature and returns LTI parameters """ # Taking a cue from oauthlib, to avoid leaking information through a timing attack, # we proceed through the entire validation before rejecting any request for any reason. # However, as noted there, the value of doing this is dubious. try: base_uri = normalize_base_string_uri(request.uri) parameters = collect_parameters(uri_query=request.uri_query, body=request.body) parameters_string = normalize_parameters(parameters) base_string = construct_base_string(request.http_method, base_uri, parameters_string) computed_signature = sign_hmac_sha1(base_string, unicode(lti_consumer_secret), '') submitted_signature = request.oauth_signature data = {parameter_value_pair[0]: parameter_value_pair[1] for parameter_value_pair in parameters} def safe_int(value): """ Interprets parameter as an int or returns 0 if not possible """ try: return int(value) except (ValueError, TypeError): return 0 oauth_timestamp = safe_int(request.oauth_timestamp) # As this must take constant time, do not use shortcutting operators such as 'and'. # Instead, use constant time operators such as '&', which is the bitwise and. valid = (lti_consumer_valid) valid = valid & (submitted_signature == computed_signature) valid = valid & (request.oauth_version == '1.0') valid = valid & (request.oauth_signature_method == 'HMAC-SHA1') valid = valid & ('user_id' in data) # Not required by LTI but can't log in without one valid = valid & (oauth_timestamp >= current_time - lti_max_timestamp_age) valid = valid & (oauth_timestamp <= current_time) if valid: return data except AttributeError as error: log.error("'{}' not found.".format(error.message)) return None
def getVRVSignature(key, secret, timestamp, nonce): headers = { "Authorization": 'OAuth oauth_consumer_key="' + key + '",oauth_signature_method="HMAC-SHA1",oauth_timestamp="' + timestamp + '",oauth_nonce="' + nonce + '",oauth_version="1.0"' } params = oauth.collect_parameters(uri_query="", body=[], headers=headers, exclude_oauth_signature=True, with_realm=False) norm_params = oauth.normalize_parameters(params) base_string = oauth.construct_base_string("GET", "https://api.vrv.co/core/index", norm_params) sig = oauth.sign_hmac_sha1(base_string, secret, '') return parse.quote(sig)
def make_args(consumer_key, consumer_secret, launch_url, oauth_timestamp, oauth_nonce, extra_args): args = { 'oauth_consumer_key': consumer_key, 'oauth_timestamp': str(oauth_timestamp), 'oauth_nonce': oauth_nonce } args.update(extra_args) base_string = signature.construct_base_string( 'POST', signature.normalize_base_string_uri(launch_url), signature.normalize_parameters( signature.collect_parameters(body=args, headers={}))) args['oauth_signature'] = signature.sign_hmac_sha1(base_string, consumer_secret, None) return args
def sign(self, url, method=u'POST', signature_method=u'HMAC-SHA1'): """ Use this method to create a signature over the authorization header, and the url query parameters :param url: request url :param method: request method (i.e. POST, GET) :param signature_method: method of signature. Only supports (HMAC-SHA1, PLAINTEXT) Note: that HMAC-SHA1 is required by Yahoo API queries since they are sent insecurely """ # could change this to support additional methods # for now only support HMAC-SHA1 and PLAINTEXT (Yahoo only supports these) self.add_param(u'oauth_signature_method', signature_method) self.add_param(u'oauth_nonce', generate_nonce()) self.add_param(u'oauth_timestamp', generate_timestamp()) if signature_method == u'HMAC-SHA1': base_string = construct_base_string(unicode(method), normalize_base_string_uri(unicode(url)), normalize_parameters(self.params)) signature = sign_hmac_sha1(base_string, self.client_secret, self.oauth_token_secret) else: signature = quote(self.client_secret + u'&' + self.oauth_token_secret) self.add_param(u'oauth_signature', signature)
def make_args(consumer_key, consumer_secret, launch_url, oauth_timestamp, oauth_nonce, extra_args): args = { "oauth_consumer_key": consumer_key, "oauth_timestamp": str(oauth_timestamp), "oauth_nonce": oauth_nonce, } args.update(extra_args) base_string = signature.signature_base_string( "POST", signature.base_string_uri(launch_url), signature.normalize_parameters( signature.collect_parameters(body=args, headers={})), ) args["oauth_signature"] = signature.sign_hmac_sha1(base_string, consumer_secret, None) return args
async def lti_login_data(session, log, hub_url, username, consumer_key, consumer_secret, launch_url, extra_args={}): """ Log in username with LTI info to hub_url log is used to emit timing and status information. """ args = { 'oauth_consumer_key': consumer_key, 'oauth_timestamp': str(time.time()), 'oauth_nonce': str(uuid.uuid4()), 'user_id': username } args.update(extra_args) base_string = signature.construct_base_string( 'POST', signature.normalize_base_string_uri(launch_url), signature.normalize_parameters( signature.collect_parameters(body=args, headers={}) ) ) args['oauth_signature'] = signature.sign_hmac_sha1(base_string, consumer_secret, None) url = hub_url / 'lti/launch' try: resp = await session.post(url, data=args, allow_redirects=False) except Exception as e: log.msg('Login: Failed with exception {}'.format(repr(e)), action='login', phase='failed', duration=time.monotonic() - start_time) raise OperationError() if resp.status != 302: log.msg('Login: Failed with response {}'.format(str(resp)), action='login', phase='failed', duration=time.monotonic() - start_time) raise OperationError() return args
def validate_launch_request( self, launch_url: str, headers: Dict[str, Any], args: Dict[str, Any], ) -> bool: """ Validate a given LTI 1.1 launch request. The arguments' k/v's are either required, recommended, or optional. The required/recommended/optional keys are defined as constants. Args: launch_url: URL (base_url + path) that receives the launch request, usually from a tool consumer. headers: HTTP headers included with the POST request args: the body sent to the launch url. Returns: True if the validation passes, False otherwise. Raises: HTTPError if a required argument is not inclued in the POST request. """ # Ensure that required oauth_* body arguments are included in the request for param in LTI11_OAUTH_ARGS: if param not in args.keys(): raise HTTPError( 400, f"Required oauth arg {param} not included in request") if not args.get(param): raise HTTPError( 400, f"Required oauth arg {param} does not have a value") # Ensure that consumer key is registered in in jupyterhub_config.py # LTI11Authenticator.consumers defined in parent class if args["oauth_consumer_key"] not in self.consumers: raise HTTPError(401, "unknown oauth_consumer_key") # Ensure that required LTI 1.1 body arguments are included in the request for param in LTI11_LAUNCH_PARAMS_REQUIRED: if param not in args.keys(): raise HTTPError( 400, f"Required LTI 1.1 arg arg {param} not included in request" ) if not args.get(param): raise HTTPError( 400, f"Required LTI 1.1 arg {param} does not have a value") # Inspiration to validate nonces/timestamps from OAuthlib # https://github.com/oauthlib/oauthlib/blob/HEAD/oauthlib/oauth1/rfc5849/endpoints/base.py#L147 if len(str(int(args["oauth_timestamp"]))) != 10: raise HTTPError(401, "Invalid timestamp format.") try: ts = int(args["oauth_timestamp"]) except ValueError: raise HTTPError(401, "Timestamp must be an integer.") else: # Reject timestamps that are older than 30 seconds if abs(time.time() - ts) > 30: raise HTTPError( 401, "Timestamp given is invalid, differ from " f"allowed by over {int(time.time() - ts)} seconds.", ) if (ts in LTI11LaunchValidator.nonces and args["oauth_nonce"] in LTI11LaunchValidator.nonces[ts]): raise HTTPError(401, "oauth_nonce + oauth_timestamp already used") LTI11LaunchValidator.nonces.setdefault(ts, set()).add( args["oauth_nonce"]) # convert arguments dict back to a list of tuples for signature args_list = [(k, v) for k, v in args.items()] base_string = signature.signature_base_string( "POST", signature.base_string_uri(launch_url), signature.normalize_parameters( signature.collect_parameters(body=args_list, headers=headers)), ) consumer_secret = self.consumers[args["oauth_consumer_key"]] sign = signature.sign_hmac_sha1(base_string, consumer_secret, None) is_valid = signature.safe_string_equals(sign, args["oauth_signature"]) self.log.debug(f"signature in request: {args['oauth_signature']}") self.log.debug(f"calculated signature: {sign}") if not is_valid: raise HTTPError(401, "Invalid oauth_signature") return True
async def post_grade(user_id, grade, sourcedid, outcomes_url): # TODO: extract this into a real library with real XML parsing # WARNING: You can use this only with data you trust! Beware, etc. post_xml = r""" <?xml version = "1.0" encoding = "UTF-8"?> <imsx_POXEnvelopeRequest xmlns = "http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0"> <imsx_POXHeader> <imsx_POXRequestHeaderInfo> <imsx_version>V1.0</imsx_version> <imsx_messageIdentifier>999999123</imsx_messageIdentifier> </imsx_POXRequestHeaderInfo> </imsx_POXHeader> <imsx_POXBody> <replaceResultRequest> <resultRecord> <sourcedGUID> <sourcedId>{sourcedid}</sourcedId> </sourcedGUID> <result> <resultScore> <language>en</language> <textString>{grade}</textString> </resultScore> </result> </resultRecord> </replaceResultRequest> </imsx_POXBody> </imsx_POXEnvelopeRequest> """ # Assumes these are read in from a config file in Jupyterhub consumer_key = os.environ['LTI_CONSUMER_KEY'] consumer_secret = os.environ['LTI_CONSUMER_SECRET'] sourcedid = "{}:{}".format(sourcedid, user_id) post_data = post_xml.format(grade=float(grade), sourcedid=sourcedid) # Yes, we do have to use sha1 :( body_hash_sha = sha1() body_hash_sha.update(post_data.encode('utf-8')) body_hash = base64.b64encode(body_hash_sha.digest()).decode('utf-8') args = { 'oauth_body_hash': body_hash, 'oauth_consumer_key': consumer_key, 'oauth_timestamp': str(time.time()), 'oauth_nonce': str(time.time()) } base_string = signature.construct_base_string( 'POST', signature.normalize_base_string_uri(outcomes_url), signature.normalize_parameters( signature.collect_parameters(body=args, headers={}))) oauth_signature = signature.sign_hmac_sha1(base_string, consumer_secret, None) args['oauth_signature'] = oauth_signature headers = parameters.prepare_headers( args, headers={'Content-Type': 'application/xml'}) async with async_timeout.timeout(10): async with aiohttp.ClientSession() as session: async with session.post(outcomes_url, data=post_data, headers=headers) as response: resp_text = await response.text() if response.status != 200: raise GradePostException(response) response_tree = etree.fromstring(resp_text.encode('utf-8')) # XML and its namespaces. UBOOF! status_tree = response_tree.find( './/{http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0}imsx_statusInfo' ) code_major = status_tree.find( '{http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0}imsx_codeMajor' ).text if code_major != 'success': raise GradePostException(response)
def post_grade(sourcedid, outcomes_url, consumer_key, consumer_secret, grade): # Who is treating XML as Text? I am! # WHY WOULD YOU MIX MULTIPART, XML (NOT EVEN JUST XML, BUT WSDL GENERATED POX WTF), AND PARTS OF OAUTH1 SIGNING # IN THE SAME STANDARD AAAA! # TODO: extract this into a real library with real XML parsing # WARNING: You can use this only with data you trust! Beware, etc. post_xml = r""" <?xml version = "1.0" encoding = "UTF-8"?> <imsx_POXEnvelopeRequest xmlns = "http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0"> <imsx_POXHeader> <imsx_POXRequestHeaderInfo> <imsx_version>V1.0</imsx_version> <imsx_messageIdentifier>999999123</imsx_messageIdentifier> </imsx_POXRequestHeaderInfo> </imsx_POXHeader> <imsx_POXBody> <replaceResultRequest> <resultRecord> <sourcedGUID> <sourcedId>{sourcedid}</sourcedId> </sourcedGUID> <result> <resultScore> <language>en</language> <textString>{grade}</textString> </resultScore> </result> </resultRecord> </replaceResultRequest> </imsx_POXBody> </imsx_POXEnvelopeRequest> """ post_data = post_xml.format(grade=float(grade), sourcedid=sourcedid) # Yes, we do have to use sha1 :( body_hash_sha = sha1() body_hash_sha.update(post_data.encode('utf-8')) body_hash = base64.b64encode(body_hash_sha.digest()).decode('utf-8') args = { 'oauth_body_hash': body_hash, 'oauth_consumer_key': consumer_key, 'oauth_timestamp': str(time.time()), 'oauth_nonce': str(time.time()) } base_string = signature.construct_base_string( 'POST', signature.normalize_base_string_uri(outcomes_url), signature.normalize_parameters( signature.collect_parameters(body=args, headers={}) ) ) oauth_signature = signature.sign_hmac_sha1(base_string, consumer_secret, None) args['oauth_signature'] = oauth_signature headers = parameters.prepare_headers(args, headers={ 'Content-Type': 'application/xml' }) resp = requests.post(outcomes_url, data=post_data, headers=headers) if resp.status_code != 200: raise GradePostException(resp) response_tree = etree.fromstring(resp.text.encode('utf-8')) # XML and its namespaces. UBOOF! status_tree = response_tree.find('.//{http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0}imsx_statusInfo') code_major = status_tree.find('{http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0}imsx_codeMajor').text if code_major != 'success': raise GradePostException(resp)
def validate_launch_request(self, launch_url, headers, args): """ Validate a given launch request launch_url: Full URL that the launch request was POSTed to headers: k/v pair of HTTP headers coming in with the POST args: dictionary of body arguments passed to the launch_url Must have the following keys to be valid: oauth_consumer_key, oauth_timestamp, oauth_nonce, oauth_signature """ # Validate args! if 'oauth_consumer_key' not in args: raise web.HTTPError(401, "oauth_consumer_key missing") if args['oauth_consumer_key'] not in self.consumers: raise web.HTTPError(401, "oauth_consumer_key not known") if 'oauth_signature' not in args: raise web.HTTPError(401, "oauth_signature missing") if 'oauth_timestamp' not in args: raise web.HTTPError(401, 'oauth_timestamp missing') # Allow 30s clock skew between LTI Consumer and Provider # Also don't accept timestamps from before our process started, since that could be # a replay attack - we won't have nonce lists from back then. This would allow users # who can control / know when our process restarts to trivially do replay attacks. oauth_timestamp = int(float(args['oauth_timestamp'])) if (int(time.time()) - oauth_timestamp > 30 or oauth_timestamp < LTILaunchValidator.PROCESS_START_TIME): raise web.HTTPError(401, "oauth_timestamp too old") if 'oauth_nonce' not in args: raise web.HTTPError(401, 'oauth_nonce missing') if (oauth_timestamp in LTILaunchValidator.nonces and args['oauth_nonce'] in LTILaunchValidator.nonces[oauth_timestamp]): raise web.HTTPError(401, "oauth_nonce + oauth_timestamp already used") LTILaunchValidator.nonces.setdefault(oauth_timestamp, set()).add(args['oauth_nonce']) args_list = [] for key, values in args.items(): if type(values) is list: args_list += [(key, value) for value in values] else: args_list.append((key, values)) base_string = signature.signature_base_string( 'POST', signature.base_string_uri(launch_url), signature.normalize_parameters( signature.collect_parameters(body=args_list, headers=headers))) consumer_secret = self.consumers[args['oauth_consumer_key']] sign = signature.sign_hmac_sha1(base_string, consumer_secret, None) is_valid = signature.safe_string_equals(sign, args['oauth_signature']) if not is_valid: raise web.HTTPError(401, "Invalid oauth_signature") return True
def _make_lti11_basic_launch_args( roles: str = "Instructor", ext_roles: str = "urn:lti:instrole:ims/lis/Instructor", lms_vendor: str = "canvas", oauth_consumer_key: str = "my_consumer_key", oauth_consumer_secret: str = "my_shared_secret", ): oauth_timestamp = str(int(time.time())) oauth_nonce = secrets.token_urlsafe(32) args = { "oauth_callback": "about:blank", "oauth_consumer_key": oauth_consumer_key, "oauth_timestamp": str(int(oauth_timestamp)), "oauth_nonce": str(oauth_nonce), "oauth_signature_method": "HMAC-SHA1", "oauth_version": "1.0", "context_id": "888efe72d4bbbdf90619353bb8ab5965ccbe9b3f", "context_label": "Introduction to Data Science", "context_title": "Introduction101", "course_lineitems": "https://canvas.instructure.com/api/lti/courses/1/line_items", "custom_canvas_assignment_title": "test-assignment", "custom_canvas_course_id": "616", "custom_canvas_enrollment_state": "active", "custom_canvas_user_id": "1091", "custom_canvas_user_login_id": "*****@*****.**", "ext_roles": ext_roles, "launch_presentation_document_target": "iframe", "launch_presentation_height": "1000", "launch_presentation_locale": "en", "launch_presentation_return_url": "https://canvas.instructure.com/courses/161/external_content/success/external_tool_redirect", "launch_presentation_width": "1000", "lis_outcome_service_url": "http://www.imsglobal.org/developers/LTI/test/v1p1/common/tool_consumer_outcome.php?b64=MTIzNDU6OjpzZWNyZXQ=", "lis_person_contact_email_primary": "*****@*****.**", "lis_person_name_family": "Bar", "lis_person_name_full": "Foo Bar", "lis_person_name_given": "Foo", "lti_message_type": "basic-lti-launch-request", "lis_result_sourcedid": "feb-123-456-2929::28883", "lti_version": "LTI-1p0", "resource_link_id": "888efe72d4bbbdf90619353bb8ab5965ccbe9b3f", "resource_link_title": "Test-Assignment", "roles": roles, "tool_consumer_info_product_family_code": lms_vendor, "tool_consumer_info_version": "cloud", "tool_consumer_instance_contact_email": "*****@*****.**", "tool_consumer_instance_guid": "srnuz6h1U8kOMmETzoqZTJiPWzbPXIYkAUnnAJ4u:test-lms", "tool_consumer_instance_name": "myedutool", "user_id": "185d6c59731a553009ca9b59ca3a885100000", "user_image": "https://lms.example.com/avatar-50.png", } extra_args = {"my_key": "this_value"} headers = {"Content-Type": "application/x-www-form-urlencoded"} launch_url = "http://jupyterhub/hub/lti/launch" args.update(extra_args) base_string = signature.signature_base_string( "POST", signature.base_string_uri(launch_url), signature.normalize_parameters( signature.collect_parameters(body=args, headers=headers) ), ) args["oauth_signature"] = signature.sign_hmac_sha1( base_string, oauth_consumer_secret, None ) return args