Пример #1
0
    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
Пример #2
0
    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)
Пример #3
0
    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
Пример #4
0
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)
Пример #5
0
    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
Пример #6
0
    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)
Пример #7
0
    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)
Пример #8
0
    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 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)
Пример #10
0
    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
Пример #11
0
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)
Пример #12
0
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
Пример #13
0
 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
Пример #15
0
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
Пример #16
0
    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
Пример #17
0
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)
Пример #18
0
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)
Пример #19
0
    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
Пример #20
0
    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