Esempio n. 1
0
    def collect_request_parameters(self, request):
        """Collect parameters in an object for convenient access"""

        class OAuthParameters(object):
            """Used as a parameter container since plain object()s can't"""
            pass

        # Collect parameters
        query = urlparse(request.url.decode("utf-8")).query
        if request.form:
            body = request.form.to_dict()
        else:
            body = request.data.decode("utf-8")
        headers = dict(encode_params_utf8(request.headers.items()))
        params = dict(collect_parameters(uri_query=query, body=body, headers=headers, with_realm=True))

        # Extract params and store for convenient and predictable access
        oauth_params = OAuthParameters()
        oauth_params.client_key = params.get(u'oauth_consumer_key')
        oauth_params.resource_owner_key = params.get(u'oauth_token', None)
        oauth_params.nonce = params.get(u'oauth_nonce')
        oauth_params.timestamp = params.get(u'oauth_timestamp')
        oauth_params.verifier = params.get(u'oauth_verifier', None)
        oauth_params.callback_uri = params.get(u'oauth_callback', None)
        oauth_params.realm = params.get(u'realm', None)
        return oauth_params
Esempio n. 2
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)
Esempio n. 3
0
def request_request_token(request):
    """
    Checks provided client key and secret and assigns request token if all goes well.
    """
    server = DjangoOAuthServer()

    if request.method == 'POST':
        try:
            is_request_valid = server.verify_request_token_request(uri=request.build_absolute_uri(), headers=request.META)[0]
        except ValueError as exception:
            return HttpResponse(exception, status=400)
        else:
            if not is_request_valid:
                return HttpResponse(status=401)

        headers = collect_parameters(headers=to_unicode(request.META, 'utf-8'))
        client_key = dict(headers)[u'oauth_consumer_key']
        client = Client.objects.get(key=client_key)

        token = Token.objects.create_request_token(client)

        token_response = urlencode((('oauth_token', token.key), ('oauth_token_secret', token.secret)))

        return HttpResponse(token_response, status=200, content_type='application/x-www-form-urlencoded')

    return HttpResponseNotAllowed('POST', _('Only POST is allowed for this URI.'))
    def collect_request_parameters(self, request):
        """Collect parameters in an object for convenient access"""
        class OAuthParameters(object):
            """Used as a parameter container since plain object()s can't"""
            pass

        # Collect parameters
        query = urlparse(request.url.decode("utf-8")).query
        content_type = request.headers.get('Content-Type', '')
        if request.form:
            body = request.form.to_dict()
        elif content_type == 'application/x-www-form-urlencoded':
            body = request.data.decode("utf-8")
        else:
            body = ''
        headers = dict(encode_params_utf8(request.headers.items()))
        params = dict(
            collect_parameters(uri_query=query, body=body, headers=headers))

        # Extract params and store for convenient and predictable access
        oauth_params = OAuthParameters()
        oauth_params.client_key = params.get(u'oauth_consumer_key')
        oauth_params.resource_owner_key = params.get(u'oauth_token', None)
        oauth_params.nonce = params.get(u'oauth_nonce')
        oauth_params.timestamp = params.get(u'oauth_timestamp')
        oauth_params.verifier = params.get(u'oauth_verifier', None)
        oauth_params.callback_uri = params.get(u'oauth_callback', None)
        oauth_params.realm = params.get(u'realm', None)
        return oauth_params
Esempio n. 5
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
    def validate_missing_parameters(self, request, parameters=None):
        parameters = parameters or []
        """ Ensures that the request contains all required parameters. """
        params = [
            "oauth_consumer_key",
            "oauth_nonce",
            "oauth_signature",
            "oauth_signature_method",
            "oauth_timestamp",
        ]

        params.extend(parameters)

        collected_request_parameters = dict(
            signature.collect_parameters(
                uri_query=request.GET.urlencode(),
                body=request.POST.dict(),
                headers=request.META,
                exclude_oauth_signature=False,
            ))
        try:
            missing = list(param for param in params
                           if param not in collected_request_parameters)
        except Exception:  # pragma: nocover
            missing = params

        if missing:
            error_message = "parameter_absent:{}".format(",".join(missing))
            logger.error(error_message)
            missing_param_info = oauth_errors.build_error(error_message)
            request.auth_header = getattr(missing_param_info, "auth_header",
                                          None)
            return missing_param_info
        else:
            return True
