def __init__(self, settings, response=None, method='redirect'): """ Constructs a Logout Response object (Initialize params from settings and if provided load the Logout Response. Arguments are: * (OneLogin_Saml2_Settings) settings. Setting data * (string) response. An UUEncoded SAML Logout response from the IdP. """ if method == 'redirect': super(SpidSaml2LogoutResponse, self).__init__(settings, response) elif method == 'post': self.__settings = settings self.__error = None self.id = None if response is not None: self.__logout_response = OneLogin_Saml2_Utils.b64decode( response) self.document = OneLogin_Saml2_XML.to_etree( self.__logout_response) self.id = self.document.get('ID', None) else: raise ValueError("Wrong value %r for argument 'method'." % method)
def __init__(self, settings, response): """ Constructs the response object. :param settings: The setting info :type settings: OneLogin_Saml2_Setting object :param response: The base64 encoded, XML string containing the samlp:Response :type response: string """ self.__settings = settings self.__error = None self.response = OneLogin_Saml2_Utils.b64decode(response) self.document = OneLogin_Saml2_XML.to_etree(self.response) self.decrypted_document = None self.encrypted = None self.valid_scd_not_on_or_after = None # Quick check for the presence of EncryptedAssertion encrypted_assertion_nodes = self.__query( '/samlp:Response/saml:EncryptedAssertion') if encrypted_assertion_nodes: decrypted_document = deepcopy(self.document) self.encrypted = True self.decrypted_document = self.__decrypt_assertion( decrypted_document)
def __init__(self, settings, response=None, method='redirect'): """ Constructs a Logout Response object (Initialize params from settings and if provided load the Logout Response. Arguments are: * (OneLogin_Saml2_Settings) settings. Setting data * (string) response. An UUEncoded SAML Logout response from the IdP. """ self.__settings = settings self.__error = None self.__is_post = method == 'post' self.id = None if response is not None: if method == 'redirect': self.__logout_response = compat.to_string( OneLogin_Saml2_Utils.decode_base64_and_inflate( response, ignore_zip=True)) elif method == 'post': self.__logout_response = OneLogin_Saml2_Utils.b64decode( response) else: raise ValueError("Wrong value %r for argument 'method'." % method) self.document = OneLogin_Saml2_XML.to_etree(self.__logout_response) self.id = self.document.get('ID', None) if method != "redirect" and method != "post": raise ValueError("Wrong value %r for argument 'method'." % method)
def __validate_signature(self, data, saml_type, raise_exceptions=False): """ Validate Signature :param data: The Request data :type data: dict :param cert: The certificate to check signature :type cert: str :param saml_type: The target URL the user should be redirected to :type saml_type: string SAMLRequest | SAMLResponse :param raise_exceptions: Whether to return false on failure or raise an exception :type raise_exceptions: Boolean """ try: signature = data.get('Signature', None) if signature is None: if self.__settings.is_strict( ) and self.__settings.get_security_data().get( 'wantMessagesSigned', False): raise OneLogin_Saml2_ValidationError( 'The %s is not signed. Rejected.' % saml_type, OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE) return True x509cert = self.get_settings().get_idp_cert() if not x509cert: error_msg = "In order to validate the sign on the %s, the x509cert of the IdP is required" % saml_type self.__errors.append(error_msg) raise OneLogin_Saml2_Error(error_msg, OneLogin_Saml2_Error.CERT_NOT_FOUND) sign_alg = data.get('SigAlg', OneLogin_Saml2_Constants.RSA_SHA1) if isinstance(sign_alg, bytes): sign_alg = sign_alg.decode('utf8') lowercase_urlencoding = False if 'lowercase_urlencoding' in self.__request_data.keys(): lowercase_urlencoding = self.__request_data[ 'lowercase_urlencoding'] signed_query = self.__build_sign_query( data[saml_type], data.get('RelayState', None), sign_alg, saml_type, lowercase_urlencoding) if not OneLogin_Saml2_Utils.validate_binary_sign( signed_query, OneLogin_Saml2_Utils.b64decode(signature), x509cert, sign_alg, self.__settings.is_debug_active()): raise OneLogin_Saml2_ValidationError( 'Signature validation failed. %s rejected.' % saml_type, OneLogin_Saml2_ValidationError.INVALID_SIGNATURE) return True except Exception as e: self.__error_reason = str(e) if raise_exceptions: raise e return False
async def post(self, file): try: # get response and Relay state responsePost = self.get_argument('JSONResponse') srelayPost = self.get_argument('RelayState') try: JSONResponse = OneLogin_Saml2_Utils.b64decode(responsePost) except Exception: try: JSONResponse = OneLogin_Saml2_Utils.decode_base64_and_inflate( responsePost) except Exception: pass responsedict = jsonpickle.decode(JSONResponse) samlErrors = responsedict['result']['samlErrors'] content = self.get_content(os.path.join(self.root, file)) tmp = b"" if isinstance(content, bytes): content = [content] for chunk in content: try: tmp = tmp.join([chunk]) except iostream.StreamClosedError: return content = tmp content = content.replace( b"$http_error$", bytes(str(responsedict['error']['httpcode']), 'utf-8')) content = content.replace(b"$ITMessage$", bytes(samlErrors['ITMessage'], 'utf-8')) content = content.replace(b"$ENMessage$", bytes(samlErrors['ENMessage'], 'utf-8')) content = content.replace(b"$statusCode$", bytes(samlErrors['statusCode'], 'utf-8')) content = content.replace( b"$subStatusCode$", bytes(samlErrors['subStatusCode'], 'utf-8')) content = content.replace( b"$statusMessage$", bytes(samlErrors['statusMessage'], 'utf-8')) await self.get(file, False) self.set_header("Content-Length", len(content)) self.write(content) await self.flush() except AssertionError as inst: self.set_header("Content-Length", len(content)) self.write(content) await self.flush()
def __validate_signature(self, data, saml_type): """ Validate Signature :param data: The Request data :type data: dict :param cert: The certificate to check signature :type cert: str :param saml_type: The target URL the user should be redirected to :type saml_type: string SAMLRequest | SAMLResponse """ signature = data.get('Signature', None) if signature is None: if self.__settings.is_strict() and self.__settings.get_security_data().get('wantMessagesSigned', False): self.__error_reason = 'The %s is not signed. Rejected.' % saml_type return False return True x509cert = self.get_settings().get_idp_cert() if x509cert is None: self.__errors.append("In order to validate the sign on the %s, the x509cert of the IdP is required" % saml_type) return False try: sign_alg = data.get('SigAlg', OneLogin_Saml2_Constants.RSA_SHA1) if isinstance(sign_alg, bytes): sign_alg = sign_alg.decode('utf8') lowercase_urlencoding = False if 'lowercase_urlencoding' in self.__request_data.keys(): lowercase_urlencoding = self.__request_data['lowercase_urlencoding'] signed_query = self.__build_sign_query(data[saml_type], data.get('RelayState', None), sign_alg, saml_type, lowercase_urlencoding ) if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, OneLogin_Saml2_Utils.b64decode(signature), x509cert, sign_alg, self.__settings.is_debug_active()): raise Exception('Signature validation failed. %s rejected.' % saml_type) return True except Exception as e: self.__error_reason = str(e) return False
def __validate_signature(self, data, saml_type): """ Validate Signature :param data: The Request data :type data: dict :param cert: The certificate to check signature :type cert: str :param saml_type: The target URL the user should be redirected to :type saml_type: string SAMLRequest | SAMLResponse """ signature = data.get('Signature', None) if signature is None: if self.__settings.is_strict( ) and self.__settings.get_security_data().get( 'wantMessagesSigned', False): self.__error_reason = 'The %s is not signed. Rejected.' % saml_type return False return True x509cert = self.get_settings().get_idp_cert() if x509cert is None: self.__errors.append( "In order to validate the sign on the %s, the x509cert of the IdP is required" % saml_type) return False try: sign_alg = data.get('SigAlg', OneLogin_Saml2_Constants.RSA_SHA1) if isinstance(sign_alg, bytes): sign_alg = sign_alg.decode('utf8') if sign_alg != OneLogin_Saml2_Constants.RSA_SHA1: raise Exception('Invalid SigAlg, the %s rejected.' % saml_type) signed_query = self.__build_sign_query( data[saml_type], data.get('RelayState', None), sign_alg, saml_type) if not OneLogin_Saml2_Utils.validate_binary_sign( signed_query, OneLogin_Saml2_Utils.b64decode(signature), x509cert, sign_alg, self.__settings.is_debug_active()): raise Exception('Signature validation failed. %s rejected.' % saml_type) return True except Exception as e: self.__error_reason = str(e) return False
def from_base64(cls, base64, url_decode=False): """ Instantiates the class using base64-encoded XML input. Args: base64 (basestring): SAML response as base64-encoded XML string url_decode (bool): True performs url decoding before parsing. Default: False. Returns: (BaseSamlParser) parsed SAML response object """ value = base64 if not url_decode else unquote(base64) # Check to see if this is valid base64 rx = r'[^a-zA-Z0-9/+=]' if re.search(rx, value): raise DataTypeInvalid("This does not appear to be valid base64") return cls(utils.b64decode(value))
def __init__(self, settings, response): """ Constructs the response object. :param settings: The setting info :type settings: OneLogin_Saml2_Setting object :param response: The base64 encoded, XML string containing the samlp:Response :type response: string """ self.__settings = settings self.__error = None self.response = OneLogin_Saml2_Utils.b64decode(response) self.document = OneLogin_Saml2_XML.to_etree(self.response) self.decrypted_document = None self.encrypted = None # Quick check for the presence of EncryptedAssertion encrypted_assertion_nodes = self.__query('/samlp:Response/saml:EncryptedAssertion') if encrypted_assertion_nodes: decrypted_document = deepcopy(self.document) self.encrypted = True self.decrypted_document = self.__decrypt_assertion(decrypted_document)
def _validate_signature(self, data, saml_type, raise_exceptions=False): """ Validate Signature :param data: The Request data :type data: dict :param cert: The certificate to check signature :type cert: str :param saml_type: The target URL the user should be redirected to :type saml_type: string SAMLRequest | SAMLResponse :param raise_exceptions: Whether to return false on failure or raise an exception :type raise_exceptions: Boolean """ try: signature = data.get('Signature', None) if signature is None: if self._settings.is_strict( ) and self._settings.get_security_data().get( 'wantMessagesSigned', False): raise OneLogin_Saml2_ValidationError( 'The %s is not signed. Rejected.' % saml_type, OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE) return True idp_data = self.get_settings().get_idp_data() exists_x509cert = self.get_settings().get_idp_cert() is not None exists_multix509sign = 'x509certMulti' in idp_data and \ 'signing' in idp_data['x509certMulti'] and \ idp_data['x509certMulti']['signing'] if not (exists_x509cert or exists_multix509sign): error_msg = 'In order to validate the sign on the %s, the x509cert of the IdP is required' % saml_type self._errors.append(error_msg) raise OneLogin_Saml2_Error(error_msg, OneLogin_Saml2_Error.CERT_NOT_FOUND) sign_alg = data.get('SigAlg', OneLogin_Saml2_Constants.RSA_SHA1) if isinstance(sign_alg, bytes): sign_alg = sign_alg.decode('utf8') security = self._settings.get_security_data() reject_deprecated_alg = security.get('rejectDeprecatedAlgorithm', False) if reject_deprecated_alg: if sign_alg in OneLogin_Saml2_Constants.DEPRECATED_ALGORITHMS: raise OneLogin_Saml2_ValidationError( 'Deprecated signature algorithm found: %s' % sign_alg, OneLogin_Saml2_ValidationError. DEPRECATED_SIGNATURE_METHOD) query_string = self._request_data.get('query_string') if query_string and self._request_data.get( 'validate_signature_from_qs'): signed_query = self._build_sign_query_from_qs( query_string, saml_type) else: lowercase_urlencoding = self._request_data.get( 'lowercase_urlencoding', False) signed_query = self._build_sign_query(data[saml_type], data.get('RelayState'), sign_alg, saml_type, lowercase_urlencoding) if exists_multix509sign: for cert in idp_data['x509certMulti']['signing']: if OneLogin_Saml2_Utils.validate_binary_sign( signed_query, OneLogin_Saml2_Utils.b64decode(signature), cert, sign_alg): return True raise OneLogin_Saml2_ValidationError( 'Signature validation failed. %s rejected' % saml_type, OneLogin_Saml2_ValidationError.INVALID_SIGNATURE) else: cert = self.get_settings().get_idp_cert() if not OneLogin_Saml2_Utils.validate_binary_sign( signed_query, OneLogin_Saml2_Utils.b64decode(signature), cert, sign_alg, self._settings.is_debug_active()): raise OneLogin_Saml2_ValidationError( 'Signature validation failed. %s rejected' % saml_type, OneLogin_Saml2_ValidationError.INVALID_SIGNATURE) return True except Exception as e: self._error_reason = str(e) if raise_exceptions: raise e return False
def processResponse(self, chkTime=True, checkInResponseTo=True): try: # get response and Relay state responsePost = self.get_argument('SAMLResponse') srelayPost = self.get_argument('RelayState') # decode saml response #response = responsePost self.response = responsePost try: self.response = OneLogin_Saml2_Utils.decode_base64_and_inflate( responsePost) except Exception: try: self.response = OneLogin_Saml2_Utils.b64decode( responsePost) except Exception: pass # try: # #response = OneLogin_Saml2_Utils.b64decode(responsePost) # self.response = OneLogin_Saml2_Utils.b64decode(responsePost) # except Exception: # pass ## parse XML and make some check ns = { 'md0': OneLogin_Saml2_Constants.NS_SAMLP, 'md1': OneLogin_Saml2_Constants.NS_SAML } parsedResponse = xml.etree.ElementTree.fromstring(self.response) self.inResponseTo = parsedResponse.get('InResponseTo') self.ResponseID = parsedResponse.get('ID') issuer = self.issuer = parsedResponse.find("md1:Issuer", ns) if issuer is None: response_obj = ResponseObj(httpcode=401) response_obj.setError('easyspid118') return response_obj #spByInResponseTo = None # try to get sp searching a corresponding request and raise error if checkInResponseTo is True # inResponseChk = waitFuture(asyncio.run_coroutine_threadsafe(self.dbobjSaml.execute_statment("chk_idAssertion('%s')" % # inResponseTo), globalsObj.ioloop)) # if inResponseChk['error'] == 0 and inResponseChk['result'] is not None: # spByInResponseTo = inResponseChk['result'][0]['cod_sp'] # # elif checkInResponseTo and inResponseChk['error'] == 0 and inResponseChk['result'] == None: # response_obj = ResponseObj(httpcode=404, ID = ResponseID) # response_obj.setError('easyspid120') # response_obj.setResult(response = str(response, 'utf-8')) # return response_obj # # elif inResponseChk['error'] > 0: # response_obj = ResponseObj(httpcode=500) # response_obj.setError('easyspid105') # response_obj.setResult(inResponseChk['result']) # return response_obj # try to get sp searching a corresponding request and raise error if checkInResponseTo is True spByInResponseTo = self.chkExistsReq(checkInResponseTo) ### check StatusCode to find errors firstChk = easyspid.lib.utils.validateAssertion( str(self.response, 'utf-8'), None, None) if not firstChk['chkStatus']: #get errors codes samlErrors = waitFuture( asyncio.run_coroutine_threadsafe( getResponseError(parsedResponse, sp=spByInResponseTo, namespace=ns), globalsObj.ioloop)) if samlErrors['error'] == '0': response_obj = ResponseObj(httpcode=400, ID=self.ResponseID) response_obj.setError('easyspid121') response_obj.setResult(response=str( self.response, 'utf-8'), format='json', samlErrors=samlErrors['status']) return self.formatError(response_obj, srelayPost, samlErrors['service']) elif samlErrors['error'] == 'easyspid114': response_obj = ResponseObj(httpcode=404) response_obj.setError('easyspid114') return response_obj else: response_obj = ResponseObj(httpcode=500) response_obj.setError('500') response_obj.setResult(samlErrors['error']) return response_obj #decode Relay state #srelay = srelayPost self.srelay = srelayPost try: self.srelay = OneLogin_Saml2_Utils.decode_base64_and_inflate( srelayPost) except Exception: try: self.srelay = OneLogin_Saml2_Utils.b64decode(srelayPost) except Exception: pass #self.srelay = OneLogin_Saml2_Utils.b64decode(srelayPost) #pass # try: # #srelay = OneLogin_Saml2_Utils.b64decode(srelayPost) # self.srelay = OneLogin_Saml2_Utils.b64decode(srelayPost) # except Exception: # pass ## get sp by ID #ns = {'md0': OneLogin_Saml2_Constants.NS_SAMLP, 'md1': OneLogin_Saml2_Constants.NS_SAML} #parsedResponse = xml.etree.ElementTree.fromstring(response) #issuer = self.issuer = parsedResponse.find("md1:Issuer", ns) #inResponseTo = parsedResponse.get('InResponseTo') #get audience audience = self.audience = parsedResponse.find( 'md1:Assertion/md1:Conditions/md1:AudienceRestriction/md1:Audience', ns) if audience is None: response_obj = ResponseObj(httpcode=401) response_obj.setError('easyspid118') return response_obj # if issuer is None or audience is None: # response_obj = ResponseObj(httpcode=401) # response_obj.setError('easyspid118') # return response_obj #task1 = asyncio.run_coroutine_threadsafe(self.dbobjSaml.execute_statment("chk_idAssertion('%s')" % # inResponseTo), globalsObj.ioloop) # task2 = asyncio.run_coroutine_threadsafe(self.dbobjSaml.execute_statment("get_provider_byentityid(%s, '%s')" % # ('True', '{'+(self.issuer.text.strip())+'}')), globalsObj.ioloop) #task3 = asyncio.run_coroutine_threadsafe(self.dbobjSaml.execute_statment("get_provider_byentityid(%s, '%s')" % # ('True', '{'+(audience.text.strip())+'}')), globalsObj.ioloop) #assert not task1.done() #inResponseChk = task1.result() #inResponseChk = waitFuture(task1) #audienceChk = waitFuture(task3) #spByAudience = None #spByInResponseTo = None #if inResponseChk['error'] == 0 and inResponseChk['result'] is not None: # spByInResponseTo = inResponseChk['result'][0]['cod_sp'] # if audienceChk['error'] == 0 and audienceChk['result'] is not None: # spByAudience = audienceChk['result'][0]['cod_provider'] #check audinece # if spByAudience is None: # response_obj = ResponseObj(httpcode=404) # response_obj.setError('easyspid115') # return response_obj # get sp by audience spByAudience = self.getSpByAudience() #check inresponseTo and spByAudience == spByInResponseTo if checkInResponseTo and spByAudience == spByInResponseTo: sp = spByAudience elif checkInResponseTo and spByAudience != spByInResponseTo: response_obj = ResponseObj(httpcode=401) response_obj.setError('easyspid110') return response_obj sp = spByAudience # get service by sp and relay_state try: task = asyncio.run_coroutine_threadsafe( self.dbobjSaml.execute_statment( "get_service(%s, '%s', '%s')" % ('True', str(self.srelay), sp)), globalsObj.ioloop) #assert not task.done() #service = task.result() service = waitFuture(task) if service['error'] == 0 and service['result'] is not None: # costruisci il routing self.routing = dict() self.routing['url'] = service['result'][0]['url'] self.routing['relaystate'] = self.srelay self.routing['format'] = service['result'][0]['format'] elif service['error'] > 0 or service['result'] is None: response_obj = ResponseObj(httpcode=500, debugMessage=service['result']) response_obj.setError("easyspid111") return response_obj except Exception: pass # get IdP # idpEntityId = waitFuture(task2) # # if idpEntityId['error'] == 0 and idpEntityId['result'] is not None: # idp_metadata = idpEntityId['result'][0]['xml'] # idp = idpEntityId['result'][0]['cod_provider'] # # elif idpEntityId['error'] == 0 and idpEntityId['result'] is None: # response_obj = ResponseObj(httpcode=404) # response_obj.setError('easyspid103') # return response_obj # # elif idpEntityId['error'] > 0: # response_obj = ResponseObj(httpcode=500, debugMessage=idpEntityId['result']) # response_obj.setError("easyspid105") # return response_obj # get IdP and metadata (idp_metadata, idp) = self.getIdentyIdp() # get sp settings task = asyncio.run_coroutine_threadsafe( easyspid.lib.easyspid.spSettings(sp, idp, close=True), globalsObj.ioloop) sp_settings = waitFuture(task) if sp_settings['error'] == 0 and sp_settings['result'] != None: ## insert response into DB task = asyncio.run_coroutine_threadsafe( self.dbobjSaml.execute_statment( "write_assertion('%s', '%s', '%s', '%s')" % (str(self.response, 'utf-8').replace( "'", "''"), sp, idp, self.remote_ip)), globalsObj.ioloop) wrtAuthn = waitFuture(task) if wrtAuthn['error'] == 0: if self.routing['format'] == 'saml': return self.passthrough() task = asyncio.run_coroutine_threadsafe( self.dbobjJwt.execute_statment( "get_token_by_cod('%s')" % (wrtAuthn['result'][0]['cod_token'])), globalsObj.ioloop) #assert not task.done() #jwt = task.result() jwt = waitFuture(task) else: response_obj = ResponseObj(httpcode=500, debugMessage=wrtAuthn['result']) response_obj.setError("easyspid105") logging.getLogger( type(self).__module__ + "." + type(self).__qualname__).error('Exception', exc_info=True) return response_obj # create settings OneLogin dict #settings = sp_settings['result'] prvdSettings = Saml2_Settings(sp_settings['result']) chk = easyspid.lib.utils.validateAssertion( str(self.response, 'utf-8'), sp_settings['result']['idp']['x509cert_fingerprint'], sp_settings['result']['idp']['x509cert_fingerprintalg']) chk['issuer'] = issuer.text.strip() chk['audience'] = audience.text.strip() if not chk['chkStatus']: response_obj = ResponseObj(httpcode=401) response_obj.setError('easyspid107') return response_obj elif not chk['schemaValidate']: response_obj = ResponseObj(httpcode=401) response_obj.setError('easyspid104') response_obj.setResult(responseValidate=chk) return response_obj elif not chk['signCheck']: response_obj = ResponseObj(httpcode=401) response_obj.setError('easyspid106') response_obj.setResult(responseValidate=chk) return response_obj elif not chk[ 'certAllowed'] and globalsObj.easyspid_checkCertificateAllowed: response_obj = ResponseObj(httpcode=401) response_obj.setError('easyspid116') response_obj.setResult(responseValidate=chk) return response_obj elif not chk[ 'certValidity'] and globalsObj.easyspid_checkCertificateValidity: response_obj = ResponseObj(httpcode=401) response_obj.setError('easyspid117') response_obj.setResult(responseValidate=chk) return response_obj elif chkTime and not chk['chkTime']: response_obj = ResponseObj(httpcode=401) response_obj.setError('easyspid108') return response_obj #get all attributes attributes = chk['serviceAttributes'] attributes_tmp = dict() for key in attributes: attributes_tmp[key] = attributes[key][0] attributes = attributes_tmp # build response form try: with open( os.path.join(globalsObj.modules_basedir, globalsObj.easyspid_responseFormPath), 'rb') as myfile: response_form = myfile.read().decode("utf-8") except: with open(globalsObj.easyspid_responseFormPath, 'rb') as myfile: response_form = myfile.read().decode("utf-8") response_obj = ResponseObj( httpcode=200, ID=wrtAuthn['result'][0]['ID_assertion']) response_obj.setError('200') response_obj.setResult(attributes=attributes, jwt=jwt['result'][0]['token'], responseValidate=chk, response=str(self.response, 'utf-8'), format='json') response_form = response_form.replace("%URLTARGET%", self.routing['url']) response_form = response_form.replace("%RELAYSTATE%", srelayPost) response_form = response_form.replace( "%RESPONSE%", OneLogin_Saml2_Utils.b64encode(response_obj.jsonWrite())) self.postTo = response_form elif sp_settings['error'] == 0 and sp_settings['result'] == None: response_obj = ResponseObj(httpcode=404) response_obj.setError('easyspid114') elif sp_settings['error'] > 0: #response_obj = sp_settings['result'] response_obj = sp_settings except goExit as e: return e.expression except tornado.web.MissingArgumentError as error: response_obj = ResponseObj(debugMessage=error.log_message, httpcode=error.status_code, devMessage=error.log_message) response_obj.setError(str(error.status_code)) logging.getLogger( type(self).__module__ + "." + type(self).__qualname__).error( '%s' % error, exc_info=True) except Exception as inst: response_obj = ResponseObj(httpcode=500) response_obj.setError('500') logging.getLogger( type(self).__module__ + "." + type(self).__qualname__).error( 'Exception', exc_info=True) return response_obj