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 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