Esempio n. 7
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
Esempio n. 8
0
    def check_oauth(self):
        """
        Checks whether the request passes Oauth signature check
        :return valid_oauth
        """
        resp = dict(self.__httprequest.POST.dict())
        orderedresp = OrderedDict(sorted(resp.items(), key=lambda t: t[0]))
        query_string = urllib.urlencode(orderedresp)
        oauth_headers = dict(
            signature.collect_parameters(query_string,
                                         exclude_oauth_signature=False))
        sig = oauth_headers.pop('oauth_signature')
        consumer_secret = self.get_oauthsecret_for_key(
            orderedresp.get('oauth_consumer_key'))

        oauthrequest = Oauthrequest()
        oauthrequest.params = oauth_headers.items()
        oauthrequest.uri = unicode(
            urllib.unquote(self.__httprequest.build_absolute_uri()))
        oauthrequest.http_method = unicode('POST')
        oauthrequest.signature = sig
        if signature.verify_hmac_sha1(request=oauthrequest,
                                      client_secret=unicode(consumer_secret)):
            return True
        return False
Esempio n. 9
0
    def verify_oauth_body_sign(self,
                               request,
                               content_type='application/x-www-form-urlencoded'
                               ):
        """
        Verify grade request from LTI provider using OAuth body signing.

        Uses http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html::

            This specification extends the OAuth signature to include integrity checks on HTTP request bodies
            with content types other than application/x-www-form-urlencoded.

        Arguments:
            request: DjangoWebobRequest.

        Raises:
            LTIError if request is incorrect.
        """

        client_key, client_secret = self.get_client_key_secret()
        headers = {
            'Authorization':
            six.text_type(request.headers.get('Authorization')),
            'Content-Type': content_type,
        }

        sha1 = hashlib.sha1()
        sha1.update(request.body)
        oauth_body_hash = base64.b64encode(sha1.digest())
        oauth_params = signature.collect_parameters(
            headers=headers, exclude_oauth_signature=False)
        oauth_headers = dict(oauth_params)
        oauth_signature = oauth_headers.pop('oauth_signature')
        mock_request_lti_1 = mock.Mock(uri=six.text_type(
            six.moves.urllib.parse.unquote(self.get_outcome_service_url())),
                                       http_method=six.text_type(
                                           request.method),
                                       params=list(oauth_headers.items()),
                                       signature=oauth_signature)
        mock_request_lti_2 = mock.Mock(
            uri=six.text_type(six.moves.urllib.parse.unquote(request.url)),
            http_method=six.text_type(request.method),
            params=list(oauth_headers.items()),
            signature=oauth_signature)
        if oauth_body_hash != oauth_headers.get('oauth_body_hash'):
            log.error("OAuth body hash verification failed, provided: {}, "
                      "calculated: {}, for url: {}, body is: {}".format(
                          oauth_headers.get('oauth_body_hash'),
                          oauth_body_hash, self.get_outcome_service_url(),
                          request.body))
            raise LTIError("OAuth body hash verification is failed.")

        if (not signature.verify_hmac_sha1(mock_request_lti_1, client_secret)
                and not signature.verify_hmac_sha1(mock_request_lti_2,
                                                   client_secret)):
            log.error("OAuth signature verification failed, for "
                      "headers:{} url:{} method:{}".format(
                          oauth_headers, self.get_outcome_service_url(),
                          six.text_type(request.method)))
            raise LTIError("OAuth signature verification has failed.")
Esempio n. 10
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)
Esempio n. 11
0
    def test_collect_parameters(self):
        """
        Test the ``collect_parameters`` function.
        """

        # ----------------
        # Examples from the OAuth 1.0a specification: RFC 5849.

        params = collect_parameters(
            self.eg_uri_query,
            self.eg_body,
            {'Authorization': self.eg_authorization_header})

        # Check params contains the same pairs as control_params, ignoring order
        self.assertEqual(sorted(self.eg_params), sorted(params))

        # ----------------
        # Examples with no parameters

        self.assertEqual([], collect_parameters('', '', {}))

        self.assertEqual([], collect_parameters(None, None, None))

        self.assertEqual([], collect_parameters())

        self.assertEqual([], collect_parameters(headers={'foo': 'bar'}))

        # ----------------
        # Test effect of exclude_oauth_signature"

        no_sig = collect_parameters(
            headers={'authorization': self.eg_authorization_header})
        with_sig = collect_parameters(
            headers={'authorization': self.eg_authorization_header},
            exclude_oauth_signature=False)

        self.assertEqual(sorted(no_sig + [('oauth_signature',
                                           'djosJKDKJSD8743243/jdk33klY=')]),
                         sorted(with_sig))

        # ----------------
        # Test effect of "with_realm" as well as header name case insensitivity

        no_realm = collect_parameters(
                headers={'authorization': self.eg_authorization_header},
                with_realm=False)
        with_realm = collect_parameters(
                headers={'AUTHORIZATION': self.eg_authorization_header},
                with_realm=True)

        self.assertEqual(sorted(no_realm + [('realm', 'Example')]),
                         sorted(with_realm))
