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 _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 _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 test_construct_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" Sample Base string generated and tested against:: POST&http%253A%2F%2Fexample.com%2Frequest%253Fb5%253D%25253D%252525 3D%2526a3%253Da%2526c%252540%253D%2526a2%253Dr%252520b&OAuth%2520re alm%253D%2522Example%2522%252Coauth_consumer_key%253D%25229djdj82h4 8djs9d2%2522%252Coauth_token%253D%2522kkk9d7dh3k39sjv7%2522%252Coau th_signature_method%253D%2522HMAC-SHA1%2522%252Coauth_timestamp%253 D%2522137131201%2522%252Coauth_nonce%253D%25227d8f3e4a%2522%252Coau th_signature%253D%2522bYT5CMsGcbgUdFHObYMEfcx6bsw%25253D%2522 """ self.assertRaises(ValueError, construct_base_string, self.http_method, self.base_string_url, self.normalized_encoded_request_parameters) self.assertRaises(ValueError, construct_base_string, self.http_method.decode('utf-8'), self.base_string_url, self.normalized_encoded_request_parameters) self.assertRaises(ValueError, construct_base_string, self.http_method.decode('utf-8'), self.base_string_url.decode('utf-8'), self.normalized_encoded_request_parameters) base_string = construct_base_string( self.http_method.decode('utf-8'), self.base_string_url.decode('utf-8'), self.normalized_encoded_request_parameters.decode('utf-8') ) self.assertEqual(self.control_base_string, base_string)
def message_cb(bot, event): source = event.data["source"]["aimId"] message = event.data["message"] method = url = query = secret_key = None for token in re.split(pattern=r"\s+", string=message): if method is None and token.upper() in HTTP_METHODS: method = token continue if url is None: try: parsed = urlparse.urlparse(url=token) assert parsed.scheme and parsed.netloc url = token query = parsed.query continue except (ValueError, AssertionError): pass if secret_key is None: secret_key = token if url is None or query is None: help_cb(bot=bot, event=event) return if method is None: method = "METHOD" params = urlparse.parse_qsl(query, keep_blank_values=True) normalized_params = signature.normalize_parameters(params) normalized_url = signature.normalize_base_string_uri(url) sign_base = signature.construct_base_string(method, normalized_url, normalized_params) sign = None if secret_key is not None: sign = b64encode(HMAC(secret_key.encode(), sign_base.encode(), hashlib.sha256).digest()).decode() bot.send_im( target=source, message="Signature base: {signature_base}{sign}".format( signature_base=sign_base, sign="" if sign is None else "\n\nSignature: {sign}".format(sign=sign) ) )
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 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 sign(self, url, method=u'POST', signature_method=u'HMAC-SHA1'): """ Use this method to create a signature over the authorization header, and the url query parameters :param url: request url :param method: request method (i.e. POST, GET) :param signature_method: method of signature. Only supports (HMAC-SHA1, PLAINTEXT) Note: that HMAC-SHA1 is required by Yahoo API queries since they are sent insecurely """ # could change this to support additional methods # for now only support HMAC-SHA1 and PLAINTEXT (Yahoo only supports these) self.add_param(u'oauth_signature_method', signature_method) self.add_param(u'oauth_nonce', generate_nonce()) self.add_param(u'oauth_timestamp', generate_timestamp()) if signature_method == u'HMAC-SHA1': base_string = construct_base_string(unicode(method), normalize_base_string_uri(unicode(url)), normalize_parameters(self.params)) signature = sign_hmac_sha1(base_string, self.client_secret, self.oauth_token_secret) else: signature = quote(self.client_secret + u'&' + self.oauth_token_secret) self.add_param(u'oauth_signature', signature)
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.construct_base_string( 'POST', signature.normalize_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
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 validate_request(self, uri, http_method="GET", body=None, headers=None): """Validate a signed OAuth request. :param uri: The full URI of the token request. :param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc. :param body: The request body as a string. :param headers: The request headers as a dict. :returns: A tuple of 2 elements. 1. True if valid, False otherwise. 2. An oauthlib.common.Request object. """ try: request = self._create_request(uri, http_method, body, headers) except errors.OAuth1Error as e: # noqa return False, None try: self._check_transport_security(request) self._check_mandatory_parameters(request) except errors.OAuth1Error as e: self.validation_error_message = e.description # TOOPHER return False, request if not self.request_validator.validate_timestamp_and_nonce( request.client_key, request.timestamp, request.nonce, request): return False, request # The server SHOULD return a 401 (Unauthorized) status code when # receiving a request with invalid client credentials. # Note: This is postponed in order to avoid timing attacks, instead # a dummy client is assigned and used to maintain near constant # time request verification. # # Note that early exit would enable client enumeration valid_client = self.request_validator.validate_client_key( request.client_key, request) if not valid_client: request.client_key = self.request_validator.dummy_client valid_signature = self._check_signature(request) # We delay checking validity until the very end, using dummy values for # calculations and fetching secrets/keys to ensure the flow of every # request remains almost identical regardless of whether valid values # have been supplied. This ensures near constant time execution and # prevents malicious users from guessing sensitive information v = all((valid_client, valid_signature)) if not v: log.info("[Failure] request verification failed.") log.info("Valid client: %s", valid_client) log.info("Valid signature: %s", valid_signature) if valid_client and not valid_signature: # TOOPHER norm_params = signature.normalize_parameters( request.params) # TOOPHER uri = signature.normalize_base_string_uri(request.uri) # TOOPHER base_signing_string = signature.construct_base_string( request.http_method, uri, norm_params) # TOOPHER self.validation_error_message = "Invalid signature. Expected signature base string: {0}".format( base_signing_string) # TOOPHER return v, request
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)