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
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)
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
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
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 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
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.")
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)
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))
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))
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
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.")
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)
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
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)
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
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
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.")
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)
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
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
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
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
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)
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)
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
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