Esempio n. 12
0
    def test_collect_parameters(self):
        """We check against parameters multiple times in case things change
        after more parameters are added.
        """
        self.assertEqual(collect_parameters(), [])

        # Check against uri_query
        parameters = collect_parameters(uri_query=self.uri_query)
        correct_parameters = [('b5', '=%3D'),
                              ('a3', 'a'),
                              ('c@', ''),
                              ('a2', 'r b'),
                              ('c2', ''),
                              ('a3', '2 q')]
        self.assertEqual(sorted(parameters), sorted(correct_parameters))

        headers = {'Authorization': self.authorization_header}
        # check against authorization header as well
        parameters = collect_parameters(
            uri_query=self.uri_query, headers=headers)
        parameters_with_realm = collect_parameters(
            uri_query=self.uri_query, headers=headers, with_realm=True)
        # Redo the checks against all the parameters. Duplicated code but
        # better safety
        correct_parameters += [
            ('oauth_nonce', '7d8f3e4a'),
            ('oauth_timestamp', '137131201'),
            ('oauth_consumer_key', '9djdj82h48djs9d2'),
            ('oauth_signature_method', 'HMAC-SHA1'),
            ('oauth_token', 'kkk9d7dh3k39sjv7')]
        correct_parameters_with_realm = (
            correct_parameters + [('realm', 'Example')])
        self.assertEqual(sorted(parameters), sorted(correct_parameters))
        self.assertEqual(sorted(parameters_with_realm),
                         sorted(correct_parameters_with_realm))

        # Add in the body.
        # TODO: Add more content for the body. Daniel Greenfeld 2012/03/12
        # Redo again the checks against all the parameters. Duplicated code
        # but better safety
        parameters = collect_parameters(
            uri_query=self.uri_query, body=self.body, headers=headers)
        correct_parameters += [
            ('content', 'This is being the body of things')]
        self.assertEqual(sorted(parameters), sorted(correct_parameters))
Esempio n. 13
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
Esempio n. 14
0
    def verify_oauth_body_sign(self, request, content_type="application/x-www-form-urlencoded"):
        """
        Verify grade request from LTI provider using OAuth body signing.

        Uses http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html::

            This specification extends the OAuth signature to include integrity checks on HTTP request bodies
            with content types other than application/x-www-form-urlencoded.

        Arguments:
            request: DjangoWebobRequest.

        Raises:
            LTIError if request is incorrect.
        """

        client_key, client_secret = self.get_client_key_secret()
        headers = {"Authorization": unicode(request.headers.get("Authorization")), "Content-Type": content_type}

        sha1 = hashlib.sha1()
        sha1.update(request.body)
        oauth_body_hash = base64.b64encode(sha1.digest())
        oauth_params = signature.collect_parameters(headers=headers, exclude_oauth_signature=False)
        oauth_headers = dict(oauth_params)
        oauth_signature = oauth_headers.pop("oauth_signature")
        mock_request_lti_1 = mock.Mock(
            uri=unicode(urllib.unquote(self.get_outcome_service_url())),
            http_method=unicode(request.method),
            params=oauth_headers.items(),
            signature=oauth_signature,
        )
        mock_request_lti_2 = mock.Mock(
            uri=unicode(urllib.unquote(request.url)),
            http_method=unicode(request.method),
            params=oauth_headers.items(),
            signature=oauth_signature,
        )
        if oauth_body_hash != oauth_headers.get("oauth_body_hash"):
            log.error(
                "OAuth body hash verification failed, provided: {}, "
                "calculated: {}, for url: {}, body is: {}".format(
                    oauth_headers.get("oauth_body_hash"), oauth_body_hash, self.get_outcome_service_url(), request.body
                )
            )
            raise LTIError("OAuth body hash verification is failed.")

        if not signature.verify_hmac_sha1(mock_request_lti_1, client_secret) and not signature.verify_hmac_sha1(
            mock_request_lti_2, client_secret
        ):
            log.error(
                "OAuth signature verification failed, for "
                "headers:{} url:{} method:{}".format(
                    oauth_headers, self.get_outcome_service_url(), unicode(request.method)
                )
            )
            raise LTIError("OAuth signature verification has failed.")
