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)
Example #3
0
    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_")
                },
            }
Example #4
0
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)
Example #5
0
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)
Example #6
0
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)
Example #7
0
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)
Example #8
0
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)
Example #10
0
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)
Example #12
0
    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_")},
            }