def check_auth(self) -> None: """ Checks for the validity of an authorization header string Raises exceptions.UnauthorizedException when the request is not authorized """ auth_header = flask.request.headers.get("Authorization") or "" full_path = flask.request.full_path.rstrip("?") dcid = flask.request.headers.get("dragonchain") timestamp = flask.request.headers.get("timestamp") content_type = flask.request.headers.get("Content-Type") content = flask.request.data if not dcid: raise exceptions.UnauthorizedException("Missing Dragonchain ID in request header") if not timestamp: raise exceptions.UnauthorizedException("Missing timestamp in request header") if not content_type: content_type = "" if not content: content = b"" authorization.verify_request_authorization( authorization=auth_header, http_verb=flask.request.method, full_path=full_path, dcid=dcid, timestamp=timestamp, content_type=content_type, content=content, interchain=self.interchain, root_only=self.root_only, )
def register_interchain_auth_v1( chain_registration_body: Dict[str, str]) -> None: dcid = chain_registration_body["dcid"] key = chain_registration_body["key"] signature = chain_registration_body["signature"] # Initialize keys with chain id as public key (dont need to pull from matchmaking) requester_keys = keys.DCKeys(pull_keys=False).initialize( public_key_string=dcid, hash_type="sha256") if not requester_keys.check_signature( f"{keys.get_public_id()}_{key}".encode("utf-8"), signature): _log.info( f"invalid signature for interchain key registration from {dcid}") raise exceptions.UnauthorizedException( "Invalid signature authorization") # Authorization successful, now register actual key auth_key = api_key_model.new_from_scratch(interchain_dcid=dcid) auth_key.key = key try: api_key_dao.save_api_key(auth_key) except Exception: _log.exception("Error saving interchain auth key") raise RuntimeError("Authorization registration failure") _log.info(f"successfully registered interchain auth key with {dcid}")
def verify_request_authorization( # noqa: C901 authorization: str, http_verb: str, full_path: str, dcid: str, timestamp: str, content_type: str, content: bytes, interchain: bool, api_resource: str, api_operation: str, api_name: str, ) -> api_key_model.APIKeyModel: """Verify an http request to the webserver Args: authorization: Authorization header of the request http_verb: HTTP Verb of the request (i.e. GET, POST, etc) full_path: full path of the request after the FQDN (including any query parameters) (i.e. /chains/transaction) dcid: dragonchain header of the request timestamp: timestamp header of the request content-type: content-type header of the request (if it exists) content: byte object of the body of the request (if it exists) interchain: boolean whether to use interchain keys to check or not api_resource: the api resource name of this endpoint api_operation: the CRUD api operation of this endpoint ("create", "read", "update", "delete") api_name: the api name of this particular endpoint Raises: exceptions.UnauthorizedException (with message) when the authorization is not valid exceptions.ActionForbidden (with message) when the authorization is valid, but the action is not allowed exceptions.APIRateLimitException (with message) when rate limit is currently exceeded for the provided api key id Returns: The api key model used for this request (if successfully authenticated) """ if dcid != keys.get_public_id(): raise exceptions.UnauthorizedException("Incorrect Dragonchain ID") try: # Note, noqa for typing on re.searches are because we explicitly catch the exceptions and handle below version = re.search("^DC(.*)-HMAC", authorization).group(1) # noqa: T484 if version == "1": hash_type = re.search("HMAC-(.*) ", authorization).group(1) # noqa: T484 try: supported_hash = get_supported_hmac_hash(hash_type) except ValueError: raise exceptions.UnauthorizedException( "Unsupported HMAC Hash Type") # Make sure clock drift isn't too far to prevent replays now = get_now_datetime() request_time = None # Tolerate given timestamps both with/without decimals of a second if "." in timestamp: request_time = datetime.datetime.strptime( timestamp, "%Y-%m-%dT%H:%M:%S.%fZ") else: request_time = datetime.datetime.strptime( timestamp, "%Y-%m-%dT%H:%M:%SZ") delta = datetime.timedelta(seconds=TIMEOUT_SEC) # Allow all requests within +/- TIMEOUT_SEC seconds of the chain's curent time if now + delta < request_time or now - delta > request_time: raise exceptions.UnauthorizedException( "Timestamp of request too skewed") hmac_index = authorization.rfind(":") if hmac_index == -1: raise exceptions.UnauthorizedException( "Malformed Authorization Header") hmac = base64.b64decode(authorization[hmac_index + 1:]) message_string = get_hmac_message_string(http_verb, full_path, dcid, timestamp, content_type, content, supported_hash) try: auth_key_id = re.search(" (.*):", authorization).group(1) # noqa: T484 try: auth_key = api_key_dao.get_api_key(auth_key_id, interchain) except exceptions.NotFound: _log.info( f"Authorization failure from key that does not exist {auth_key_id}" ) raise exceptions.UnauthorizedException( "Invalid HMAC Authentication") # Check if this key should be rate limited (does not apply to interchain keys) if not interchain and should_rate_limit(auth_key_id): raise exceptions.APIRateLimitException( f"API Rate Limit Exceeded. {RATE_LIMIT} requests allowed per minute." ) if crypto.compare_hmac(supported_hash, hmac, auth_key.key, message_string): # Check if this signature has already been used for replay protection if signature_is_replay( f"{auth_key_id}:{base64.b64encode(hmac).decode('ascii')}" ): raise exceptions.UnauthorizedException( "Previous matching request found (no replays allowed)" ) # Check that this key is allowed to perform this action try: if auth_key.is_key_allowed(api_resource, api_operation, api_name, interchain): # Signature is valid and key is allowed; Return the api key used on success return auth_key except Exception: _log.exception( "Uncaught exception checking if api key is allowed" ) raise exceptions.ActionForbidden( f"This key is not allowed to perform {api_name}") else: # HMAC doesn't match raise exceptions.UnauthorizedException( "Invalid HMAC Authentication") except exceptions.DragonchainException: raise except Exception: raise exceptions.UnauthorizedException("Invalid HMAC Format") else: raise exceptions.UnauthorizedException( "Unsupported DC Authorization Version") except exceptions.DragonchainException: raise except Exception: raise exceptions.UnauthorizedException( "Malformed Authorization Header")
def test_webserver_error_handler_unauthorized_exception(self, mock_http_response, mock_report_exception): exception = exceptions.UnauthorizedException() helpers.webserver_error_handler(exception) mock_report_exception.assert_not_called() mock_http_response.assert_called_once_with(401, ANY)