Esempio n. 15
0
    def test_signature_base_string(self):
        """
        Example text to be turned into a base string::

            POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b HTTP/1.1
            Host: example.com
            Content-Type: application/x-www-form-urlencoded
            Authorization: OAuth realm="Example",
                           oauth_consumer_key="9djdj82h48djs9d2",
                           oauth_token="kkk9d7dh3k39sjv7",
                           oauth_signature_method="HMAC-SHA1",
                           oauth_timestamp="137131201",
                           oauth_nonce="7d8f3e4a",
                           oauth_signature="bYT5CMsGcbgUdFHObYMEfcx6bsw%3D"
            c2&a3=2+q

        Sample Base string generated and tested against::
            POST&http%3A%2F%2Fexample.com%2Frequest&a2%3Dr%2520b%26a3%3D2%2520q
            %26a3%3Da%26b5%3D%253D%25253D%26c%2540%3D%26c2%3D%26oauth_consumer_
            key%3D9djdj82h48djs9d2%26oauth_nonce%3D7d8f3e4a%26oauth_signature_m
            ethod%3DHMAC-SHA1%26oauth_timestamp%3D137131201%26oauth_token%3Dkkk
            9d7dh3k39sjv7
        """

        self.assertRaises(ValueError, base_string_uri, self.base_string_url)
        base_string_url = base_string_uri(self.base_string_url.decode('utf-8'))
        base_string_url = base_string_url.encode('utf-8')
        querystring = self.base_string_url.split(b'?', 1)[1]
        query_params = collect_parameters(querystring.decode('utf-8'),
                                          body=self.body)
        normalized_encoded_query_params = sorted(
            [(quote(k), quote(v)) for k, v in query_params])
        normalized_request_string = "&".join(sorted(
            ['='.join((k, v)) for k, v in (
                        self.normalized_encoded_request_params +
                        normalized_encoded_query_params)
             if k.lower() != 'oauth_signature']))
        self.assertRaises(ValueError, signature_base_string,
                          self.http_method,
                          base_string_url,
                          normalized_request_string)
        self.assertRaises(ValueError, signature_base_string,
                          self.http_method.decode('utf-8'),
                          base_string_url,
                          normalized_request_string)

        base_string = signature_base_string(
            self.http_method.decode('utf-8'),
            base_string_url.decode('utf-8'),
            normalized_request_string
        )

        self.assertEqual(self.control_base_string, base_string)
Esempio n. 16
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
Esempio n. 17
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)
Esempio n. 18
0
    def verify_oauth_body_sign(self, request):
        """
        Verify grade request from LTI provider using OAuth body signing.

        Uses http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html::

            This specification extends the OAuth signature to include integrity checks on HTTP request bodies
            with content types other than application/x-www-form-urlencoded.

        Arguments:
            request: DjangoWebobRequest.

        Raises:
            LTIError if request is incorrect.
        """

        client_key, client_secret = self.get_client_key_secret()

        headers = {
            'Authorization':unicode(request.headers.get('Authorization')),
            'Content-Type': 'application/x-www-form-urlencoded',
        }

        sha1 = hashlib.sha1()
        sha1.update(request.body)
        oauth_body_hash = base64.b64encode(sha1.digest())

        oauth_params = signature.collect_parameters(headers=headers, exclude_oauth_signature=False)
        oauth_headers =dict(oauth_params)
        oauth_signature = oauth_headers.pop('oauth_signature')

        mock_request = mock.Mock(
            uri=unicode(urllib.unquote(request.url)),
            http_method=unicode(request.method),
            params=oauth_headers.items(),
            signature=oauth_signature
        )
        if oauth_body_hash != oauth_headers.get('oauth_body_hash'):
            log.debug("[LTI]: OAuth body hash verification is failed.")
            raise LTIError
        if not signature.verify_hmac_sha1(mock_request, client_secret):
            log.debug("[LTI]: OAuth signature verification is failed.")
            raise LTIError
