def test_dubious_extra_args(): consumer_key = "key1" consumer_secret = "secret1" launch_url = "http://localhost:8000/hub/lti/launch" headers = {} oauth_timestamp = time.time() oauth_nonce = str(time.time()) extra_args = {"arg1": "value1"} args = make_args( consumer_key, consumer_secret, launch_url, oauth_timestamp, oauth_nonce, extra_args, ) validator = LTI11LaunchValidator({consumer_key: consumer_secret}) assert validator.validate_launch_request(launch_url, headers, args) args["extra_credential"] = "i have admin powers" with pytest.raises(web.HTTPError): validator.validate_launch_request(launch_url, headers, args)
def test_partial_replay_timestamp(): consumer_key = "key1" consumer_secret = "secret1" launch_url = "http://localhost:8000/hub/lti/launch" headers = {} oauth_timestamp = time.time() oauth_nonce = str(time.time()) extra_args = {"arg1": "value1"} args = make_args( consumer_key, consumer_secret, launch_url, oauth_timestamp, oauth_nonce, extra_args, ) validator = LTI11LaunchValidator({consumer_key: consumer_secret}) assert validator.validate_launch_request(launch_url, headers, args) args["oauth_timestamp"] = str(int(float(args["oauth_timestamp"])) - 1) with pytest.raises(web.HTTPError): validator.validate_launch_request(launch_url, headers, args)
def authenticate(self, handler, data) -> dict: # FIXME: Run a process that cleans up old nonces every other minute validator = LTI11LaunchValidator(self.consumers) args = {} for k, values in handler.request.body_arguments.items(): args[k] = ( values[0].decode() if len(values) == 1 else [v.decode() for v in values] ) # handle multiple layers of proxied protocol (comma separated) and take the outermost # value (first from the list) if "x-forwarded-proto" in handler.request.headers: # x-forwarded-proto might contain comma delimited values # left-most value is the one sent by original client hops = [ h.strip() for h in handler.request.headers["x-forwarded-proto"].split(",") ] protocol = hops[0] else: protocol = handler.request.protocol launch_url = protocol + "://" + handler.request.host + handler.request.uri if validator.validate_launch_request(launch_url, handler.request.headers, args): # Before we return lti_user_id, check to see if a canvas_custom_user_id was sent. # If so, this indicates two things: # 1. The request was sent from Canvas, not edX # 2. The request was sent from a Canvas course not running in anonymous mode # If this is the case we want to use the canvas ID to allow grade returns through the Canvas API # If Canvas is running in anonymous mode, we'll still want the 'user_id' (which is the `lti_user_id``) canvas_id = handler.get_body_argument("custom_canvas_user_id", default=None) if canvas_id is not None: user_id = handler.get_body_argument("custom_canvas_user_id") else: user_id = handler.get_body_argument("user_id") return { "name": user_id, "auth_state": { k: v for k, v in args.items() if not k.startswith("oauth_") }, }
def test_basic_lti11_launch_request(make_lti11_basic_launch_request_args): """ Does a standard launch request work? """ oauth_consumer_key = "my_consumer_key" oauth_consumer_secret = "my_shared_secret" launch_url = "http://jupyterhub/hub/lti/launch" headers = {"Content-Type": "application/x-www-form-urlencoded"} args = make_lti11_basic_launch_request_args( oauth_consumer_key, oauth_consumer_secret, ) validator = LTI11LaunchValidator( {oauth_consumer_key: oauth_consumer_secret}) assert validator.validate_launch_request(launch_url, headers, args)
def test_unregistered_shared_secret(make_lti11_basic_launch_request_args): """ Does the launch request work with a shared secret that does not match? """ oauth_consumer_key = "my_consumer_key" oauth_consumer_secret = "my_shared_secret" launch_url = "http://jupyterhub/hub/lti/launch" headers = {"Content-Type": "application/x-www-form-urlencoded"} args = make_lti11_basic_launch_request_args( oauth_consumer_key, oauth_consumer_secret, ) validator = LTI11LaunchValidator( {oauth_consumer_key: "my_other_shared_secret"}) with pytest.raises(HTTPError): validator.validate_launch_request(launch_url, headers, args)
def test_launch_with_same_oauth_nonce_different_oauth_timestamp( make_lti11_basic_launch_request_args, ): """ Does the launch request pass with when using a different timestamp with the same nonce? """ oauth_consumer_key = "my_consumer_key" oauth_consumer_secret = "my_shared_secret" launch_url = "http://jupyterhub/hub/lti/launch" headers = {"Content-Type": "application/x-www-form-urlencoded"} args = make_lti11_basic_launch_request_args(oauth_consumer_key, oauth_consumer_secret) validator = LTI11LaunchValidator( {oauth_consumer_key: oauth_consumer_secret}) with pytest.raises(HTTPError): args["oauth_timestamp"] = "0123456789" validator.validate_launch_request(launch_url, headers, args)
def test_launch_with_empty_user_id_value(make_lti11_basic_launch_request_args): """ Does the launch request work with an empty user_id value? """ oauth_consumer_key = "my_consumer_key" oauth_consumer_secret = "my_shared_secret" launch_url = "http://jupyterhub/hub/lti/launch" headers = {"Content-Type": "application/x-www-form-urlencoded"} args = make_lti11_basic_launch_request_args( oauth_consumer_key, oauth_consumer_secret, ) validator = LTI11LaunchValidator( {oauth_consumer_key: oauth_consumer_secret}) with pytest.raises(HTTPError): args["user_id"] = "" validator.validate_launch_request(launch_url, headers, args)
def test_launch_with_missing_oauth_signature_method_key( make_lti11_basic_launch_request_args, ): """ Does the launch request work with a missing oauth_signature_method key? """ oauth_consumer_key = "my_consumer_key" oauth_consumer_secret = "my_shared_secret" launch_url = "http://jupyterhub/hub/lti/launch" headers = {"Content-Type": "application/x-www-form-urlencoded"} args = make_lti11_basic_launch_request_args(oauth_consumer_key, oauth_consumer_secret) del args["oauth_signature_method"] validator = LTI11LaunchValidator( {oauth_consumer_key: oauth_consumer_secret}) with pytest.raises(HTTPError): validator.validate_launch_request(launch_url, headers, args)
def test_launch(): consumer_key = "key1" consumer_secret = "secret1" launch_url = "http://localhost:8000/hub/lti/launch" headers = {} oauth_timestamp = time.time() oauth_nonce = str(time.time()) extra_args = {"arg1": "value1"} args = make_args( consumer_key, consumer_secret, launch_url, oauth_timestamp, oauth_nonce, extra_args, ) validator = LTI11LaunchValidator({consumer_key: consumer_secret}) assert validator.validate_launch_request(launch_url, headers, args)
def test_launch_with_fake_oauth_consumer_key_value( make_lti11_basic_launch_request_args, ): """ Does the launch request work when the consumer_key isn't correct? """ oauth_consumer_key = "my_consumer_key" oauth_consumer_secret = "my_shared_secret" launch_url = "http://jupyterhub/hub/lti/launch" headers = {"Content-Type": "application/x-www-form-urlencoded"} args = make_lti11_basic_launch_request_args( oauth_consumer_key, oauth_consumer_secret, ) validator = LTI11LaunchValidator( {oauth_consumer_key: oauth_consumer_secret}) with pytest.raises(HTTPError): args["oauth_consumer_key"] = [b"fake_consumer_key"][0].decode("utf-8") assert validator.validate_launch_request(launch_url, headers, args)
def test_wrong_secret(): consumer_key = "key1" consumer_secret = "secret1" launch_url = "http://localhost:8000/hub/lti/launch" headers = {} oauth_timestamp = time.time() oauth_nonce = str(time.time()) extra_args = {"arg1": "value1"} args = make_args( consumer_key, consumer_secret, launch_url, oauth_timestamp, oauth_nonce, extra_args, ) validator = LTI11LaunchValidator({consumer_key: "wrongsecret"}) with pytest.raises(web.HTTPError): validator.validate_launch_request(launch_url, headers, args)
async def authenticate( # noqa: C901 self, handler: BaseHandler, data: dict = None) -> dict: # noqa: C901 """ LTI 1.1 Authenticator. One or more consumer keys/values must be set in the jupyterhub config with the LTI11Authenticator.consumers dict. Args: handler: JupyterHub's Authenticator handler object. For LTI 1.1 requests, the handler is an instance of LTIAuthenticateHandler. data: optional data object Returns: Authentication dictionary Raises: HTTPError if the required values are not in the request """ # log deprecation warning when using the default custom_canvas_user_id setting if self.username_key == "custom_canvas_user_id": self.log.warning( dedent( """The default username_key 'custom_canvas_user_id' will be replaced by 'user_id' in a future release. Set c.LTIAuthenticator.username_key to `custom_canvas_user_id` to preserve current behavior. """)) validator = LTI11LaunchValidator(self.consumers) self.log.debug("Original arguments received in request: %s" % handler.request.arguments) # extract the request arguments to a dict args = convert_request_to_dict(handler.request.arguments) self.log.debug("Decoded args from request: %s" % args) # get the origin protocol protocol = get_client_protocol(handler) self.log.debug("Origin protocol is: %s" % protocol) # build the full launch url value required for oauth1 signatures launch_url = f"{protocol}://{handler.request.host}{handler.request.uri}" self.log.debug("Launch url is: %s" % launch_url) if validator.validate_launch_request(launch_url, handler.request.headers, args): # raise an http error if the username_key is not in the request's arguments. if self.username_key not in args.keys(): self.log.warning( "%s the specified username_key did not match any of the launch request arguments." ) # get the username_key. if empty, fetch the username from the request's user_id value. username = args.get(self.username_key) if not username: username = args.get("user_id") # if username is still empty or none, raise an http error. if not username: raise HTTPError( 400, "The %s value in the launch request is empty or None." % self.username_key, ) # return standard authentication where all launch request arguments are added to the auth_state key # except for the oauth_* arguments. return { "name": username, "auth_state": {k: v for k, v in args.items() if not k.startswith("oauth_")}, }