def testGetSelfURLNoQuery(self): """ Tests the get_self_url_no_query method of the OneLogin_Saml2_Utils """ request_data = { 'http_host': 'example.com', 'script_name': '/index.html' } url = OneLogin_Saml2_Utils.get_self_url_host(request_data) + request_data['script_name'] self.assertEqual(url, OneLogin_Saml2_Utils.get_self_url_no_query(request_data)) request_data['path_info'] = '/test' self.assertEqual(url + '/test', OneLogin_Saml2_Utils.get_self_url_no_query(request_data))
def testGetSelfURLNoQuery(self): """ Tests the get_self_url_no_query method of the OneLogin_Saml2_Utils """ request_data = { 'http_host': 'example.com', 'script_name': '/index.html' } url = OneLogin_Saml2_Utils.get_self_url_host(request_data) + request_data['script_name'] self.assertEqual(url, OneLogin_Saml2_Utils.get_self_url_no_query(request_data)) request_data['path_info'] = '/test' self.assertEqual(url + '/test', OneLogin_Saml2_Utils.get_self_url_no_query(request_data))
def testProcessResponseValid(self): """ Tests the processResponse method of the OneLogin_Saml2_Auth class Case Valid Response, After processing the response the user is authenticated, attributes are returned, also has a nameID and the error array is empty """ request_data = self.get_request() message = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64')) plain_message = b64decode(message) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) del request_data['get_data'] request_data['post_data'] = { 'SAMLResponse': b64encode(plain_message) } auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) auth.process_response() self.assertTrue(auth.is_authenticated()) self.assertEqual(len(auth.get_errors()), 0) self.assertEqual('*****@*****.**', auth.get_nameid()) attributes = auth.get_attributes() self.assertNotEqual(len(attributes), 0) self.assertEqual(auth.get_attribute('mail'), attributes['mail']) auth.set_strict(True) auth.process_response() self.assertEqual(len(auth.get_errors()), 0)
def testProcessResponseInvalidRequestId(self): """ Tests the processResponse method of the OneLogin_Saml2_Auth class Case Invalid Response, Invalid requestID """ request_data = self.get_request() message = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64')) plain_message = b64decode(message) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) del request_data['get_data'] request_data['post_data'] = { 'SAMLResponse': b64encode(plain_message) } auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) request_id = 'invalid' auth.process_response(request_id) self.assertEqual(len(auth.get_errors()), 0) auth.set_strict(True) auth.process_response(request_id) self.assertEqual(auth.get_errors(), ['invalid_response']) valid_request_id = '_57bcbf70-7b1f-012e-c821-782bcb13bb38' auth.process_response(valid_request_id) self.assertEqual(len(auth.get_errors()), 0)
def testProcessSLORequestSignedResponse(self): """ Tests the process_slo method of the OneLogin_Saml2_Auth class Case Valid Logout Request, validating the relayState, a signed LogoutResponse is created and a redirection executed """ settings_info = self.loadSettingsJSON() settings_info["security"]["logoutResponseSigned"] = True request_data = self.get_request() message = self.file_contents(join(self.data_path, "logout_requests", "logout_request_deflated.xml.base64")) # In order to avoid the destination problem plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace("http://stuff.com/endpoints/endpoints/sls.php", current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) request_data["get_data"]["SAMLRequest"] = message request_data["get_data"]["RelayState"] = "http://relaystate.com" auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_info) auth.set_strict(True) target_url = auth.process_slo(False) parsed_query = parse_qs(urlparse(target_url)[4]) slo_url = settings_info["idp"]["singleLogoutService"]["url"] self.assertIn(slo_url, target_url) self.assertIn("SAMLResponse", parsed_query) self.assertIn("RelayState", parsed_query) self.assertIn("SigAlg", parsed_query) self.assertIn("Signature", parsed_query) self.assertIn("http://relaystate.com", parsed_query["RelayState"]) self.assertIn(OneLogin_Saml2_Constants.RSA_SHA1, parsed_query["SigAlg"])
def testIsInValidIssuer(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutResponse Case invalid Issuer """ request_data = { 'http_host': 'example.com', 'script_name': 'index.html', 'get_data': {} } settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) plain_message = plain_message.replace('http://idp.example.com/', 'http://invalid.issuer.example.com') message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) settings.set_strict(False) response = OneLogin_Saml2_Logout_Response(settings, message) self.assertTrue(response.is_valid(request_data)) settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, message) self.assertFalse(response_2.is_valid(request_data)) self.assertIn('Invalid issuer in the Logout Response', response_2.get_error())
def testProcessResponseInvalidRequestId(self): """ Tests the process_response method of the OneLogin_Saml2_Auth class Case Invalid Response, Invalid requestID """ request_data = self.get_request() message = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64')) plain_message = compat.to_string(b64decode(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) del request_data['get_data'] request_data['post_data'] = { 'SAMLResponse': compat.to_string(b64encode(compat.to_bytes(plain_message))) } auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) request_id = 'invalid' auth.process_response(request_id) self.assertEqual('No Signature found. SAML Response rejected', auth.get_last_error_reason()) auth.set_strict(True) auth.process_response(request_id) self.assertEqual(auth.get_errors(), ['invalid_response']) self.assertEqual('The InResponseTo of the Response: _57bcbf70-7b1f-012e-c821-782bcb13bb38, does not match the ID of the AuthNRequest sent by the SP: invalid', auth.get_last_error_reason()) valid_request_id = '_57bcbf70-7b1f-012e-c821-782bcb13bb38' auth.process_response(valid_request_id) self.assertEqual('No Signature found. SAML Response rejected', auth.get_last_error_reason())
def logout(self, return_to=None, name_id=None, session_index=None, nq=None, name_id_format=None): """ Initiates the SLO process. :param return_to: Optional argument. The target URL the user should be redirected to after logout. :type return_to: string :param name_id: The NameID that will be set in the LogoutRequest. :type name_id: string :param session_index: SessionIndex that identifies the session of the user. :type session_index: string :param nq: IDP Name Qualifier :type: string :param name_id_format: The NameID Format that will be set in the LogoutRequest. :type: string :returns: Redirection URL """ slo_url = self.get_slo_url() if slo_url is None: raise OneLogin_Saml2_Error( 'The IdP does not support Single Log Out', OneLogin_Saml2_Error.SAML_SINGLE_LOGOUT_NOT_SUPPORTED) if name_id is None and self.__nameid is not None: name_id = self.__nameid if name_id_format is None and self.__nameid_format is not None: name_id_format = self.__nameid_format logout_request = OneLogin_Saml2_Logout_Request( self.__settings, name_id=name_id, session_index=session_index, nq=nq, name_id_format=name_id_format) self.__last_request = logout_request.get_xml() self.__last_request_id = logout_request.id parameters = {'SAMLRequest': logout_request.get_request()} if return_to is not None: parameters['RelayState'] = return_to else: parameters[ 'RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query( self.__request_data) security = self.__settings.get_security_data() if security.get('logoutRequestSigned', False): self.add_request_signature(parameters, security['signatureAlgorithm']) return self.redirect_to(slo_url, parameters)
def testIsValidWithCapitalization(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutResponse """ request_data = { 'http_host': 'exaMPLe.com', 'script_name': 'index.html', 'get_data': {} } settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) message = self.file_contents( join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) response = OneLogin_Saml2_Logout_Response(settings, message) self.assertTrue(response.is_valid(request_data)) settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, message) with self.assertRaisesRegex(Exception, 'The LogoutResponse was received at'): response_2.is_valid(request_data, raise_exceptions=True) plain_message = compat.to_string( OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query( request_data).lower() plain_message = plain_message.replace( 'http://stuff.com/endpoints/endpoints/sls.php', current_url) message_3 = OneLogin_Saml2_Utils.deflate_and_base64_encode( plain_message) response_3 = OneLogin_Saml2_Logout_Response(settings, message_3) self.assertTrue(response_3.is_valid(request_data))
def testProcessSLORequestSignedResponse(self): """ Tests the process_slo method of the OneLogin_Saml2_Auth class Case Valid Logout Request, validating the relayState, a signed LogoutResponse is created and a redirection executed """ settings_info = self.loadSettingsJSON() settings_info['security']['logoutResponseSigned'] = True request_data = self.get_request() message = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_deflated.xml.base64')) # In order to avoid the destination problem plain_message = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) request_data['get_data']['SAMLRequest'] = message request_data['get_data']['RelayState'] = 'http://relaystate.com' auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_info) auth.set_strict(True) target_url = auth.process_slo(False) parsed_query = parse_qs(urlparse(target_url)[4]) slo_url = settings_info['idp']['singleLogoutService']['url'] self.assertIn(slo_url, target_url) self.assertIn('SAMLResponse', parsed_query) self.assertIn('RelayState', parsed_query) self.assertIn('SigAlg', parsed_query) self.assertIn('Signature', parsed_query) self.assertIn('http://relaystate.com', parsed_query['RelayState']) self.assertIn(OneLogin_Saml2_Constants.RSA_SHA1, parsed_query['SigAlg'])
def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_policy=True): """ Initiates the SSO process. :param return_to: Optional argument. The target URL the user should be redirected to after login. :type return_to: string :param force_authn: Optional argument. When true the AuthNRequest will set the ForceAuthn='true'. :type force_authn: bool :param is_passive: Optional argument. When true the AuthNRequest will set the Ispassive='true'. :type is_passive: bool :param set_nameid_policy: Optional argument. When true the AuthNRequest will set a nameIdPolicy element. :type set_nameid_policy: bool :returns: Redirection URL :rtype: string """ authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive, set_nameid_policy) self.__last_request = authn_request.get_xml() self.__last_request_id = authn_request.get_id() saml_request = authn_request.get_request() parameters = {'SAMLRequest': saml_request} if return_to is not None: parameters['RelayState'] = return_to else: parameters['RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query(self.__request_data) security = self.__settings.get_security_data() if security.get('authnRequestsSigned', False): parameters['SigAlg'] = security['signatureAlgorithm'] parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState'], security['signatureAlgorithm']) return self.redirect_to(self.get_sso_url(), parameters)
def testIsInvalidNotOnOrAfter(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutRequest Case Invalid NotOnOrAfter """ request_data = { 'http_host': 'example.com', 'script_name': 'index.html' } request = self.file_contents( join(self.data_path, 'logout_requests', 'invalids', 'not_after_failed.xml')) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) request = request.replace( 'http://stuff.com/endpoints/endpoints/sls.php', current_url) settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) logout_request = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) self.assertTrue(logout_request.is_valid(request_data)) settings.set_strict(True) logout_request2 = OneLogin_Saml2_Logout_Request( settings, b64encode(request)) self.assertFalse(logout_request2.is_valid(request_data)) self.assertIn( 'Could not validate timestamp: expired. Check system clock.', logout_request2.get_error())
def testProcessSLOResponseValidDeletingSession(self): """ Tests the process_slo method of the OneLogin_Saml2_Auth class Case Valid Logout Response, validating deleting the local session """ request_data = self.get_request() message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) # FIXME # if (!isset($_SESSION)) { # $_SESSION = array(); # } # $_SESSION['samltest'] = true; # In order to avoid the destination problem plain_message = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) request_data['get_data']['SAMLResponse'] = message auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) auth.set_strict(True) auth.process_slo(False) self.assertEqual(len(auth.get_errors()), 0)
def login(self, return_to=None, force_authn=False, is_passive=False): """ Initiates the SSO process. :param return_to: Optional argument. The target URL the user should be redirected to after login. :type return_to: string :param force_authn: Optional argument. When true the AuthNReuqest will set the ForceAuthn='true'. :type force_authn: string :param is_passive: Optional argument. When true the AuthNReuqest will set the Ispassive='true'. :type is_passive: string :returns: Redirection url """ authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive) saml_request = authn_request.get_request() parameters = {'SAMLRequest': saml_request} if return_to is not None: parameters['RelayState'] = return_to else: parameters['RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query(self.__request_data) security = self.__settings.get_security_data() if security.get('authnRequestsSigned', False): parameters['SigAlg'] = OneLogin_Saml2_Constants.RSA_SHA1 parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState']) return self.redirect_to(self.get_sso_url(), parameters)
def testIsValid(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutResponse """ request_data = { 'http_host': 'example.com', 'script_name': 'index.html', 'get_data': {} } settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) response = OneLogin_Saml2_Logout_Response(settings, message) self.assertTrue(response.is_valid(request_data)) settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, message) try: valid = response_2.is_valid(request_data) self.assertFalse(valid) except Exception as e: self.assertIn('The LogoutRequest was received at', e.message) plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message_3 = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) response_3 = OneLogin_Saml2_Logout_Response(settings, message_3) self.assertTrue(response_3.is_valid(request_data))
def login(self, return_to=None, force_authn=False, is_passive=False): """ Initiates the SSO process. :param return_to: Optional argument. The target URL the user should be redirected to after login. :type return_to: string :param force_authn: Optional argument. When true the AuthNReuqest will set the ForceAuthn='true'. :type force_authn: string :param is_passive: Optional argument. When true the AuthNReuqest will set the Ispassive='true'. :type is_passive: string :returns: Redirection url """ authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive) saml_request = authn_request.get_request() parameters = {'SAMLRequest': saml_request} if return_to is not None: parameters['RelayState'] = return_to else: parameters[ 'RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query( self.__request_data) security = self.__settings.get_security_data() if security.get('authnRequestsSigned', False): parameters['SigAlg'] = OneLogin_Saml2_Constants.RSA_SHA1 parameters['Signature'] = self.build_request_signature( saml_request, parameters['RelayState']) return self.redirect_to(self.get_sso_url(), parameters)
def testProcessResponseInvalidRequestId(self): """ Tests the process_response method of the OneLogin_Saml2_Auth class Case Invalid Response, Invalid requestID """ request_data = self.get_request() message = self.file_contents(join(self.data_path, "responses", "unsigned_response.xml.base64")) plain_message = b64decode(message) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace("http://stuff.com/endpoints/endpoints/acs.php", current_url) del request_data["get_data"] request_data["post_data"] = {"SAMLResponse": b64encode(plain_message)} auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) request_id = "invalid" auth.process_response(request_id) self.assertEqual("No Signature found. SAML Response rejected", auth.get_last_error_reason()) auth.set_strict(True) auth.process_response(request_id) self.assertEqual(auth.get_errors(), ["invalid_response"]) self.assertEqual( "The InResponseTo of the Response: _57bcbf70-7b1f-012e-c821-782bcb13bb38, does not match the ID of the AuthNRequest sent by the SP: invalid", auth.get_last_error_reason(), ) valid_request_id = "_57bcbf70-7b1f-012e-c821-782bcb13bb38" auth.process_response(valid_request_id) self.assertEqual("No Signature found. SAML Response rejected", auth.get_last_error_reason())
def testIsInValidIssuer(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutResponse Case invalid Issuer """ request_data = { 'http_host': 'example.com', 'script_name': 'index.html', 'get_data': {} } settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) message = self.file_contents( join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) plain_message = compat.to_string( OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace( 'http://stuff.com/endpoints/endpoints/sls.php', current_url) plain_message = plain_message.replace( 'http://idp.example.com/', 'http://invalid.issuer.example.com') message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) settings.set_strict(False) response = OneLogin_Saml2_Logout_Response(settings, message) self.assertTrue(response.is_valid(request_data)) settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, message) with self.assertRaisesRegexp(Exception, 'Invalid issuer in the Logout Request'): response_2.is_valid(request_data, raise_exceptions=True)
def testProcessResponseInvalidRequestId(self): """ Tests the process_response method of the OneLogin_Saml2_Auth class Case Invalid Response, Invalid requestID """ request_data = self.get_request() message = self.file_contents( join(self.data_path, 'responses', 'unsigned_response.xml.base64')) plain_message = b64decode(message) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace( 'http://stuff.com/endpoints/endpoints/acs.php', current_url) del request_data['get_data'] request_data['post_data'] = {'SAMLResponse': b64encode(plain_message)} auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) request_id = 'invalid' auth.process_response(request_id) self.assertEqual('No Signature found. SAML Response rejected', auth.get_last_error_reason()) auth.set_strict(True) auth.process_response(request_id) self.assertEqual(auth.get_errors(), ['invalid_response']) self.assertEqual( 'The InResponseTo of the Response: _57bcbf70-7b1f-012e-c821-782bcb13bb38, does not match the ID of the AuthNRequest sent by the SP: invalid', auth.get_last_error_reason()) valid_request_id = '_57bcbf70-7b1f-012e-c821-782bcb13bb38' auth.process_response(valid_request_id) self.assertEqual('No Signature found. SAML Response rejected', auth.get_last_error_reason())
def is_valid(self, request_data, request_id=None): """ Determines if the SAML LogoutResponse is valid :param request_id: The ID of the LogoutRequest sent by this SP to the IdP :type request_id: string :return: Returns if the SAML LogoutResponse is or not valid :rtype: boolean """ self.__error = None try: idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data['entityId'] get_data = request_data['get_data'] if self.__settings.is_strict(): res = OneLogin_Saml2_XML.validate_xml( self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if isinstance(res, str): raise Exception( 'Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd' ) security = self.__settings.get_security_data() # Check if the InResponseTo of the Logout Response matchs the ID of the Logout Request (requestId) if provided in_response_to = self.document.get('InResponseTo', None) if request_id is not None and in_response_to and in_response_to != request_id: raise Exception( 'The InResponseTo of the Logout Response: %s, does not match the ID of the Logout request sent by the SP: %s' % (in_response_to, request_id)) # Check issuer issuer = self.get_issuer() if issuer is not None and issuer != idp_entity_id: raise Exception('Invalid issuer in the Logout Request') current_url = OneLogin_Saml2_Utils.get_self_url_no_query( request_data) # Check destination destination = self.document.get('Destination', None) if destination and current_url not in destination: raise Exception( 'The LogoutRequest was received at $currentURL instead of $destination' ) if security['wantMessagesSigned']: if 'Signature' not in get_data: raise Exception( 'The Message of the Logout Response is not signed and the SP require it' ) return True # pylint: disable=R0801 except Exception as err: self.__error = str(err) debug = self.__settings.is_debug_active() if debug: print(err) return False
def testProcessSLOResponseValidDeletingSession(self): """ Tests the process_slo method of the OneLogin_Saml2_Auth class Case Valid Logout Response, validating deleting the local session """ request_data = self.get_request() message = self.file_contents( join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) # FIXME # if (!isset($_SESSION)) { # $_SESSION = array(); # } # $_SESSION['samltest'] = true; # In order to avoid the destination problem plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace( 'http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) request_data['get_data']['SAMLResponse'] = message auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) auth.set_strict(True) auth.process_slo(False) self.assertEqual(len(auth.get_errors()), 0)
def testProcessSLOResponseRequestId(self): """ Tests the process_slo method of the OneLogin_Saml2_Auth class Case Logout Response with valid and invalid Request ID """ request_data = self.get_request() message = self.file_contents( join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) # In order to avoid the destination problem plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace( 'http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) request_data['get_data']['SAMLResponse'] = message auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) request_id = 'wrongID' auth.set_strict(True) auth.process_slo(True, request_id) self.assertEqual(auth.get_errors(), ['invalid_logout_response']) request_id = 'ONELOGIN_21584ccdfaca36a145ae990442dcd96bfe60151e' auth.process_slo(True, request_id) self.assertEqual(len(auth.get_errors()), 0)
def testProcessResponseValid(self): """ Tests the process_response method of the OneLogin_Saml2_Auth class Case Valid Response, After processing the response the user is authenticated, attributes are returned, also has a nameID and the error array is empty """ request_data = self.get_request() message = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64')) plain_message = b64decode(message) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) del request_data['get_data'] request_data['post_data'] = { 'SAMLResponse': b64encode(plain_message) } auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) auth.process_response() self.assertTrue(auth.is_authenticated()) self.assertEqual(len(auth.get_errors()), 0) self.assertEqual('*****@*****.**', auth.get_nameid()) attributes = auth.get_attributes() self.assertNotEqual(len(attributes), 0) self.assertEqual(auth.get_attribute('mail'), attributes['mail']) auth.set_strict(True) auth.process_response() self.assertEqual(len(auth.get_errors()), 0)
def testProcessSLORequestSignedResponse(self): """ Tests the process_slo method of the OneLogin_Saml2_Auth class Case Valid Logout Request, validating the relayState, a signed LogoutResponse is created and a redirection executed """ settings_info = self.loadSettingsJSON() settings_info['security']['logoutResponseSigned'] = True request_data = self.get_request() message = self.file_contents( join(self.data_path, 'logout_requests', 'logout_request_deflated.xml.base64')) # In order to avoid the destination problem plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace( 'http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) request_data['get_data']['SAMLRequest'] = message request_data['get_data']['RelayState'] = 'http://relaystate.com' auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_info) auth.set_strict(True) target_url = auth.process_slo(False) parsed_query = parse_qs(urlparse(target_url)[4]) slo_url = settings_info['idp']['singleLogoutService']['url'] self.assertIn(slo_url, target_url) self.assertIn('SAMLResponse', parsed_query) self.assertIn('RelayState', parsed_query) self.assertIn('SigAlg', parsed_query) self.assertIn('Signature', parsed_query) self.assertIn('http://relaystate.com', parsed_query['RelayState']) self.assertIn(OneLogin_Saml2_Constants.RSA_SHA1, parsed_query['SigAlg'])
def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_policy=True): """ Initiates the SSO process. :param return_to: Optional argument. The target URL the user should be redirected to after login. :type return_to: string :param force_authn: Optional argument. When true the AuthNReuqest will set the ForceAuthn='true'. :type force_authn: bool :param is_passive: Optional argument. When true the AuthNReuqest will set the Ispassive='true'. :type is_passive: bool :param set_nameid_policy: Optional argument. When true the AuthNReuqest will set a nameIdPolicy element. :type set_nameid_policy: bool :returns: Redirection url :rtype: string """ authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive, set_nameid_policy) saml_request = authn_request.get_request() parameters = {'SAMLRequest': saml_request} if return_to is not None: parameters['RelayState'] = return_to else: parameters['RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query(self.__request_data) security = self.__settings.get_security_data() if security.get('authnRequestsSigned', False): self.add_request_signature(parameters, security['signatureAlgorithm']) return self.redirect_to(self.get_sso_url(), parameters)
def testIsInvalidIssuer(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutRequest Case Invalid Issuer """ request = self.file_contents( join(self.data_path, 'logout_requests', 'invalids', 'invalid_issuer.xml')) request_data = { 'http_host': 'example.com', 'script_name': 'index.html' } current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) request = request.replace( 'http://stuff.com/endpoints/endpoints/sls.php', current_url) settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) logout_request = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) self.assertTrue(logout_request.is_valid(request_data)) settings.set_strict(True) logout_request2 = OneLogin_Saml2_Logout_Request( settings, b64encode(request)) self.assertFalse(logout_request2.is_valid(request_data)) self.assertIn('Invalid issuer in the Logout Request', logout_request2.get_error())
def testIsValid(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutResponse """ request_data = { 'http_host': 'example.com', 'script_name': 'index.html', 'get_data': {} } settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) response = OneLogin_Saml2_Logout_Response(settings, message) self.assertTrue(response.is_valid(request_data)) settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, message) self.assertFalse(response_2.is_valid(request_data)) self.assertIn('The LogoutResponse was received at', response_2.get_error()) plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message_3 = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) response_3 = OneLogin_Saml2_Logout_Response(settings, message_3) self.assertTrue(response_3.is_valid(request_data))
def testIsInvalidNotOnOrAfter(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutRequest Case Invalid NotOnOrAfter """ request_data = { 'http_host': 'example.com', 'script_name': 'index.html' } request = self.file_contents( join(self.data_path, 'logout_requests', 'invalids', 'not_after_failed.xml')) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) request = request.replace( 'http://stuff.com/endpoints/endpoints/sls.php', current_url) settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) logout_request = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) self.assertTrue(logout_request.is_valid(request_data)) settings.set_strict(True) try: logout_request2 = OneLogin_Saml2_Logout_Request( settings, b64encode(request)) valid = logout_request2.is_valid(request_data) self.assertFalse(valid) except Exception as e: self.assertIn('Timing issues (please check your clock settings)', e.message)
def testIsValid(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutRequest """ request_data = { 'http_host': 'example.com', 'script_name': 'index.html' } request = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml')) settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) logout_request = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) self.assertTrue(logout_request.is_valid(request_data)) settings.set_strict(True) logout_request2 = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) self.assertFalse(logout_request2.is_valid(request_data)) settings.set_strict(False) dom = parseString(request) logout_request3 = OneLogin_Saml2_Logout_Request(settings, b64encode(dom.toxml())) self.assertTrue(logout_request3.is_valid(request_data)) settings.set_strict(True) logout_request4 = OneLogin_Saml2_Logout_Request(settings, b64encode(dom.toxml())) self.assertFalse(logout_request4.is_valid(request_data)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) request = request.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) logout_request5 = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) self.assertTrue(logout_request5.is_valid(request_data))
def logout(self, return_to=None): """ Initiates the SLO process. :param return_to: Optional argument. The target URL the user should be redirected to after logout. :type return_to: string :returns: Redirection url """ slo_url = self.get_slo_url() if slo_url is None: raise OneLogin_Saml2_Error( 'The IdP does not support Single Log Out', OneLogin_Saml2_Error.SAML_SINGLE_LOGOUT_NOT_SUPPORTED ) logout_request = OneLogin_Saml2_Logout_Request(self.__settings) saml_request = logout_request.get_request() parameters = {'SAMLRequest': logout_request.get_request()} if return_to is not None: parameters['RelayState'] = return_to else: parameters['RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query(self.__request_data) security = self.__settings.get_security_data() if security.get('logoutRequestSigned', False): parameters['SigAlg'] = OneLogin_Saml2_Constants.RSA_SHA1 parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState']) return self.redirect_to(slo_url, parameters)
def testIsInValidRequestId(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutResponse Case invalid request Id """ request_data = { 'http_host': 'example.com', 'script_name': 'index.html', 'get_data': {} } settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) message = self.file_contents( join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) plain_message = compat.to_string( OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace( 'http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) request_id = 'invalid_request_id' settings.set_strict(False) response = OneLogin_Saml2_Logout_Response(settings, message) self.assertTrue(response.is_valid(request_data, request_id)) settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, message) try: valid = response_2.is_valid(request_data, request_id) self.assertFalse(valid) except Exception as e: self.assertIn('The InResponseTo of the Logout Response:', str(e))
def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_policy=True): """ Initiates the SSO process. :param return_to: Optional argument. The target URL the user should be redirected to after login. :type return_to: string :param force_authn: Optional argument. When true the AuthNReuqest will set the ForceAuthn='true'. :type force_authn: bool :param is_passive: Optional argument. When true the AuthNReuqest will set the Ispassive='true'. :type is_passive: bool :param set_nameid_policy: Optional argument. When true the AuthNReuqest will set a nameIdPolicy element. :type set_nameid_policy: bool :returns: Redirection url :rtype: string """ authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive, set_nameid_policy) self.__last_request_id = authn_request.get_id() saml_request = authn_request.get_request() parameters = {'SAMLRequest': saml_request} if return_to is not None: parameters['RelayState'] = return_to else: parameters['RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query(self.__request_data) security = self.__settings.get_security_data() if security.get('authnRequestsSigned', False): parameters['SigAlg'] = security['signatureAlgorithm'] parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState'], security['signatureAlgorithm']) # HTTP-POST binding requires generation of a form if self.get_settings().get_idp_data()['singleSignOnService'].get('binding', None) == 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST': log.debug("Generating AuthnRequest HTTP-POST binding form") # Return HTML form template_file = open(join(dirname(__file__), 'templates/authn_request.html')) template_text = template_file.read() template = Template(template_text) context = { 'sso_url': self.get_sso_url(), 'saml_request': saml_request, 'relay_state': parameters['RelayState'] } html = template.render(context) log.debug("Generated HTML: %s" % html) return html return self.redirect_to(self.get_sso_url(), parameters)
def process_slo(self, keep_local_session=False, request_id=None, delete_session_cb=None): """ Process the SAML Logout Response / Logout Request sent by the IdP. :param keep_local_session: When false will destroy the local session, otherwise will destroy it :type keep_local_session: bool :param request_id: The ID of the LogoutRequest sent by this SP to the IdP :type request_id: string :returns: Redirection url """ self.__errors = [] if 'get_data' in self.__request_data and 'SAMLResponse' in self.__request_data['get_data']: logout_response = OneLogin_Saml2_Logout_Response(self.__settings, self.__request_data['get_data']['SAMLResponse']) if not logout_response.is_valid(self.__request_data, request_id): self.__errors.append('invalid_logout_response') self.__error_reason = logout_response.get_error() elif logout_response.get_status() != OneLogin_Saml2_Constants.STATUS_SUCCESS: self.__errors.append('logout_not_success') elif not keep_local_session: OneLogin_Saml2_Utils.delete_local_session(delete_session_cb) elif 'get_data' in self.__request_data and 'SAMLRequest' in self.__request_data['get_data']: logout_request = OneLogin_Saml2_Logout_Request(self.__settings, self.__request_data['get_data']['SAMLRequest']) if not logout_request.is_valid(self.__request_data): self.__errors.append('invalid_logout_request') self.__error_reason = logout_request.get_error() else: if not keep_local_session: OneLogin_Saml2_Utils.delete_local_session(delete_session_cb) in_response_to = logout_request.id response_builder = OneLogin_Saml2_Logout_Response(self.__settings) response_builder.build(in_response_to) logout_response = response_builder.get_response() parameters = {'SAMLResponse': logout_response} if 'RelayState' in self.__request_data['get_data']: parameters['RelayState'] = self.__request_data['get_data']['RelayState'] else: parameters['RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query(self.__request_data) security = self.__settings.get_security_data() if 'logoutResponseSigned' in security and security['logoutResponseSigned']: parameters['SigAlg'] = security['signatureAlgorithm'] parameters['Signature'] = self.build_response_signature(logout_response, parameters.get('RelayState', None), security['signatureAlgorithm']) return self.redirect_to(self.get_slo_url(), parameters) else: self.__errors.append('invalid_binding') raise OneLogin_Saml2_Error( 'SAML LogoutRequest/LogoutResponse not found. Only supported HTTP_REDIRECT Binding', OneLogin_Saml2_Error.SAML_LOGOUTMESSAGE_NOT_FOUND )
def logout(self, return_to=None, name_id=None, session_index=None, nq=None, name_id_format=None): """ Initiates the SLO process. :param return_to: Optional argument. The target URL the user should be redirected to after logout. :type return_to: string :param name_id: The NameID that will be set in the LogoutRequest. :type name_id: string :param session_index: SessionIndex that identifies the session of the user. :type session_index: string :param nq: IDP Name Qualifier :type: string :param name_id_format: The NameID Format that will be set in the LogoutRequest. :type: string :returns: Redirection url """ slo_url = self.get_slo_url() if slo_url is None: raise OneLogin_Saml2_Error( 'The IdP does not support Single Log Out', OneLogin_Saml2_Error.SAML_SINGLE_LOGOUT_NOT_SUPPORTED ) if name_id is None and self.__nameid is not None: name_id = self.__nameid if name_id_format is None and self.__nameid_format is not None: name_id_format = self.__nameid_format logout_request = OneLogin_Saml2_Logout_Request( self.__settings, name_id=name_id, session_index=session_index, nq=nq, name_id_format=name_id_format ) self.__last_request = logout_request.get_xml() self.__last_request_id = logout_request.id saml_request = logout_request.get_request() parameters = {'SAMLRequest': logout_request.get_request()} if return_to is not None: parameters['RelayState'] = return_to else: parameters['RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query(self.__request_data) security = self.__settings.get_security_data() if security.get('logoutRequestSigned', False): parameters['SigAlg'] = security['signatureAlgorithm'] parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState'], security['signatureAlgorithm']) return self.redirect_to(slo_url, parameters)
def is_valid(self, request_data, request_id=None): """ Determines if the SAML LogoutResponse is valid :param request_id: The ID of the LogoutRequest sent by this SP to the IdP :type request_id: string :return: Returns if the SAML LogoutResponse is or not valid :rtype: boolean """ self.__error = None try: idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data['entityId'] get_data = request_data['get_data'] if self.__settings.is_strict(): res = OneLogin_Saml2_XML.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if isinstance(res, str): raise Exception('Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd') security = self.__settings.get_security_data() # Check if the InResponseTo of the Logout Response matches the ID of the Logout Request (requestId) if provided in_response_to = self.document.get('InResponseTo', None) if request_id is not None and in_response_to and in_response_to != request_id: raise Exception('The InResponseTo of the Logout Response: %s, does not match the ID of the Logout request sent by the SP: %s' % (in_response_to, request_id)) # Check issuer issuer = self.get_issuer() if issuer is not None and issuer != idp_entity_id: raise Exception('Invalid issuer in the Logout Request') current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) # Check destination destination = self.document.get('Destination', None) if destination and current_url not in destination: raise Exception('The LogoutRequest was received at $currentURL instead of $destination') if security['wantMessagesSigned']: if 'Signature' not in get_data: raise Exception('The Message of the Logout Response is not signed and the SP require it') return True # pylint: disable=R0801 except Exception as err: self.__error = str(err) debug = self.__settings.is_debug_active() if debug: print(err) return False
def testProcessSLORequestDeletingSession(self): """ Tests the process_slo method of the OneLogin_Saml2_Auth class Case Valid Logout Request, validating that the local session is deleted, a LogoutResponse is created and a redirection executed """ settings_info = self.loadSettingsJSON() request_data = self.get_request() message = self.file_contents( join(self.data_path, 'logout_requests', 'logout_request_deflated.xml.base64')) # In order to avoid the destination problem plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace( 'http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) request_data['get_data']['SAMLRequest'] = message # FIXME # if (!isset($_SESSION)) { # $_SESSION = array(); # } # $_SESSION['samltest'] = true; auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_info) auth.set_strict(True) target_url = auth.process_slo(True) parsed_query = parse_qs(urlparse(target_url)[4]) slo_url = settings_info['idp']['singleLogoutService']['url'] self.assertIn(slo_url, target_url) self.assertIn('SAMLResponse', parsed_query) self.assertNotIn('RelayState', parsed_query) # FIXME // Session is not alive # $this->assertFalse(isset($_SESSION['samltest'])); # $_SESSION['samltest'] = true; auth.set_strict(True) target_url_2 = auth.process_slo(True) target_url_2 = auth.process_slo(True) parsed_query_2 = parse_qs(urlparse(target_url_2)[4]) slo_url = settings_info['idp']['singleLogoutService']['url'] self.assertIn(slo_url, target_url_2) self.assertIn('SAMLResponse', parsed_query_2) self.assertNotIn('RelayState', parsed_query_2)
def testProcessSLORequestNotOnOrAfterFailed(self): """ Tests the process_slo method of the OneLogin_Saml2_Auth class Case Logout Request NotOnOrAfter failed """ request_data = self.get_request() message = self.file_contents(join(self.data_path, 'logout_requests', 'invalids', 'not_after_failed.xml.base64')) # In order to avoid the destination problem plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) request_data['get_data']['SAMLRequest'] = message auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) auth.set_strict(True) auth.process_slo(True) self.assertEqual(auth.get_errors(), ['invalid_logout_request'])
def testProcessSLORequestNotOnOrAfterFailed(self): """ Tests the process_slo method of the OneLogin_Saml2_Auth class Case Logout Request NotOnOrAfter failed """ request_data = self.get_request() message = self.file_contents(join(self.data_path, 'logout_requests', 'invalids', 'not_after_failed.xml.base64')) # In order to avoid the destination problem plain_message = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) request_data['get_data']['SAMLRequest'] = message auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) auth.set_strict(True) auth.process_slo(True) self.assertEqual(auth.get_errors(), ['invalid_logout_request'])
def testProcessSLORequestDeletingSession(self): """ Tests the process_slo method of the OneLogin_Saml2_Auth class Case Valid Logout Request, validating that the local session is deleted, a LogoutResponse is created and a redirection executed """ settings_info = self.loadSettingsJSON() request_data = self.get_request() message = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_deflated.xml.base64')) # In order to avoid the destination problem plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) request_data['get_data']['SAMLRequest'] = message # FIXME # if (!isset($_SESSION)) { # $_SESSION = array(); # } # $_SESSION['samltest'] = true; auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_info) auth.set_strict(True) target_url = auth.process_slo(True) parsed_query = parse_qs(urlparse(target_url)[4]) slo_url = settings_info['idp']['singleLogoutService']['url'] self.assertIn(slo_url, target_url) self.assertIn('SAMLResponse', parsed_query) self.assertNotIn('RelayState', parsed_query) # FIXME // Session is not alive # $this->assertFalse(isset($_SESSION['samltest'])); # $_SESSION['samltest'] = true; auth.set_strict(True) target_url_2 = auth.process_slo(True) target_url_2 = auth.process_slo(True) parsed_query_2 = parse_qs(urlparse(target_url_2)[4]) slo_url = settings_info['idp']['singleLogoutService']['url'] self.assertIn(slo_url, target_url_2) self.assertIn('SAMLResponse', parsed_query_2) self.assertNotIn('RelayState', parsed_query_2)
def testIsInvalidIssuer(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutRequest Case Invalid Issuer """ request = self.file_contents(join(self.data_path, 'logout_requests', 'invalids', 'invalid_issuer.xml')) request_data = { 'http_host': 'example.com', 'script_name': 'index.html' } current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) request = request.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) logout_request = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) self.assertTrue(logout_request.is_valid(request_data)) settings.set_strict(True) logout_request2 = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) self.assertFalse(logout_request2.is_valid(request_data)) self.assertIn('Invalid issuer in the Logout Request', logout_request2.get_error())
def testProcessSLOResponseNoSucess(self): """ Tests the process_slo method of the OneLogin_Saml2_Auth class Case Logout Response not sucess """ request_data = self.get_request() message = self.file_contents( join(self.data_path, "logout_responses", "invalids", "status_code_responder.xml.base64") ) # In order to avoid the destination problem plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace("http://stuff.com/endpoints/endpoints/sls.php", current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) request_data["get_data"]["SAMLResponse"] = message auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) auth.set_strict(True) auth.process_slo(True) self.assertEqual(auth.get_errors(), ["logout_not_success"])
def testIsInvalidNotOnOrAfter(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutRequest Case Invalid NotOnOrAfter """ request_data = { 'http_host': 'example.com', 'script_name': 'index.html' } request = self.file_contents(join(self.data_path, 'logout_requests', 'invalids', 'not_after_failed.xml')) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) request = request.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) self.assertTrue(OneLogin_Saml2_Logout_Request.is_valid(settings, request, request_data)) settings.set_strict(True) try: valid = OneLogin_Saml2_Logout_Request.is_valid(settings, request, request_data) self.assertFalse(valid) except Exception as e: self.assertIn('Timing issues (please check your clock settings)', e.message)
def testIsInvalidNotOnOrAfter(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutRequest Case Invalid NotOnOrAfter """ request_data = { 'http_host': 'example.com', 'script_name': 'index.html' } request = self.file_contents(join(self.data_path, 'logout_requests', 'invalids', 'not_after_failed.xml')) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) request = request.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) logout_request = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) self.assertTrue(logout_request.is_valid(request_data)) settings.set_strict(True) logout_request2 = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) self.assertFalse(logout_request2.is_valid(request_data)) self.assertIn('Could not validate timestamp: expired. Check system clock.', logout_request2.get_error())
def testProcessSLOResponseRequestId(self): """ Tests the process_slo method of the OneLogin_Saml2_Auth class Case Logout Response with valid and invalid Request ID """ request_data = self.get_request() message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) # In order to avoid the destination problem plain_message = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) request_data['get_data']['SAMLResponse'] = message auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) request_id = 'wrongID' auth.set_strict(True) auth.process_slo(True, request_id) self.assertEqual(auth.get_errors(), ['invalid_logout_response']) request_id = 'ONELOGIN_21584ccdfaca36a145ae990442dcd96bfe60151e' auth.process_slo(True, request_id) self.assertEqual(len(auth.get_errors()), 0)
def logout(self, return_to=None, name_id=None, session_index=None): """ Initiates the SLO process. :param return_to: Optional argument. The target URL the user should be redirected to after logout. :type return_to: string :param name_id: The NameID that will be set in the LogoutRequest. :type name_id: string :param session_index: SessionIndex that identifies the session of the user. :type session_index: string :returns: Redirection url """ slo_url = self.get_slo_url() if slo_url is None: raise OneLogin_Saml2_Error( "The IdP does not support Single Log Out", OneLogin_Saml2_Error.SAML_SINGLE_LOGOUT_NOT_SUPPORTED ) if name_id is None and self.__nameid is not None: name_id = self.__nameid logout_request = OneLogin_Saml2_Logout_Request(self.__settings, name_id=name_id, session_index=session_index) parameters = {"SAMLRequest": logout_request.get_request()} if return_to is not None: parameters["RelayState"] = return_to else: parameters["RelayState"] = OneLogin_Saml2_Utils.get_self_url_no_query(self.__request_data) security = self.__settings.get_security_data() if security.get("logoutRequestSigned", False): self.add_request_signature(parameters, security["signatureAlgorithm"]) return self.redirect_to(slo_url, parameters)
def is_valid(self, request_data): """ Checks if the Logout Request received is valid :param request_data: Request Data :type request_data: dict :return: If the Logout Request is or not valid :rtype: boolean """ self.__error = None lowercase_urlencoding = False try: dom = fromstring(self.__logout_request) idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data['entityId'] if 'get_data' in request_data.keys(): get_data = request_data['get_data'] else: get_data = {} if 'lowercase_urlencoding' in request_data.keys(): lowercase_urlencoding = request_data['lowercase_urlencoding'] if self.__settings.is_strict(): res = OneLogin_Saml2_Utils.validate_xml(dom, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if not isinstance(res, Document): raise Exception('Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd') security = self.__settings.get_security_data() current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) # Check NotOnOrAfter if dom.get('NotOnOrAfter', None): na = OneLogin_Saml2_Utils.parse_SAML_to_time(dom.get('NotOnOrAfter')) if na <= OneLogin_Saml2_Utils.now(): raise Exception('Timing issues (please check your clock settings)') # Check destination if dom.get('Destination', None): destination = dom.get('Destination') if destination != '': if current_url not in destination: raise Exception( 'The LogoutRequest was received at ' '%(currentURL)s instead of %(destination)s' % { 'currentURL': current_url, 'destination': destination, } ) # Check issuer issuer = OneLogin_Saml2_Logout_Request.get_issuer(dom) if issuer is not None and issuer != idp_entity_id: raise Exception('Invalid issuer in the Logout Request') if security['wantMessagesSigned']: if 'Signature' not in get_data: raise Exception('The Message of the Logout Request is not signed and the SP require it') if 'Signature' in get_data: if 'SigAlg' not in get_data: sign_alg = OneLogin_Saml2_Constants.RSA_SHA1 else: sign_alg = get_data['SigAlg'] signed_query = 'SAMLRequest=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLRequest', lowercase_urlencoding=lowercase_urlencoding) if 'RelayState' in get_data: signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding)) signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding)) if 'x509cert' not in idp_data or idp_data['x509cert'] is None: raise Exception('In order to validate the sign on the Logout Request, the x509cert of the IdP is required') cert = idp_data['x509cert'] if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg): raise Exception('Signature validation failed. Logout Request rejected') return True except Exception as err: # pylint: disable=R0801sign_alg self.__error = err.__str__() debug = self.__settings.is_debug_active() if debug: print err.__str__() return False
def is_valid(self, request_data, request_id=None, raise_exceptions=False): """ Validates the response object. :param request_data: Request Data :type request_data: dict :param request_id: Optional argument. The ID of the AuthNRequest sent by this SP to the IdP :type request_id: string :param raise_exceptions: Whether to return false on failure or raise an exception :type raise_exceptions: Boolean :returns: True if the SAML Response is valid, False if not :rtype: bool """ self.__error = None try: # Checks SAML version if self.document.get('Version', None) != '2.0': raise OneLogin_Saml2_ValidationError( 'Unsupported SAML version', OneLogin_Saml2_ValidationError.UNSUPPORTED_SAML_VERSION ) # Checks that ID exists if self.document.get('ID', None) is None: raise OneLogin_Saml2_ValidationError( 'Missing ID attribute on SAML Response', OneLogin_Saml2_ValidationError.MISSING_ID ) # Checks that the response only has one assertion if not self.validate_num_assertions(): raise OneLogin_Saml2_ValidationError( 'SAML Response must contain 1 assertion', OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_ASSERTIONS ) # Checks that the response has the SUCCESS status self.check_status() idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data['entityId'] sp_data = self.__settings.get_sp_data() sp_entity_id = sp_data['entityId'] signed_elements = self.process_signed_elements() has_signed_response = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP in signed_elements has_signed_assertion = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML in signed_elements if self.__settings.is_strict(): no_valid_xml_msg = 'Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd' res = OneLogin_Saml2_XML.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if isinstance(res, str): raise OneLogin_Saml2_ValidationError( no_valid_xml_msg, OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT ) # If encrypted, check also the decrypted document if self.encrypted: res = OneLogin_Saml2_XML.validate_xml(self.decrypted_document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if isinstance(res, str): raise OneLogin_Saml2_ValidationError( no_valid_xml_msg, OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT ) security = self.__settings.get_security_data() current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) # Check if the InResponseTo of the Response matchs the ID of the AuthNRequest (requestId) if provided in_response_to = self.document.get('InResponseTo', None) if in_response_to is not None and request_id is not None: if in_response_to != request_id: raise OneLogin_Saml2_ValidationError( 'The InResponseTo of the Response: %s, does not match the ID of the AuthNRequest sent by the SP: %s' % (in_response_to, request_id), OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO ) if not self.encrypted and security['wantAssertionsEncrypted']: raise OneLogin_Saml2_ValidationError( 'The assertion of the Response is not encrypted and the SP require it', OneLogin_Saml2_ValidationError.NO_ENCRYPTED_ASSERTION ) if security['wantNameIdEncrypted']: encrypted_nameid_nodes = self.__query_assertion('/saml:Subject/saml:EncryptedID/xenc:EncryptedData') if len(encrypted_nameid_nodes) != 1: raise OneLogin_Saml2_ValidationError( 'The NameID of the Response is not encrypted and the SP require it', OneLogin_Saml2_ValidationError.NO_ENCRYPTED_NAMEID ) # Checks that a Conditions element exists if not self.check_one_condition(): raise OneLogin_Saml2_ValidationError( 'The Assertion must include a Conditions element', OneLogin_Saml2_ValidationError.MISSING_CONDITIONS ) # Validates Assertion timestamps self.validate_timestamps(raise_exceptions=True) # Checks that an AuthnStatement element exists and is unique if not self.check_one_authnstatement(): raise OneLogin_Saml2_ValidationError( 'The Assertion must include an AuthnStatement element', OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_AUTHSTATEMENTS ) # Checks that there is at least one AttributeStatement if required attribute_statement_nodes = self.__query_assertion('/saml:AttributeStatement') if security.get('wantAttributeStatement', True) and not attribute_statement_nodes: raise OneLogin_Saml2_ValidationError( 'There is no AttributeStatement on the Response', OneLogin_Saml2_ValidationError.NO_ATTRIBUTESTATEMENT ) encrypted_attributes_nodes = self.__query_assertion('/saml:AttributeStatement/saml:EncryptedAttribute') if encrypted_attributes_nodes: raise OneLogin_Saml2_ValidationError( 'There is an EncryptedAttribute in the Response and this SP not support them', OneLogin_Saml2_ValidationError.ENCRYPTED_ATTRIBUTES ) # Checks destination destination = self.document.get('Destination', None) if destination: if not destination.startswith(current_url): # TODO: Review if following lines are required, since we can control the # request_data # current_url_routed = OneLogin_Saml2_Utils.get_self_routed_url_no_query(request_data) # if not destination.startswith(current_url_routed): raise OneLogin_Saml2_ValidationError( 'The response was received at %s instead of %s' % (current_url, destination), OneLogin_Saml2_ValidationError.WRONG_DESTINATION ) elif destination == '': raise OneLogin_Saml2_ValidationError( 'The response has an empty Destination value', OneLogin_Saml2_ValidationError.EMPTY_DESTINATION ) # Checks audience valid_audiences = self.get_audiences() if valid_audiences and sp_entity_id not in valid_audiences: raise OneLogin_Saml2_ValidationError( '%s is not a valid audience for this Response' % sp_entity_id, OneLogin_Saml2_ValidationError.WRONG_AUDIENCE ) # Checks the issuers issuers = self.get_issuers() for issuer in issuers: if issuer is None or issuer != idp_entity_id: raise OneLogin_Saml2_ValidationError( 'Invalid issuer in the Assertion/Response', OneLogin_Saml2_ValidationError.WRONG_ISSUER ) # Checks the session Expiration session_expiration = self.get_session_not_on_or_after() if session_expiration and session_expiration <= OneLogin_Saml2_Utils.now(): raise OneLogin_Saml2_ValidationError( 'The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response', OneLogin_Saml2_ValidationError.SESSION_EXPIRED ) # Checks the SubjectConfirmation, at least one SubjectConfirmation must be valid any_subject_confirmation = False subject_confirmation_nodes = self.__query_assertion('/saml:Subject/saml:SubjectConfirmation') for scn in subject_confirmation_nodes: method = scn.get('Method', None) if method and method != OneLogin_Saml2_Constants.CM_BEARER: continue sc_data = scn.find('saml:SubjectConfirmationData', namespaces=OneLogin_Saml2_Constants.NSMAP) if sc_data is None: continue else: irt = sc_data.get('InResponseTo', None) if in_response_to and irt and irt != in_response_to: continue recipient = sc_data.get('Recipient', None) if recipient and current_url not in recipient: continue nooa = sc_data.get('NotOnOrAfter', None) if nooa: parsed_nooa = OneLogin_Saml2_Utils.parse_SAML_to_time(nooa) if parsed_nooa <= OneLogin_Saml2_Utils.now(): continue nb = sc_data.get('NotBefore', None) if nb: parsed_nb = OneLogin_Saml2_Utils.parse_SAML_to_time(nb) if parsed_nb > OneLogin_Saml2_Utils.now(): continue any_subject_confirmation = True break if not any_subject_confirmation: raise OneLogin_Saml2_ValidationError( 'A valid SubjectConfirmation was not found on this Response', OneLogin_Saml2_ValidationError.WRONG_SUBJECTCONFIRMATION ) if security['wantAssertionsSigned'] and not has_signed_assertion: raise OneLogin_Saml2_ValidationError( 'The Assertion of the Response is not signed and the SP require it', OneLogin_Saml2_ValidationError.NO_SIGNED_ASSERTION ) if security['wantMessagesSigned'] and not has_signed_response: raise OneLogin_Saml2_ValidationError( 'The Message of the Response is not signed and the SP require it', OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE ) if not signed_elements or (not has_signed_response and not has_signed_assertion): raise OneLogin_Saml2_ValidationError( 'No Signature found. SAML Response rejected', OneLogin_Saml2_ValidationError.NO_SIGNATURE_FOUND ) else: cert = idp_data.get('x509cert', None) fingerprint = idp_data.get('certFingerprint', None) if fingerprint: fingerprint = OneLogin_Saml2_Utils.format_finger_print(fingerprint) fingerprintalg = idp_data.get('certFingerprintAlgorithm', None) # If find a Signature on the Response, validates it checking the original response if has_signed_response and not OneLogin_Saml2_Utils.validate_sign(self.document, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH, raise_exceptions=False): raise OneLogin_Saml2_ValidationError( 'Signature validation failed. SAML Response rejected', OneLogin_Saml2_ValidationError.INVALID_SIGNATURE ) document_check_assertion = self.decrypted_document if self.encrypted else self.document if has_signed_assertion and not OneLogin_Saml2_Utils.validate_sign(document_check_assertion, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH, raise_exceptions=False): raise OneLogin_Saml2_ValidationError( 'Signature validation failed. SAML Response rejected', OneLogin_Saml2_ValidationError.INVALID_SIGNATURE ) return True except Exception as err: self.__error = str(err) debug = self.__settings.is_debug_active() if debug: print(err) if raise_exceptions: raise return False
def is_valid(self, request_data, request_id=None): """ Validates the response object. :param request_data: Request Data :type request_data: dict :param request_id: Optional argument. The ID of the AuthNRequest sent by this SP to the IdP :type request_id: string :returns: True if the SAML Response is valid, False if not :rtype: bool """ self.__error = None try: # Checks SAML version if self.document.get('Version', None) != '2.0': raise Exception('Unsupported SAML version') # Checks that ID exists if self.document.get('ID', None) is None: raise Exception('Missing ID attribute on SAML Response') # Checks that the response only has one assertion if not self.validate_num_assertions(): raise Exception('SAML Response must contain 1 assertion') # Checks that the response has the SUCCESS status self.check_status() idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data.get('entityId', '') sp_data = self.__settings.get_sp_data() sp_entity_id = sp_data.get('entityId', '') signed_elements = self.process_signed_elements() if self.__settings.is_strict(): no_valid_xml_msg = 'Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd' res = OneLogin_Saml2_Utils.validate_xml(etree.tostring(self.document), 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if not isinstance(res, Document): raise Exception(no_valid_xml_msg) # If encrypted, check also the decrypted document if self.encrypted: res = OneLogin_Saml2_Utils.validate_xml(etree.tostring(self.decrypted_document), 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if not isinstance(res, Document): raise Exception(no_valid_xml_msg) security = self.__settings.get_security_data() current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) # Check if the InResponseTo of the Response matchs the ID of the AuthNRequest (requestId) if provided in_response_to = self.document.get('InResponseTo', None) if in_response_to and request_id: if in_response_to != request_id: raise Exception('The InResponseTo of the Response: %s, does not match the ID of the AuthNRequest sent by the SP: %s' % (in_response_to, request_id)) if not self.encrypted and security.get('wantAssertionsEncrypted', False): raise Exception('The assertion of the Response is not encrypted and the SP require it') if security.get('wantNameIdEncrypted', False): encrypted_nameid_nodes = self.__query_assertion('/saml:Subject/saml:EncryptedID/xenc:EncryptedData') if len(encrypted_nameid_nodes) == 0: raise Exception('The NameID of the Response is not encrypted and the SP require it') # Checks that there is at least one AttributeStatement if required attribute_statement_nodes = self.__query_assertion('/saml:AttributeStatement') if security.get('wantAttributeStatement', True) and not attribute_statement_nodes: raise Exception('There is no AttributeStatement on the Response') # Validates Assertion timestamps if not self.validate_timestamps(): raise Exception('Timing issues (please check your clock settings)') encrypted_attributes_nodes = self.__query_assertion('/saml:AttributeStatement/saml:EncryptedAttribute') if encrypted_attributes_nodes: raise Exception('There is an EncryptedAttribute in the Response and this SP not support them') # Checks destination destination = self.document.get('Destination', '') if destination: if not destination.startswith(current_url): # TODO: Review if following lines are required, since we can control the # request_data # current_url_routed = OneLogin_Saml2_Utils.get_self_routed_url_no_query(request_data) # if not destination.startswith(current_url_routed): raise Exception('The response was received at %s instead of %s' % (current_url, destination)) # Checks audience valid_audiences = self.get_audiences() if valid_audiences and sp_entity_id not in valid_audiences: raise Exception('%s is not a valid audience for this Response' % sp_entity_id) # Checks the issuers issuers = self.get_issuers() for issuer in issuers: if issuer is None or issuer != idp_entity_id: raise Exception('Invalid issuer in the Assertion/Response') # Checks the session Expiration session_expiration = self.get_session_not_on_or_after() if session_expiration and session_expiration <= OneLogin_Saml2_Utils.now(): raise Exception('The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response') # Checks the SubjectConfirmation, at least one SubjectConfirmation must be valid any_subject_confirmation = False subject_confirmation_nodes = self.__query_assertion('/saml:Subject/saml:SubjectConfirmation') for scn in subject_confirmation_nodes: method = scn.get('Method', None) if method and method != OneLogin_Saml2_Constants.CM_BEARER: continue sc_data = scn.find('saml:SubjectConfirmationData', namespaces=OneLogin_Saml2_Constants.NSMAP) if sc_data is None: continue else: irt = sc_data.get('InResponseTo', None) if in_response_to and irt and irt != in_response_to: continue recipient = sc_data.get('Recipient', None) if recipient and current_url not in recipient: continue nooa = sc_data.get('NotOnOrAfter', None) if nooa: parsed_nooa = OneLogin_Saml2_Utils.parse_SAML_to_time(nooa) if parsed_nooa <= OneLogin_Saml2_Utils.now(): continue nb = sc_data.get('NotBefore', None) if nb: parsed_nb = OneLogin_Saml2_Utils.parse_SAML_to_time(nb) if parsed_nb > OneLogin_Saml2_Utils.now(): continue any_subject_confirmation = True break if not any_subject_confirmation: raise Exception('A valid SubjectConfirmation was not found on this Response') if security.get('wantAssertionsSigned', False) and ('{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML) not in signed_elements: raise Exception('The Assertion of the Response is not signed and the SP require it') if security.get('wantMessagesSigned', False) and ('{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP) not in signed_elements: raise Exception('The Message of the Response is not signed and the SP require it') if len(signed_elements) > 0: if len(signed_elements) > 2: raise Exception('Too many Signatures found. SAML Response rejected') certs = idp_data.get('x509certs', []) fingerprint = idp_data.get('certFingerprint', None) fingerprintalg = idp_data.get('certFingerprintAlgorithm', None) # If find a Signature on the Response, validates it checking the original response if '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP in signed_elements: document_to_validate = self.document # Otherwise validates the assertion (decrypted assertion if was encrypted) else: if self.encrypted: document_to_validate = self.decrypted_document else: document_to_validate = self.document if certs: validated = any(OneLogin_Saml2_Utils.validate_sign(document_to_validate, cert, fingerprint, fingerprintalg) for cert in certs) else: validated = OneLogin_Saml2_Utils.validate_sign(document_to_validate, None, fingerprint, fingerprintalg) if not validated: raise Exception('Signature validation failed. SAML Response rejected') else: raise Exception('No Signature found. SAML Response rejected') return True except Exception as err: self.__error = err.__str__() debug = self.__settings.is_debug_active() if debug: print err.__str__() return False
def is_valid(self, request_data): """ Checks if the Logout Request recieved is valid :param request_data: Request Data :type request_data: dict :return: If the Logout Request is or not valid :rtype: boolean """ self.__error = None try: root = OneLogin_Saml2_XML.to_etree(self.__logout_request) idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data['entityId'] get_data = ('get_data' in request_data and request_data['get_data']) or dict() if self.__settings.is_strict(): res = OneLogin_Saml2_XML.validate_xml(root, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if isinstance(res, str): raise Exception('Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd') security = self.__settings.get_security_data() current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) # Check NotOnOrAfter if root.get('NotOnOrAfter', None): na = OneLogin_Saml2_Utils.parse_SAML_to_time(root.get('NotOnOrAfter')) if na <= OneLogin_Saml2_Utils.now(): raise Exception('Timing issues (please check your clock settings)') # Check destination if root.get('Destination', None): destination = root.get('Destination') if destination != '': if current_url not in destination: raise Exception( 'The LogoutRequest was received at ' '%(currentURL)s instead of %(destination)s' % { 'currentURL': current_url, 'destination': destination, } ) # Check issuer issuer = OneLogin_Saml2_Logout_Request.get_issuer(root) if issuer is not None and issuer != idp_entity_id: raise Exception('Invalid issuer in the Logout Request') if security['wantMessagesSigned']: if 'Signature' not in get_data: raise Exception('The Message of the Logout Request is not signed and the SP require it') return True except Exception as err: # pylint: disable=R0801 self.__error = str(err) debug = self.__settings.is_debug_active() if debug: print(err) return False
def testIsInValidSign(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutResponse """ request_data = { 'http_host': 'example.com', 'script_name': 'index.html', 'get_data': {} } settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) settings.set_strict(False) request_data['get_data'] = { 'SAMLResponse': 'fZJva8IwEMa/Ssl7TZrW/gnqGHMMwSlM8cXeyLU9NaxNQi9lfvxVZczB5ptwSe733MPdjQma2qmFPdjOvyE5awiDU1MbUpevCetaoyyQJmWgQVK+VOvH14WSQ6Fca70tbc1ukPsEEGHrtTUsmM8mbDfKUhnFci8gliGINI/yXIAAiYnsw6JIRgWWAKlkwRZb6skJ64V6nKjDuSEPxvdPIowHIhpIsQkTFaYqSt9ZMEPy2oC/UEfvHSnOnfZFV38MjR1oN7TtgRv8tAZre9CGV9jYkGtT4Wnoju6Bauprme/ebOyErZbPi9XLfLnDoohwhHGc5WVSVhjCKM6rBMpYQpWJrIizfZ4IZNPxuTPqYrmd/m+EdONqPOfy8yG5rhxv0EMFHs52xvxWaHyd3tqD7+j37clWGGyh7vD+POiSrdZdWSIR49NrhR9R/teGTL8A', 'RelayState': 'https://pitbulk.no-ip.org/newonelogin/demo1/index.php', 'SigAlg': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', 'Signature': 'vfWbbc47PkP3ejx4bjKsRX7lo9Ml1WRoE5J5owF/0mnyKHfSY6XbhO1wwjBV5vWdrUVX+xp6slHyAf4YoAsXFS0qhan6txDiZY4Oec6yE+l10iZbzvie06I4GPak4QrQ4gAyXOSzwCrRmJu4gnpeUxZ6IqKtdrKfAYRAcVfNKGA=' } response = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) self.assertTrue(response.is_valid(request_data)) relayState = request_data['get_data']['RelayState'] del request_data['get_data']['RelayState'] inv_response = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) self.assertFalse(inv_response.is_valid(request_data)) request_data['get_data']['RelayState'] = relayState settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) self.assertFalse(response_2.is_valid(request_data)) self.assertIn('Invalid issuer in the Logout Response', response_2.get_error()) settings.set_strict(False) old_signature = request_data['get_data']['Signature'] request_data['get_data']['Signature'] = 'vfWbbc47PkP3ejx4bjKsRX7lo9Ml1WRoE5J5owF/0mnyKHfSY6XbhO1wwjBV5vWdrUVX+xp6slHyAf4YoAsXFS0qhan6txDiZY4Oec6yE+l10iZbzvie06I4GPak4QrQ4gAyXOSzwCrRmJu4gnpeUxZ6IqKtdrKfAYRAcVf3333=' response_3 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) self.assertFalse(response_3.is_valid(request_data)) self.assertIn('Signature validation failed. Logout Response rejected', response_3.get_error()) request_data['get_data']['Signature'] = old_signature old_signature_algorithm = request_data['get_data']['SigAlg'] del request_data['get_data']['SigAlg'] response_4 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) self.assertTrue(response_4.is_valid(request_data)) request_data['get_data']['RelayState'] = 'http://example.com/relaystate' response_5 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) self.assertFalse(response_5.is_valid(request_data)) self.assertIn('Signature validation failed. Logout Response rejected', response_5.get_error()) settings.set_strict(True) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message_6 = OneLogin_Saml2_Utils.decode_base64_and_inflate(request_data['get_data']['SAMLResponse']) plain_message_6 = plain_message_6.replace('https://pitbulk.no-ip.org/newonelogin/demo1/index.php?sls', current_url) plain_message_6 = plain_message_6.replace('https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php', 'http://idp.example.com/') request_data['get_data']['SAMLResponse'] = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message_6) response_6 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) self.assertFalse(response_6.is_valid(request_data)) self.assertIn('Signature validation failed. Logout Response rejected', response_6.get_error()) settings.set_strict(False) response_7 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) self.assertFalse(response_7.is_valid(request_data)) self.assertIn('Signature validation failed. Logout Response rejected', response_7.get_error()) request_data['get_data']['SigAlg'] = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1' response_8 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) self.assertFalse(response_8.is_valid(request_data)) self.assertIn('Signature validation failed. Logout Response rejected', response_8.get_error()) settings_info = self.loadSettingsJSON() settings_info['strict'] = True settings_info['security']['wantMessagesSigned'] = True settings = OneLogin_Saml2_Settings(settings_info) request_data['get_data']['SigAlg'] = old_signature_algorithm old_signature = request_data['get_data']['Signature'] del request_data['get_data']['Signature'] request_data['get_data']['SAMLResponse'] = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message_6) response_9 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) self.assertFalse(response_9.is_valid(request_data)) self.assertIn('The Message of the Logout Response is not signed and the SP require it', response_9.get_error()) request_data['get_data']['Signature'] = old_signature settings_info['idp']['certFingerprint'] = 'afe71c28ef740bc87425be13a2263d37971da1f9' del settings_info['idp']['x509cert'] settings_2 = OneLogin_Saml2_Settings(settings_info) response_10 = OneLogin_Saml2_Logout_Response(settings_2, request_data['get_data']['SAMLResponse']) self.assertFalse(response_10.is_valid(request_data)) self.assertIn('In order to validate the sign on the Logout Response, the x509cert of the IdP is required', response_10.get_error())
def is_valid(self, request_data, raise_exceptions=False): """ Checks if the Logout Request received is valid :param request_data: Request Data :type request_data: dict :param raise_exceptions: Whether to return false on failure or raise an exception :type raise_exceptions: Boolean :return: If the Logout Request is or not valid :rtype: boolean """ self.__error = None try: root = OneLogin_Saml2_XML.to_etree(self.__logout_request) idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data['entityId'] get_data = ('get_data' in request_data and request_data['get_data']) or dict() if self.__settings.is_strict(): res = OneLogin_Saml2_XML.validate_xml(root, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if isinstance(res, str): raise OneLogin_Saml2_ValidationError( 'Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd', OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT ) security = self.__settings.get_security_data() current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) # Check NotOnOrAfter if root.get('NotOnOrAfter', None): na = OneLogin_Saml2_Utils.parse_SAML_to_time(root.get('NotOnOrAfter')) if na <= OneLogin_Saml2_Utils.now(): raise OneLogin_Saml2_ValidationError( 'Could not validate timestamp: expired. Check system clock.)', OneLogin_Saml2_ValidationError.RESPONSE_EXPIRED ) # Check destination if root.get('Destination', None): destination = root.get('Destination') if destination != '': if current_url not in destination: raise OneLogin_Saml2_ValidationError( 'The LogoutRequest was received at ' '%(currentURL)s instead of %(destination)s' % { 'currentURL': current_url, 'destination': destination, }, OneLogin_Saml2_ValidationError.WRONG_DESTINATION ) # Check issuer issuer = OneLogin_Saml2_Logout_Request.get_issuer(root) if issuer is not None and issuer != idp_entity_id: raise OneLogin_Saml2_ValidationError( 'Invalid issuer in the Logout Request (expected %(idpEntityId)s, got %(issuer)s)' % { 'idpEntityId': idp_entity_id, 'issuer': issuer }, OneLogin_Saml2_ValidationError.WRONG_ISSUER ) if security['wantMessagesSigned']: if 'Signature' not in get_data: raise OneLogin_Saml2_ValidationError( 'The Message of the Logout Request is not signed and the SP require it', OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE ) return True except Exception as err: # pylint: disable=R0801 self.__error = str(err) debug = self.__settings.is_debug_active() if debug: print(err) if raise_exceptions: raise return False
def is_valid(self, request_data): """ Checks if the Logout Request recieved is valid :param request_data: Request Data :type request_data: dict :return: If the Logout Request is or not valid :rtype: boolean """ self.__error = None try: dom = fromstring(self.__logout_request) idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data['entityId'] if 'get_data' in request_data.keys(): get_data = request_data['get_data'] else: get_data = {} if self.__settings.is_strict(): res = OneLogin_Saml2_Utils.validate_xml( dom, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if not isinstance(res, Document): raise Exception( 'Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd' ) security = self.__settings.get_security_data() current_url = OneLogin_Saml2_Utils.get_self_url_no_query( request_data) # Check NotOnOrAfter if dom.get('NotOnOrAfter', None): na = OneLogin_Saml2_Utils.parse_SAML_to_time( dom.get('NotOnOrAfter')) if na <= OneLogin_Saml2_Utils.now(): raise Exception( 'Timing issues (please check your clock settings)') # Check destination if dom.get('Destination', None): destination = dom.get('Destination') if destination != '': if current_url not in destination: raise Exception( 'The LogoutRequest was received at ' '%(currentURL)s instead of %(destination)s' % { 'currentURL': current_url, 'destination': destination, }) # Check issuer issuer = OneLogin_Saml2_Logout_Request.get_issuer(dom) if issuer is not None and issuer != idp_entity_id: raise Exception('Invalid issuer in the Logout Request') if security['wantMessagesSigned']: if 'Signature' not in get_data: raise Exception( 'The Message of the Logout Request is not signed and the SP require it' ) if 'Signature' in get_data: if 'SigAlg' not in get_data: sign_alg = OneLogin_Saml2_Constants.RSA_SHA1 else: sign_alg = get_data['SigAlg'] signed_query = 'SAMLRequest=%s' % quote_plus( get_data['SAMLRequest']) if 'RelayState' in get_data: signed_query = '%s&RelayState=%s' % ( signed_query, quote_plus(get_data['RelayState'])) signed_query = '%s&SigAlg=%s' % (signed_query, quote_plus(sign_alg)) if 'x509cert' not in idp_data or idp_data['x509cert'] is None: raise Exception( 'In order to validate the sign on the Logout Request, the x509cert of the IdP is required' ) cert = idp_data['x509cert'] if not OneLogin_Saml2_Utils.validate_binary_sign( signed_query, b64decode(get_data['Signature']), cert, sign_alg): raise Exception( 'Signature validation failed. Logout Request rejected') return True except Exception as err: # pylint: disable=R0801sign_alg self.__error = err.__str__() debug = self.__settings.is_debug_active() if debug: print err.__str__() return False
def is_valid(self, request_data, request_id=None, raise_exceptions=False): """ Determines if the SAML LogoutResponse is valid :param request_id: The ID of the LogoutRequest sent by this SP to the IdP :type request_id: string :param raise_exceptions: Whether to return false on failure or raise an exception :type raise_exceptions: Boolean :return: Returns if the SAML LogoutResponse is or not valid :rtype: boolean """ self.__error = None try: idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data['entityId'] get_data = request_data['get_data'] if self.__settings.is_strict(): res = OneLogin_Saml2_XML.validate_xml( self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if isinstance(res, str): raise OneLogin_Saml2_ValidationError( 'Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd', OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT) security = self.__settings.get_security_data() # Check if the InResponseTo of the Logout Response matches the ID of the Logout Request (requestId) if provided in_response_to = self.document.get('InResponseTo', None) if request_id is not None and in_response_to and in_response_to != request_id: raise OneLogin_Saml2_ValidationError( 'The InResponseTo of the Logout Response: %s, does not match the ID of the Logout request sent by the SP: %s' % (in_response_to, request_id), OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO) # Check issuer issuer = self.get_issuer() if issuer is not None and issuer != idp_entity_id: raise OneLogin_Saml2_ValidationError( 'Invalid issuer in the Logout Request', OneLogin_Saml2_ValidationError.WRONG_ISSUER) current_url = OneLogin_Saml2_Utils.get_self_url_no_query( request_data) # Check destination destination = self.document.get('Destination', None) if destination and current_url not in destination: raise OneLogin_Saml2_ValidationError( 'The LogoutResponse was received at %s instead of %s' % (current_url, destination), OneLogin_Saml2_ValidationError.WRONG_DESTINATION) if security['wantMessagesSigned']: if 'Signature' not in get_data: raise OneLogin_Saml2_ValidationError( 'The Message of the Logout Response is not signed and the SP require it', OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE) return True # pylint: disable=R0801 except Exception as err: self.__error = str(err) debug = self.__settings.is_debug_active() if debug: print(err) if raise_exceptions: raise return False
def is_valid(self, request_data): """ Checks if the Logout Request received is valid :param request_data: Request Data :type request_data: dict :return: If the Logout Request is or not valid :rtype: boolean """ self.__error = None try: root = OneLogin_Saml2_XML.to_etree(self.__logout_request) idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data['entityId'] get_data = ('get_data' in request_data and request_data['get_data']) or dict() if self.__settings.is_strict(): res = OneLogin_Saml2_XML.validate_xml( root, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if isinstance(res, str): raise Exception( 'Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd' ) security = self.__settings.get_security_data() current_url = OneLogin_Saml2_Utils.get_self_url_no_query( request_data) # Check NotOnOrAfter if root.get('NotOnOrAfter', None): na = OneLogin_Saml2_Utils.parse_SAML_to_time( root.get('NotOnOrAfter')) if na <= OneLogin_Saml2_Utils.now(): raise Exception( 'Timing issues (please check your clock settings)') # Check destination if root.get('Destination', None): destination = root.get('Destination') if destination != '': if current_url not in destination: raise Exception( 'The LogoutRequest was received at ' '%(currentURL)s instead of %(destination)s' % { 'currentURL': current_url, 'destination': destination, }) # Check issuer issuer = OneLogin_Saml2_Logout_Request.get_issuer(root) if issuer is not None and issuer != idp_entity_id: raise Exception('Invalid issuer in the Logout Request') if security['wantMessagesSigned']: if 'Signature' not in get_data: raise Exception( 'The Message of the Logout Request is not signed and the SP require it' ) return True except Exception as err: # pylint: disable=R0801 self.__error = str(err) debug = self.__settings.is_debug_active() if debug: print(err) return False
def is_valid(self, request_data, request_id=None): """ Validates the response object. :param request_data: Request Data :type request_data: dict :param request_id: Optional argument. The ID of the AuthNRequest sent by this SP to the IdP :type request_id: string :returns: True if the SAML Response is valid, False if not :rtype: bool """ self.__error = None try: # Checks SAML version if self.document.get('Version', None) != '2.0': raise Exception('Unsupported SAML version') # Checks that ID exists if self.document.get('ID', None) is None: raise Exception('Missing ID attribute on SAML Response') # Checks that the response only has one assertion if not self.validate_num_assertions(): raise Exception('SAML Response must contain 1 assertion') # Checks that the response has the SUCCESS status self.check_status() idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data.get('entityId', '') sp_data = self.__settings.get_sp_data() sp_entity_id = sp_data.get('entityId', '') sign_nodes = self.__query('//ds:Signature') signed_elements = [] for sign_node in sign_nodes: signed_elements.append(sign_node.getparent().tag) if self.__settings.is_strict(): res = OneLogin_Saml2_Utils.validate_xml( etree.tostring(self.document), 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if not isinstance(res, Document): raise Exception( 'Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd' ) security = self.__settings.get_security_data() current_url = OneLogin_Saml2_Utils.get_self_url_no_query( request_data) # Check if the InResponseTo of the Response matchs the ID of the AuthNRequest (requestId) if provided in_response_to = self.document.get('InResponseTo', None) if in_response_to and request_id: if in_response_to != request_id: raise Exception( 'The InResponseTo of the Response: %s, does not match the ID of the AuthNRequest sent by the SP: %s' % (in_response_to, request_id)) if not self.encrypted and security.get( 'wantAssertionsEncrypted', False): raise Exception( 'The assertion of the Response is not encrypted and the SP require it' ) if security.get('wantNameIdEncrypted', False): encrypted_nameid_nodes = self.__query_assertion( '/saml:Subject/saml:EncryptedID/xenc:EncryptedData') if len(encrypted_nameid_nodes) == 0: raise Exception( 'The NameID of the Response is not encrypted and the SP require it' ) # Checks that there is at least one AttributeStatement if required attribute_statement_nodes = self.__query_assertion( '/saml:AttributeStatement') if security.get('wantAttributeStatement', True) and not attribute_statement_nodes: raise Exception( 'There is no AttributeStatement on the Response') # Validates Assertion timestamps if not self.validate_timestamps(): raise Exception( 'Timing issues (please check your clock settings)') encrypted_attributes_nodes = self.__query_assertion( '/saml:AttributeStatement/saml:EncryptedAttribute') if encrypted_attributes_nodes: raise Exception( 'There is an EncryptedAttribute in the Response and this SP not support them' ) # Checks destination destination = self.document.get('Destination', '') if destination: if not destination.startswith(current_url): # TODO: Review if following lines are required, since we can control the # request_data # current_url_routed = OneLogin_Saml2_Utils.get_self_routed_url_no_query(request_data) # if not destination.startswith(current_url_routed): raise Exception( 'The response was received at %s instead of %s' % (current_url, destination)) # Checks audience valid_audiences = self.get_audiences() if valid_audiences and sp_entity_id not in valid_audiences: raise Exception( '%s is not a valid audience for this Response' % sp_entity_id) # Checks the issuers issuers = self.get_issuers() for issuer in issuers: if issuer is None or issuer != idp_entity_id: raise Exception( 'Invalid issuer in the Assertion/Response') # Checks the session Expiration session_expiration = self.get_session_not_on_or_after() if session_expiration and session_expiration <= OneLogin_Saml2_Utils.now( ): raise Exception( 'The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response' ) # Checks the SubjectConfirmation, at least one SubjectConfirmation must be valid any_subject_confirmation = False subject_confirmation_nodes = self.__query_assertion( '/saml:Subject/saml:SubjectConfirmation') for scn in subject_confirmation_nodes: method = scn.get('Method', None) if method and method != OneLogin_Saml2_Constants.CM_BEARER: continue sc_data = scn.find( 'saml:SubjectConfirmationData', namespaces=OneLogin_Saml2_Constants.NSMAP) if sc_data is None: continue else: irt = sc_data.get('InResponseTo', None) if irt != in_response_to: continue recipient = sc_data.get('Recipient', None) if recipient and current_url not in recipient: continue nooa = sc_data.get('NotOnOrAfter', None) if nooa: parsed_nooa = OneLogin_Saml2_Utils.parse_SAML_to_time( nooa) if parsed_nooa <= OneLogin_Saml2_Utils.now(): continue nb = sc_data.get('NotBefore', None) if nb: parsed_nb = OneLogin_Saml2_Utils.parse_SAML_to_time( nb) if parsed_nb > OneLogin_Saml2_Utils.now(): continue any_subject_confirmation = True break if not any_subject_confirmation: raise Exception( 'A valid SubjectConfirmation was not found on this Response' ) if security.get('wantAssertionsSigned', False) and ( '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML ) not in signed_elements: raise Exception( 'The Assertion of the Response is not signed and the SP require it' ) if security.get('wantMessagesSigned', False) and ( '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP ) not in signed_elements: raise Exception( 'The Message of the Response is not signed and the SP require it' ) if len(signed_elements) > 0: if len(signed_elements) > 2: raise Exception( 'Too many Signatures found. SAML Response rejected' ) cert = idp_data.get('x509cert', None) fingerprint = idp_data.get('certFingerprint', None) fingerprintalg = idp_data.get('certFingerprintAlgorithm', None) # If find a Signature on the Response, validates it checking the original response if '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP in signed_elements: document_to_validate = self.document # Otherwise validates the assertion (decrypted assertion if was encrypted) else: if self.encrypted: document_to_validate = self.decrypted_document else: document_to_validate = self.document if not OneLogin_Saml2_Utils.validate_sign( document_to_validate, cert, fingerprint, fingerprintalg): raise Exception( 'Signature validation failed. SAML Response rejected' ) else: raise Exception( 'No Signature found. SAML Response rejected') return True except Exception as err: self.__error = err.__str__() debug = self.__settings.is_debug_active() if debug: print err.__str__() return False
def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_policy=True): """ Initiates the SSO process. :param return_to: Optional argument. The target URL the user should be redirected to after login. :type return_to: string :param force_authn: Optional argument. When true the AuthNReuqest will set the ForceAuthn='true'. :type force_authn: bool :param is_passive: Optional argument. When true the AuthNReuqest will set the Ispassive='true'. :type is_passive: bool :param set_nameid_policy: Optional argument. When true the AuthNReuqest will set a nameIdPolicy element. :type set_nameid_policy: bool :returns: Redirection url :rtype: string """ authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive, set_nameid_policy) self.__last_request_id = authn_request.get_id() saml_request = authn_request.get_request() parameters = {'SAMLRequest': saml_request} if return_to is not None: parameters['RelayState'] = return_to else: parameters[ 'RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query( self.__request_data) security = self.__settings.get_security_data() if security.get('authnRequestsSigned', False): parameters['SigAlg'] = security['signatureAlgorithm'] parameters['Signature'] = self.build_request_signature( saml_request, parameters['RelayState'], security['signatureAlgorithm']) # HTTP-POST binding requires generation of a form if self.get_settings().get_idp_data()['singleSignOnService'].get( 'binding', None) == 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST': log.debug("Generating AuthnRequest HTTP-POST binding form") # Return HTML form template_file = open( join(dirname(__file__), 'templates/authn_request.html')) template_text = template_file.read() template = Template(template_text) context = { 'sso_url': self.get_sso_url(), 'saml_request': saml_request, 'relay_state': parameters['RelayState'] } html = template.render(context) log.debug("Generated HTML: %s" % html) return html return self.redirect_to(self.get_sso_url(), parameters)
def process_slo(self, keep_local_session=False, request_id=None, delete_session_cb=None): """ Process the SAML Logout Response / Logout Request sent by the IdP. :param keep_local_session: When false will destroy the local session, otherwise will destroy it :type keep_local_session: bool :param request_id: The ID of the LogoutRequest sent by this SP to the IdP :type request_id: string :returns: Redirection url """ self.__errors = [] if 'get_data' in self.__request_data and 'SAMLResponse' in self.__request_data[ 'get_data']: logout_response = OneLogin_Saml2_Logout_Response( self.__settings, self.__request_data['get_data']['SAMLResponse']) if not logout_response.is_valid(self.__request_data, request_id): self.__errors.append('invalid_logout_response') self.__error_reason = logout_response.get_error() elif logout_response.get_status( ) != OneLogin_Saml2_Constants.STATUS_SUCCESS: self.__errors.append('logout_not_success') elif not keep_local_session: OneLogin_Saml2_Utils.delete_local_session(delete_session_cb) elif 'get_data' in self.__request_data and 'SAMLRequest' in self.__request_data[ 'get_data']: logout_request = OneLogin_Saml2_Logout_Request( self.__settings, self.__request_data['get_data']['SAMLRequest']) if not logout_request.is_valid(self.__request_data): self.__errors.append('invalid_logout_request') self.__error_reason = logout_request.get_error() else: if not keep_local_session: OneLogin_Saml2_Utils.delete_local_session( delete_session_cb) in_response_to = logout_request.id response_builder = OneLogin_Saml2_Logout_Response( self.__settings) response_builder.build(in_response_to) logout_response = response_builder.get_response() parameters = {'SAMLResponse': logout_response} if 'RelayState' in self.__request_data['get_data']: parameters['RelayState'] = self.__request_data['get_data'][ 'RelayState'] else: parameters[ 'RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query( self.__request_data) security = self.__settings.get_security_data() if 'logoutResponseSigned' in security and security[ 'logoutResponseSigned']: parameters['SigAlg'] = security['signatureAlgorithm'] parameters['Signature'] = self.build_response_signature( logout_response, parameters.get('RelayState', None), security['signatureAlgorithm']) return self.redirect_to(self.get_slo_url(), parameters) else: self.__errors.append('invalid_binding') raise OneLogin_Saml2_Error( 'SAML LogoutRequest/LogoutResponse not found. Only supported HTTP_REDIRECT Binding', OneLogin_Saml2_Error.SAML_LOGOUTMESSAGE_NOT_FOUND)
def testIsInValidSign(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutResponse """ request_data = { 'http_host': 'example.com', 'script_name': 'index.html', 'get_data': {} } settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) settings.set_strict(False) request_data['get_data'] = { 'SAMLResponse': 'fZJva8IwEMa/Ssl7TZrW/gnqGHMMwSlM8cXeyLU9NaxNQi9lfvxVZczB5ptwSe733MPdjQma2qmFPdjOvyE5awiDU1MbUpevCetaoyyQJmWgQVK+VOvH14WSQ6Fca70tbc1ukPsEEGHrtTUsmM8mbDfKUhnFci8gliGINI/yXIAAiYnsw6JIRgWWAKlkwRZb6skJ64V6nKjDuSEPxvdPIowHIhpIsQkTFaYqSt9ZMEPy2oC/UEfvHSnOnfZFV38MjR1oN7TtgRv8tAZre9CGV9jYkGtT4Wnoju6Bauprme/ebOyErZbPi9XLfLnDoohwhHGc5WVSVhjCKM6rBMpYQpWJrIizfZ4IZNPxuTPqYrmd/m+EdONqPOfy8yG5rhxv0EMFHs52xvxWaHyd3tqD7+j37clWGGyh7vD+POiSrdZdWSIR49NrhR9R/teGTL8A', 'RelayState': 'https://pitbulk.no-ip.org/newonelogin/demo1/index.php', 'SigAlg': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', 'Signature': 'vfWbbc47PkP3ejx4bjKsRX7lo9Ml1WRoE5J5owF/0mnyKHfSY6XbhO1wwjBV5vWdrUVX+xp6slHyAf4YoAsXFS0qhan6txDiZY4Oec6yE+l10iZbzvie06I4GPak4QrQ4gAyXOSzwCrRmJu4gnpeUxZ6IqKtdrKfAYRAcVfNKGA=' } response = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) self.assertTrue(response.is_valid(request_data)) relayState = request_data['get_data']['RelayState'] del request_data['get_data']['RelayState'] inv_response = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) self.assertFalse(inv_response.is_valid(request_data)) request_data['get_data']['RelayState'] = relayState settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) try: valid = response_2.is_valid(request_data) self.assertFalse(valid) except Exception as e: self.assertIn('Invalid issuer in the Logout Request', e.message) settings.set_strict(False) old_signature = request_data['get_data']['Signature'] request_data['get_data']['Signature'] = 'vfWbbc47PkP3ejx4bjKsRX7lo9Ml1WRoE5J5owF/0mnyKHfSY6XbhO1wwjBV5vWdrUVX+xp6slHyAf4YoAsXFS0qhan6txDiZY4Oec6yE+l10iZbzvie06I4GPak4QrQ4gAyXOSzwCrRmJu4gnpeUxZ6IqKtdrKfAYRAcVf3333=' response_3 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) try: valid = response_3.is_valid(request_data) self.assertFalse(valid) except Exception as e: self.assertIn('Signature validation failed. Logout Response rejected', e.message) request_data['get_data']['Signature'] = old_signature old_signature_algorithm = request_data['get_data']['SigAlg'] del request_data['get_data']['SigAlg'] response_4 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) self.assertTrue(response_4.is_valid(request_data)) request_data['get_data']['RelayState'] = 'http://example.com/relaystate' response_5 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) try: valid = response_5.is_valid(request_data) self.assertFalse(valid) except Exception as e: self.assertIn('Signature validation failed. Logout Response rejected', e.message) settings.set_strict(True) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message_6 = OneLogin_Saml2_Utils.decode_base64_and_inflate(request_data['get_data']['SAMLResponse']) plain_message_6 = plain_message_6.replace('https://pitbulk.no-ip.org/newonelogin/demo1/index.php?sls', current_url) plain_message_6 = plain_message_6.replace('https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php', 'http://idp.example.com/') request_data['get_data']['SAMLResponse'] = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message_6) response_6 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) try: valid = response_6.is_valid(request_data) self.assertFalse(valid) except Exception as e: self.assertIn('Signature validation failed. Logout Response rejected', e.message) settings.set_strict(False) response_7 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) try: valid = response_7.is_valid(request_data) self.assertFalse(valid) except Exception as e: self.assertIn('Signature validation failed. Logout Response rejected', e.message) request_data['get_data']['SigAlg'] = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1' response_8 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) try: valid = response_8.is_valid(request_data) self.assertFalse(valid) except Exception as e: self.assertIn('Invalid signAlg in the recieved Logout Response', e.message) settings_info = self.loadSettingsJSON() settings_info['strict'] = True settings_info['security']['wantMessagesSigned'] = True settings = OneLogin_Saml2_Settings(settings_info) request_data['get_data']['SigAlg'] = old_signature_algorithm old_signature = request_data['get_data']['Signature'] del request_data['get_data']['Signature'] request_data['get_data']['SAMLResponse'] = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message_6) response_9 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) try: valid = response_9.is_valid(request_data) self.assertFalse(valid) except Exception as e: self.assertIn('The Message of the Logout Response is not signed and the SP require it', e.message) request_data['get_data']['Signature'] = old_signature settings_info['idp']['certFingerprint'] = 'afe71c28ef740bc87425be13a2263d37971da1f9' del settings_info['idp']['x509cert'] settings_2 = OneLogin_Saml2_Settings(settings_info) response_10 = OneLogin_Saml2_Logout_Response(settings_2, request_data['get_data']['SAMLResponse']) try: valid = response_10.is_valid(request_data) self.assertFalse(valid) except Exception as e: self.assertIn('In order to validate the sign on the Logout Response, the x509cert of the IdP is required', e.message)