Esempio n. 19
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
Esempio n. 20
0
    def verify_oauth_body_sign(self,
                               request,
                               content_type='application/x-www-form-urlencoded'
                               ):
        """
        Verify grade request from LTI provider using OAuth body signing.

        Uses http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html::

            This specification extends the OAuth signature to include integrity checks on HTTP request bodies
            with content types other than application/x-www-form-urlencoded.

        Arguments:
            request: DjangoWebobRequest.

        Raises:
            LTIError if request is incorrect.
        """

        client_key, client_secret = self.get_client_key_secret()
        headers = {
            'Authorization': unicode(request.headers.get('Authorization')),
            'Content-Type': content_type,
        }

        sha1 = hashlib.sha1()
        sha1.update(request.body)
        oauth_body_hash = base64.b64encode(sha1.digest())
        oauth_params = signature.collect_parameters(
            headers=headers, exclude_oauth_signature=False)
        oauth_headers = dict(oauth_params)
        oauth_signature = oauth_headers.pop('oauth_signature')
        mock_request = mock.Mock(uri=unicode(urllib.unquote(request.url)),
                                 http_method=unicode(request.method),
                                 params=oauth_headers.items(),
                                 signature=oauth_signature)

        if oauth_body_hash != oauth_headers.get('oauth_body_hash'):
            raise LTIError("OAuth body hash verification is failed.")

        if not signature.verify_hmac_sha1(mock_request, client_secret):
            raise LTIError("OAuth signature verification is failed.")
Esempio n. 21
0
    def test_normalize_parameters(self):
        """ We copy some of the variables from the test method above."""

        headers = {'Authorization': self.authorization_header}
        parameters = collect_parameters(
            uri_query=self.uri_query, body=self.body, headers=headers)
        normalized = normalize_parameters(parameters)

        # Unicode everywhere and always
        self.assertIsInstance(normalized, unicode_type)

        # Lets see if things are in order
        # check to see that querystring keys come in alphanumeric order:
        querystring_keys = ['a2', 'a3', 'b5', 'content', 'oauth_consumer_key',
                            'oauth_nonce', 'oauth_signature_method',
                            'oauth_timestamp', 'oauth_token']
        index = -1  # start at -1 because the 'a2' key starts at index 0
        for key in querystring_keys:
            self.assertGreater(normalized.index(key), index)
            index = normalized.index(key)
Esempio n. 22
0
    def check_oauth(self):
        """
        Checks whether the request passes Oauth signature check
        :return valid_oauth
        """
        resp = dict(self.__httprequest.POST.dict())
        orderedresp = OrderedDict(sorted(resp.items(), key=lambda t: t[0]))
        query_string = urllib.urlencode(orderedresp)
        oauth_headers = dict(signature.collect_parameters(query_string, exclude_oauth_signature=False))
        sig = oauth_headers.pop('oauth_signature')
        consumer_secret = self.get_oauthsecret_for_key(orderedresp.get('oauth_consumer_key'))

        oauthrequest = Oauthrequest()
        oauthrequest.params = oauth_headers.items()
        oauthrequest.uri = unicode(urllib.unquote(self.__httprequest.build_absolute_uri()))
        oauthrequest.http_method = unicode('POST')
        oauthrequest.signature = sig
        if signature.verify_hmac_sha1(request=oauthrequest, client_secret=unicode(consumer_secret)):
            return True
        return False
Esempio n. 23
0
    def test_normalize_parameters(self):
        """ We copy some of the variables from the test method above."""

        headers = {'Authorization': self.authorization_header}
        parameters = collect_parameters(
            uri_query=self.uri_query, body=self.body, headers=headers)
        normalized = normalize_parameters(parameters)

        # Unicode everywhere and always
        self.assertIsInstance(normalized, unicode_type)

        # Lets see if things are in order
        # check to see that querystring keys come in alphanumeric order:
        querystring_keys = ['a2', 'a3', 'b5', 'oauth_consumer_key',
                            'oauth_nonce', 'oauth_signature_method',
                            'oauth_timestamp', 'oauth_token']
        index = -1  # start at -1 because the 'a2' key starts at index 0
        for key in querystring_keys:
            self.assertGreater(normalized.index(key), index)
            index = normalized.index(key)
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
Esempio n. 25
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
Esempio n. 26
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
Esempio n. 27
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)
Esempio n. 28
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)
Esempio n. 29
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
Esempio n. 30
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