def get_issuers(self): """ Gets the issuers (from message and from assertion) :returns: The issuers :rtype: list """ issuers = [] message_issuer_nodes = OneLogin_Saml2_Utils.query(self.document, '/samlp:Response/saml:Issuer') if len(message_issuer_nodes) > 0: if len(message_issuer_nodes) == 1: issuers.append(OneLogin_Saml2_Utils.element_text(message_issuer_nodes[0])) else: raise OneLogin_Saml2_ValidationError( 'Issuer of the Response is multiple.', OneLogin_Saml2_ValidationError.ISSUER_MULTIPLE_IN_RESPONSE ) assertion_issuer_nodes = self.__query_assertion('/saml:Issuer') if len(assertion_issuer_nodes) == 1: issuers.append(OneLogin_Saml2_Utils.element_text(assertion_issuer_nodes[0])) else: raise OneLogin_Saml2_ValidationError( 'Issuer of the Assertion not found or multiple.', OneLogin_Saml2_ValidationError.ISSUER_NOT_FOUND_IN_ASSERTION ) return list(set(issuers))
def validate_timestamps(self): """ Verifies that the document is valid according to Conditions Element :param raise_exceptions: Whether to return false on failure or raise an exception :type raise_exceptions: Boolean :returns: True if the condition is valid, False otherwise :rtype: bool """ conditions_nodes = self.__query_assertion('/saml:Conditions') for conditions_node in conditions_nodes: nb_attr = conditions_node.get('NotBefore') nooa_attr = conditions_node.get('NotOnOrAfter') if nb_attr and OneLogin_Saml2_Utils.parse_SAML_to_time(nb_attr) > OneLogin_Saml2_Utils.now() + OneLogin_Saml2_Constants.ALLOWED_CLOCK_DRIFT: raise OneLogin_Saml2_ValidationError( 'Could not validate timestamp: not yet valid. Check system clock.', OneLogin_Saml2_ValidationError.ASSERTION_TOO_EARLY ) if nooa_attr and OneLogin_Saml2_Utils.parse_SAML_to_time(nooa_attr) + OneLogin_Saml2_Constants.ALLOWED_CLOCK_DRIFT <= OneLogin_Saml2_Utils.now(): raise OneLogin_Saml2_ValidationError( 'Could not validate timestamp: expired. Check system clock.', OneLogin_Saml2_ValidationError.ASSERTION_EXPIRED ) return True
def testLoginForceAuthN(self): """ Tests the login method of the OneLogin_Saml2_Auth class Case Logout with no parameters. A AuthN Request is built with ForceAuthn and redirect executed """ settings_info = self.loadSettingsJSON() return_to = u"http://example.com/returnto" sso_url = settings_info["idp"]["singleSignOnService"]["url"] auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) target_url = auth.login(return_to) parsed_query = parse_qs(urlparse(target_url)[4]) sso_url = settings_info["idp"]["singleSignOnService"]["url"] self.assertIn(sso_url, target_url) self.assertIn("SAMLRequest", parsed_query) request = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query["SAMLRequest"][0]) self.assertNotIn('ForceAuthn="true"', request) auth_2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) target_url_2 = auth_2.login(return_to, False, False) parsed_query_2 = parse_qs(urlparse(target_url_2)[4]) self.assertIn(sso_url, target_url_2) self.assertIn("SAMLRequest", parsed_query_2) request_2 = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_2["SAMLRequest"][0]) self.assertNotIn('ForceAuthn="true"', request_2) auth_3 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) target_url_3 = auth_3.login(return_to, True, False) parsed_query_3 = parse_qs(urlparse(target_url_3)[4]) self.assertIn(sso_url, target_url_3) self.assertIn("SAMLRequest", parsed_query_3) request_3 = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_3["SAMLRequest"][0]) self.assertIn('ForceAuthn="true"', request_3)
def testCreateRequestAuthContextComparision(self): """ Tests the OneLogin_Saml2_Authn_Request Constructor. The creation of a deflated SAML Request with defined AuthnContextComparison """ saml_settings = self.loadSettingsJSON() settings = OneLogin_Saml2_Settings(saml_settings) authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded)) self.assertRegexpMatches(inflated, '^<samlp:AuthnRequest') self.assertIn(OneLogin_Saml2_Constants.AC_PASSWORD, inflated) self.assertNotIn(OneLogin_Saml2_Constants.AC_X509, inflated) saml_settings['security']['requestedAuthnContext'] = True settings = OneLogin_Saml2_Settings(saml_settings) authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded)) self.assertRegexpMatches(inflated, '^<samlp:AuthnRequest') self.assertIn('RequestedAuthnContext Comparison="exact"', inflated) saml_settings['security']['requestedAuthnContextComparison'] = 'minimun' settings = OneLogin_Saml2_Settings(saml_settings) authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded)) self.assertRegexpMatches(inflated, '^<samlp:AuthnRequest') self.assertIn('RequestedAuthnContext Comparison="minimun"', inflated)
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 testLoginIsPassive(self): """ Tests the login method of the OneLogin_Saml2_Auth class Case Logout with no parameters. A AuthN Request is built with IsPassive and redirect executed """ settings_info = self.loadSettingsJSON() return_to = u'http://example.com/returnto' settings_info['idp']['singleSignOnService']['url'] auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) target_url = auth.login(return_to) parsed_query = parse_qs(urlparse(target_url)[4]) sso_url = settings_info['idp']['singleSignOnService']['url'] self.assertIn(sso_url, target_url) self.assertIn('SAMLRequest', parsed_query) request = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0])) self.assertNotIn('IsPassive="true"', request) auth_2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) target_url_2 = auth_2.login(return_to, False, False) parsed_query_2 = parse_qs(urlparse(target_url_2)[4]) self.assertIn(sso_url, target_url_2) self.assertIn('SAMLRequest', parsed_query_2) request_2 = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_2['SAMLRequest'][0])) self.assertNotIn('IsPassive="true"', request_2) auth_3 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) target_url_3 = auth_3.login(return_to, False, True) parsed_query_3 = parse_qs(urlparse(target_url_3)[4]) self.assertIn(sso_url, target_url_3) self.assertIn('SAMLRequest', parsed_query_3) request_3 = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_3['SAMLRequest'][0])) self.assertIn('IsPassive="true"', request_3)
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 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 testCreateEncSAMLRequest(self): """ Tests the OneLogin_Saml2_Authn_Request Constructor. The creation of a deflated SAML Request """ settings = self.loadSettingsJSON() settings['organization'] = { 'es': { 'name': 'sp_prueba', 'displayname': 'SP prueba', 'url': 'http://sp.example.com' } } settings['security']['wantNameIdEncrypted'] = True settings = OneLogin_Saml2_Settings(settings) authn_request = OneLogin_Saml2_Authn_Request(settings) parameters = { 'SAMLRequest': authn_request.get_request() } auth_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SSOService.php', parameters, True) self.assertRegexpMatches(auth_url, '^http://idp\.example\.com\/SSOService\.php\?SAMLRequest=') exploded = urlparse(auth_url) exploded = parse_qs(exploded[4]) payload = exploded['SAMLRequest'][0] inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(payload)) self.assertRegexpMatches(inflated, '^<samlp:AuthnRequest') self.assertRegexpMatches(inflated, 'AssertionConsumerServiceURL="http://stuff.com/endpoints/endpoints/acs.php">') self.assertRegexpMatches(inflated, '<saml:Issuer>http://stuff.com/endpoints/metadata.php</saml:Issuer>') self.assertRegexpMatches(inflated, 'Format="urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted"') self.assertRegexpMatches(inflated, 'ProviderName="SP prueba"')
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 acs(request): req = prepare_django_request(request) auth = init_saml_auth(req) auth.process_response() data = { 'errors': auth.get_errors(), 'not_auth_warn': not auth.is_authenticated() } if not data['errors']: request.session['samlUserdata'] = auth.get_attributes() request.session['samlNameId'] = auth.get_nameid() request.session['samlSessionIndex'] = auth.get_session_index() user = authenticate(saml_authentication=auth) if user is None: data['errors'] = ['Authentication backend failed.'] elif not user.is_active: data['errors'] = ['User is not active. TODO: logout at idp?'] else: login(request, user) if 'RelayState' in req['post_data'] and OneLogin_Saml2_Utils.get_self_url(req) != req['post_data']['RelayState']: return HttpResponseRedirect(auth.redirect_to(req['post_data']['RelayState'])) return HttpResponseRedirect(OneLogin_Saml2_Utils.get_self_url(req) + '/profile') return draw_page(request, data)
def testGetselfhost(self): """ Tests the get_self_host method of the OneLogin_Saml2_Utils """ request_data = {} self.assertRaisesRegexp(Exception, 'No hostname defined', OneLogin_Saml2_Utils.get_self_host, request_data) request_data = { 'server_name': 'example.com' } self.assertEqual('example.com', OneLogin_Saml2_Utils.get_self_host(request_data)) request_data = { 'http_host': 'example.com' } self.assertEqual('example.com', OneLogin_Saml2_Utils.get_self_host(request_data)) request_data = { 'http_host': 'example.com:443' } self.assertEqual('example.com', OneLogin_Saml2_Utils.get_self_host(request_data)) request_data = { 'http_host': 'example.com:ok' } self.assertEqual('example.com:ok', OneLogin_Saml2_Utils.get_self_host(request_data))
def saml_validate_post_response(response): issuer = 'urn:rea:sshephalopod' request_id_expiration_period_ms = 3600000 xml = ET.fromstring(response) path = "/*[local-name()='Response']/@InResponseTo" in_response_to = xml.xpath(path) in_response_to = in_response_to[0] if in_response_to else "" path = ".//*[local-name()='X509Certificate']/text()" cert = xml.xpath(path)[0] cert = "-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----".format(cert) if not OneLogin_Saml2_Utils.validate_sign(xml, cert, validatecert=True): raise Exception("Top level signature is not valid!") assertions = xml.xpath("/*[local-name()='Response']/*[local-name()='Assertion']") encrypted_assertions = xml.xpath("/*[local-name()='Response']/*[local-name()='EncryptedAssertion']") if len(assertions) + len(encrypted_assertions) > 1: # There's apparently no reason we want to handle multiple assertions raise Exception("Too many assertions!") if len(assertions) == 1: if not OneLogin_Saml2_Utils.validate_sign(assertions[0], cert, validatecert=True): raise Exception("Invalid signature in assertion") return process_validly_signed_assertion(ET.tostring(assertions[0])) raise NotImplementedError("Sorry, only know about if there is an assertion in the response")
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 testIsInvalidDestination(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutRequest Case Invalid Destination """ 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, OneLogin_Saml2_Utils.b64encode(request)) self.assertTrue(logout_request.is_valid(request_data)) settings.set_strict(True) try: logout_request2 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) valid = logout_request2.is_valid(request_data) self.assertFalse(valid) except Exception as e: self.assertIn('The LogoutRequest was received at', str(e)) dom = parseString(request) dom.documentElement.setAttribute('Destination', None) logout_request3 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(dom.toxml())) self.assertTrue(logout_request3.is_valid(request_data)) dom.documentElement.removeAttribute('Destination') logout_request4 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(dom.toxml())) self.assertTrue(logout_request4.is_valid(request_data))
def validate_timestamps(self): """ Verifies that the document is valid according to Conditions Element :returns: (True, '') if the condition is valid, (False, error_msg) otherwise :rtype: (bool, str) """ conditions_nodes = self.__query_assertion('/saml:Conditions') for conditions_node in conditions_nodes: nb_attr = conditions_node.get('NotBefore') nooa_attr = conditions_node.get('NotOnOrAfter') nb_attr_time = OneLogin_Saml2_Utils.parse_SAML_to_time(nb_attr) now = OneLogin_Saml2_Utils.now() if nb_attr and nb_attr_time > now + self.__settings.get_allowed_clock_drift(): return False, ('There was a problem in validating the response: Current time (%s) is earlier than ' 'NotBefore condition (%s)' % (datetime.fromtimestamp(now), datetime.fromtimestamp(nb_attr_time))) nooa_attr_time = OneLogin_Saml2_Utils.parse_SAML_to_time(nooa_attr) now = OneLogin_Saml2_Utils.now() if nooa_attr and nooa_attr_time + self.__settings.get_allowed_clock_drift() <= now: return False, ('There was a problem in validating the response: Current time (%s) is later than ' 'NotOnOrAfter condition (%s)' % (datetime.fromtimestamp(now), datetime.fromtimestamp(nooa_attr_time))) return True, ''
def testGetIdPData(self): """ Tests the getIdPData method of the OneLogin_Saml2_Settings """ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) idp_data = settings.get_idp_data() self.assertNotEqual(len(idp_data), 0) self.assertIn('entityId', idp_data) self.assertIn('singleSignOnService', idp_data) self.assertIn('singleLogoutService', idp_data) self.assertIn('x509cert', idp_data) self.assertEqual('http://idp.example.com/', idp_data['entityId']) self.assertEqual('http://idp.example.com/SSOService.php', idp_data['singleSignOnService']['url']) self.assertEqual('http://idp.example.com/SingleLogoutService.php', idp_data['singleLogoutService']['url']) x509cert = 'MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo' formated_x509_cert = OneLogin_Saml2_Utils.format_cert(x509cert) self.assertEqual(formated_x509_cert, idp_data['x509cert']) settings2 = OneLogin_Saml2_Settings(self.loadSettingsJSON('settings8.json')) idp_data2 = settings2.get_idp_data() self.assertNotEqual(len(idp_data2), 0) self.assertIn('x509certMulti', idp_data2) self.assertIn('signing', idp_data2['x509certMulti']) self.assertIn('encryption', idp_data2['x509certMulti']) x509cert2 = 'MIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIyNDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAfBgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZE+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kYvrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8m' formated_x509_cert2 = OneLogin_Saml2_Utils.format_cert(x509cert2) self.assertEqual(formated_x509_cert2, idp_data2['x509certMulti']['signing'][0]) self.assertEqual(formated_x509_cert, idp_data2['x509certMulti']['signing'][1]) self.assertEqual(formated_x509_cert, idp_data2['x509certMulti']['encryption'][0])
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 = [] get_data = 'get_data' in self.__request_data and self.__request_data['get_data'] if get_data and 'SAMLResponse' in get_data: logout_response = OneLogin_Saml2_Logout_Response(self.__settings, get_data['SAMLResponse']) if not self.validate_response_signature(get_data): self.__errors.append('invalid_logout_response_signature') self.__errors.append('Signature validation failed. Logout Response rejected') elif 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 and 'SAMLRequest' in get_data: logout_request = OneLogin_Saml2_Logout_Request(self.__settings, get_data['SAMLRequest']) if not self.validate_request_signature(get_data): self.__errors.append("invalid_logout_request_signature") self.__errors.append('Signature validation failed. Logout Request rejected') elif 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'] security = self.__settings.get_security_data() if security['logoutResponseSigned']: self.add_response_signature(parameters, 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 testCreateRequest(self): """ Tests the OneLogin_Saml2_Authn_Request Constructor. The creation of a deflated SAML Request """ saml_settings = self.loadSettingsJSON() settings = OneLogin_Saml2_Settings(saml_settings) settings._OneLogin_Saml2_Settings__organization = { u'en-US': { u'url': u'http://sp.example.com', u'name': u'sp_test' } } authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded)) self.assertRegexpMatches(inflated, '^<samlp:AuthnRequest') self.assertNotIn('ProviderName="SP test"', inflated) saml_settings['organization'] = {} settings = OneLogin_Saml2_Settings(saml_settings) authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded)) self.assertRegexpMatches(inflated, '^<samlp:AuthnRequest') self.assertNotIn('ProviderName="SP test"', inflated)
def __init__(self, settings): """ Constructs the AuthnRequest object. Arguments are: * (OneLogin_Saml2_Settings) settings. Setting data """ self.__settings = settings sp_data = self.__settings.get_sp_data() idp_data = self.__settings.get_idp_data() security = self.__settings.get_security_data() uid = OneLogin_Saml2_Utils.generate_unique_id() self.__id = uid issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(OneLogin_Saml2_Utils.now()) destination = idp_data['singleSignOnService']['url'] name_id_policy_format = sp_data['NameIDFormat'] if 'wantNameIdEncrypted' in security and security['wantNameIdEncrypted']: name_id_policy_format = OneLogin_Saml2_Constants.NAMEID_ENCRYPTED provider_name_str = '' organization_data = settings.get_organization() if isinstance(organization_data, dict) and organization_data: langs = organization_data.keys() if 'en-US' in langs: lang = 'en-US' else: lang = langs[0] if 'displayname' in organization_data[lang] and organization_data[lang]['displayname'] is not None: provider_name_str = 'ProviderName="%s"' % organization_data[lang]['displayname'] request = """<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="%(id)s" Version="2.0" %(provider_name)s IssueInstant="%(issue_instant)s" Destination="%(destination)s" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="%(assertion_url)s"> <saml:Issuer>%(entity_id)s</saml:Issuer> <samlp:NameIDPolicy Format="%(name_id_policy)s" AllowCreate="true" /> </samlp:AuthnRequest>""" % \ { 'id': uid, 'provider_name': provider_name_str, 'issue_instant': issue_instant, 'destination': destination, 'assertion_url': sp_data['assertionConsumerService']['url'], 'entity_id': sp_data['entityId'], 'name_id_policy': name_id_policy_format, } self.__authn_request = request
def testGetSelfURL(self): """ Tests the get_self_url method of the OneLogin_Saml2_Utils """ request_data = { 'http_host': 'example.com' } url = OneLogin_Saml2_Utils.get_self_url_host(request_data) self.assertEqual(url, OneLogin_Saml2_Utils.get_self_url(request_data)) request_data['request_uri'] = '' self.assertEqual(url, OneLogin_Saml2_Utils.get_self_url(request_data)) request_data['request_uri'] = '/' self.assertEqual(url + '/', OneLogin_Saml2_Utils.get_self_url(request_data)) request_data['request_uri'] = 'index.html' self.assertEqual(url + 'index.html', OneLogin_Saml2_Utils.get_self_url(request_data)) request_data['request_uri'] = '?index.html' self.assertEqual(url + '?index.html', OneLogin_Saml2_Utils.get_self_url(request_data)) request_data['request_uri'] = '/index.html' self.assertEqual(url + '/index.html', OneLogin_Saml2_Utils.get_self_url(request_data)) request_data['request_uri'] = '/index.html?testing' self.assertEqual(url + '/index.html?testing', OneLogin_Saml2_Utils.get_self_url(request_data)) request_data['request_uri'] = '/test/index.html?testing' self.assertEqual(url + '/test/index.html?testing', OneLogin_Saml2_Utils.get_self_url(request_data)) request_data['request_uri'] = 'https://example.com/testing' self.assertEqual(url + '/testing', OneLogin_Saml2_Utils.get_self_url(request_data))
def testParseDuration(self): """ Tests the parse_duration method of the OneLogin_Saml2_Utils """ duration = 'PT1393462294S' timestamp = 1393876825 parsed_duration = OneLogin_Saml2_Utils.parse_duration(duration, timestamp) self.assertEqual(2787339119, parsed_duration) parsed_duration_2 = OneLogin_Saml2_Utils.parse_duration(duration) self.assertTrue(parsed_duration_2 > parsed_duration) invalid_duration = 'PT1Y' try: parsed_duration_3 = OneLogin_Saml2_Utils.parse_duration(invalid_duration) self.assertEqual(parsed_duration_3, 42) except Exception as e: self.assertIn('Unrecognised ISO 8601 date format', e.message) new_duration = 'P1Y1M' parsed_duration_4 = OneLogin_Saml2_Utils.parse_duration(new_duration, timestamp) self.assertEqual(1428091225, parsed_duration_4) neg_duration = '-P14M' parsed_duration_5 = OneLogin_Saml2_Utils.parse_duration(neg_duration, timestamp) self.assertEqual(1357243225, parsed_duration_5)
def build(self, in_response_to): """ Creates a Logout Response object. :param in_response_to: InResponseTo value for the Logout Response. :type in_response_to: string """ sp_data = self.__settings.get_sp_data() idp_data = self.__settings.get_idp_data() uid = OneLogin_Saml2_Utils.generate_unique_id() issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(OneLogin_Saml2_Utils.now()) logout_response = """<samlp:LogoutResponse xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="%(id)s" Version="2.0" IssueInstant="%(issue_instant)s" Destination="%(destination)s" InResponseTo="%(in_response_to)s" > <saml:Issuer>%(entity_id)s</saml:Issuer> <samlp:Status> <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /> </samlp:Status> </samlp:LogoutResponse>""" % { "id": uid, "issue_instant": issue_instant, "destination": idp_data["singleLogoutService"]["url"], "in_response_to": in_response_to, "entity_id": sp_data["entityId"], } self.__logout_response = logout_response
def __decrypt_assertion(self, dom): """ Decrypts the Assertion :raises: Exception if no private key available :param dom: Encrypted Assertion :type dom: Element :returns: Decrypted Assertion :rtype: Element """ key = self.__settings.get_sp_key() debug = self.__settings.is_debug_active() if not key: raise OneLogin_Saml2_Error( 'No private key available to decrypt the assertion, check settings', OneLogin_Saml2_Error.PRIVATE_KEY_NOT_FOUND ) encrypted_assertion_nodes = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/saml:EncryptedAssertion') if encrypted_assertion_nodes: encrypted_data_nodes = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], '//saml:EncryptedAssertion/xenc:EncryptedData') if encrypted_data_nodes: keyinfo = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], '//saml:EncryptedAssertion/xenc:EncryptedData/ds:KeyInfo') if not keyinfo: raise OneLogin_Saml2_ValidationError( 'No KeyInfo present, invalid Assertion', OneLogin_Saml2_ValidationError.KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA ) keyinfo = keyinfo[0] children = keyinfo.getchildren() if not children: raise OneLogin_Saml2_ValidationError( 'KeyInfo has no children nodes, invalid Assertion', OneLogin_Saml2_ValidationError.CHILDREN_NODE_NOT_FOUND_IN_KEYINFO ) for child in children: if 'RetrievalMethod' in child.tag: if child.attrib['Type'] != 'http://www.w3.org/2001/04/xmlenc#EncryptedKey': raise OneLogin_Saml2_ValidationError( 'Unsupported Retrieval Method found', OneLogin_Saml2_ValidationError.UNSUPPORTED_RETRIEVAL_METHOD ) uri = child.attrib['URI'] if not uri.startswith('#'): break uri = uri.split('#')[1] encrypted_key = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], './xenc:EncryptedKey[@Id=$tagid]', None, uri) if encrypted_key: keyinfo.append(encrypted_key[0]) encrypted_data = encrypted_data_nodes[0] decrypted = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key, debug=debug, inplace=True) dom.replace(encrypted_assertion_nodes[0], decrypted) return dom
def __init__(self, settings, request=None): """ Constructs the Logout Request object. Arguments are: * (OneLogin_Saml2_Settings) settings. Setting data """ self.__settings = settings self.__error = None if request is None: sp_data = self.__settings.get_sp_data() idp_data = self.__settings.get_idp_data() security = self.__settings.get_security_data() uid = OneLogin_Saml2_Utils.generate_unique_id() name_id_value = OneLogin_Saml2_Utils.generate_unique_id() issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(OneLogin_Saml2_Utils.now()) cert = None if 'nameIdEncrypted' in security and security['nameIdEncrypted']: cert = idp_data['x509cert'] name_id = OneLogin_Saml2_Utils.generate_name_id( name_id_value, sp_data['entityId'], sp_data['NameIDFormat'], cert ) logout_request = """<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="%(id)s" Version="2.0" IssueInstant="%(issue_instant)s" Destination="%(single_logout_url)s"> <saml:Issuer>%(entity_id)s</saml:Issuer> %(name_id)s </samlp:LogoutRequest>""" % \ { 'id': uid, 'issue_instant': issue_instant, 'single_logout_url': idp_data['singleLogoutService']['url'], 'entity_id': sp_data['entityId'], 'name_id': name_id, } else: decoded = b64decode(request) # We try to inflate try: inflated = decompress(decoded, -15) logout_request = inflated except Exception: logout_request = decoded self.__logout_request = logout_request
def testFormatFingerPrint(self): """ Tests the format_finger_print method of the OneLogin_Saml2_Utils """ finger_print_1 = 'AF:E7:1C:28:EF:74:0B:C8:74:25:BE:13:A2:26:3D:37:97:1D:A1:F9' self.assertEqual('afe71c28ef740bc87425be13a2263d37971da1f9', OneLogin_Saml2_Utils.format_finger_print(finger_print_1)) finger_print_2 = 'afe71c28ef740bc87425be13a2263d37971da1f9' self.assertEqual('afe71c28ef740bc87425be13a2263d37971da1f9', OneLogin_Saml2_Utils.format_finger_print(finger_print_2))
def validate_num_assertions(self): """ Verifies that the document only contains a single Assertion (encrypted or not) :returns: True if only 1 assertion encrypted or not :rtype: bool """ encrypted_assertion_nodes = OneLogin_Saml2_Utils.query(self.document, '//saml:EncryptedAssertion') assertion_nodes = OneLogin_Saml2_Utils.query(self.document, '//saml:Assertion') return (len(encrypted_assertion_nodes) + len(assertion_nodes)) == 1
def index(request): req = prepare_django_request(request) auth = init_saml_auth(req) errors = [] not_auth_warn = False success_slo = False attributes = False paint_logout = False if 'sso' in req['get_data']: return HttpResponseRedirect(auth.login()) elif 'sso2' in req['get_data']: return_to = OneLogin_Saml2_Utils.get_self_url(req) + reverse('attrs') return HttpResponseRedirect(auth.login(return_to)) elif 'slo' in req['get_data']: name_id = None session_index = None if 'samlNameId' in request.session: name_id = request.session['samlNameId'] if 'samlSessionIndex' in request.session: session_index = request.session['samlSessionIndex'] return HttpResponseRedirect(auth.logout(name_id=name_id, session_index=session_index)) elif 'acs' in req['get_data']: auth.process_response() errors = auth.get_errors() not_auth_warn = not auth.is_authenticated() if not errors: request.session['samlUserdata'] = auth.get_attributes() request.session['samlNameId'] = auth.get_nameid() request.session['samlSessionIndex'] = auth.get_session_index() if 'RelayState' in req['post_data'] and OneLogin_Saml2_Utils.get_self_url(req) != req['post_data']['RelayState']: return HttpResponseRedirect(auth.redirect_to(req['post_data']['RelayState'])) elif 'sls' in req['get_data']: dscb = lambda: request.session.flush() url = auth.process_slo(delete_session_cb=dscb) errors = auth.get_errors() if len(errors) == 0: if url is not None: return HttpResponseRedirect(url) else: success_slo = True if 'samlUserdata' in request.session: paint_logout = True if len(request.session['samlUserdata']) > 0: attributes = request.session['samlUserdata'].items() return render_to_response('index.html', {'errors': errors, 'not_auth_warn': not_auth_warn, 'success_slo': success_slo, 'attributes': attributes, 'paint_logout': paint_logout}, context_instance=RequestContext(request))
def testDecryptElement(self): """ Tests the decrypt_element method of the OneLogin_Saml2_Utils """ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) key = settings.get_sp_key() xml_nameid_enc = b64decode(self.file_contents(join(self.data_path, 'responses', 'response_encrypted_nameid.xml.base64'))) dom_nameid_enc = etree.fromstring(xml_nameid_enc) encrypted_nameid_nodes = dom_nameid_enc.find('.//saml:EncryptedID', namespaces=OneLogin_Saml2_Constants.NSMAP) encrypted_data = encrypted_nameid_nodes[0] decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key) self.assertEqual('{%s}NameID' % OneLogin_Saml2_Constants.NS_SAML, decrypted_nameid.tag) self.assertEqual('2de11defd199f8d5bb63f9b7deb265ba5c675c10', decrypted_nameid.text) xml_assertion_enc = b64decode(self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion_encrypted_nameid.xml.base64'))) dom_assertion_enc = etree.fromstring(xml_assertion_enc) encrypted_assertion_enc_nodes = dom_assertion_enc.find('.//saml:EncryptedAssertion', namespaces=OneLogin_Saml2_Constants.NSMAP) encrypted_data_assert = encrypted_assertion_enc_nodes[0] decrypted_assertion = OneLogin_Saml2_Utils.decrypt_element(encrypted_data_assert, key) self.assertEqual('{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML, decrypted_assertion.tag) self.assertEqual('_6fe189b1c241827773902f2b1d3a843418206a5c97', decrypted_assertion.get('ID')) encrypted_nameid_nodes = decrypted_assertion.xpath('./saml:Subject/saml:EncryptedID', namespaces=OneLogin_Saml2_Constants.NSMAP) encrypted_data = encrypted_nameid_nodes[0][0] decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key) self.assertEqual('{%s}NameID' % OneLogin_Saml2_Constants.NS_SAML, decrypted_nameid.tag) self.assertEqual('457bdb600de717891c77647b0806ce59c089d5b8', decrypted_nameid.text) key_2_file_name = join(self.data_path, 'misc', 'sp2.key') f = open(key_2_file_name, 'r') key2 = f.read() f.close() self.assertRaises(Exception, OneLogin_Saml2_Utils.decrypt_element, encrypted_data, key2) key_3_file_name = join(self.data_path, 'misc', 'sp2.key') f = open(key_3_file_name, 'r') key3 = f.read() f.close() self.assertRaises(Exception, OneLogin_Saml2_Utils.decrypt_element, encrypted_data, key3) xml_nameid_enc_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'encrypted_nameID_without_EncMethod.xml.base64'))) dom_nameid_enc_2 = etree.fromstring(xml_nameid_enc_2) encrypted_nameid_nodes_2 = dom_nameid_enc_2.find('.//saml:EncryptedID', namespaces=OneLogin_Saml2_Constants.NSMAP) encrypted_data_2 = encrypted_nameid_nodes_2[0] self.assertRaises(Exception, OneLogin_Saml2_Utils.decrypt_element, encrypted_data_2, key) xml_nameid_enc_3 = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'encrypted_nameID_without_keyinfo.xml.base64'))) dom_nameid_enc_3 = etree.fromstring(xml_nameid_enc_3) encrypted_nameid_nodes_3 = dom_nameid_enc_3.find('.//saml:EncryptedID', namespaces=OneLogin_Saml2_Constants.NSMAP) encrypted_data_3 = encrypted_nameid_nodes_3[0] self.assertRaises(Exception, OneLogin_Saml2_Utils.decrypt_element, encrypted_data_3, key)
def testRedirect(self): """ Tests the redirect method of the OneLogin_Saml2_Utils """ request_data = { 'http_host': 'example.com' } # Check relative and absolute hostname = OneLogin_Saml2_Utils.get_self_host(request_data) url = 'http://%s/example' % hostname url2 = '/example' target_url = OneLogin_Saml2_Utils.redirect(url, {}, request_data) target_url2 = OneLogin_Saml2_Utils.redirect(url2, {}, request_data) self.assertEqual(target_url, target_url2) # Check that accept http/https and reject other protocols url3 = 'https://%s/example?test=true' % hostname url4 = 'ftp://%s/example' % hostname target_url3 = OneLogin_Saml2_Utils.redirect(url3, {}, request_data) self.assertIn('test=true', target_url3) try: target_url4 = OneLogin_Saml2_Utils.redirect(url4, {}, request_data) self.assertTrue(target_url4 == 42) except Exception as e: self.assertIn('Redirect to invalid URL', e.message) # Review parameter prefix parameters1 = { 'value1': 'a' } target_url5 = OneLogin_Saml2_Utils.redirect(url, parameters1, request_data) self.assertEqual('http://%s/example?value1=a' % hostname, target_url5) target_url6 = OneLogin_Saml2_Utils.redirect(url3, parameters1, request_data) self.assertEqual('https://%s/example?test=true&value1=a' % hostname, target_url6) # Review parameters parameters2 = { 'alphavalue': 'a', 'numvaluelist': ['1', '2'], 'testing': None } target_url7 = OneLogin_Saml2_Utils.redirect(url, parameters2, request_data) self.assertEqual('http://%s/example?numvaluelist[]=1&numvaluelist[]=2&testing&alphavalue=a' % hostname, target_url7) parameters3 = { 'alphavalue': 'a', 'emptynumvaluelist': [], 'numvaluelist': [''], } target_url8 = OneLogin_Saml2_Utils.redirect(url, parameters3, request_data) self.assertEqual('http://%s/example?numvaluelist[]=&alphavalue=a' % hostname, target_url8)
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['entityId'] sp_data = self.__settings.get_sp_data() sp_entity_id = sp_data['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_XML.validate_xml( self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if isinstance(res, str): 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['wantAssertionsEncrypted']: raise Exception( 'The assertion of the Response is not encrypted and the SP require it' ) if security['wantNameIdEncrypted']: 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[ 'wantAttributeStatement'] 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['wantAssertionsSigned'] 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['wantMessagesSigned'] 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['x509cert'] fingerprint = idp_data['certFingerprint'] fingerprintalg = idp_data['certFingerprintAlgorithm'] # 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 = str(err) debug = self.__settings.is_debug_active() if debug: print(err) return False
def __validate_signature(self, data, saml_type, raise_exceptions=False): """ Validate Signature :param data: The Request data :type data: dict :param cert: The certificate to check signature :type cert: str :param saml_type: The target URL the user should be redirected to :type saml_type: string SAMLRequest | SAMLResponse :param raise_exceptions: Whether to return false on failure or raise an exception :type raise_exceptions: Boolean """ try: signature = data.get('Signature', None) if signature is None: if self.__settings.is_strict( ) and self.__settings.get_security_data().get( 'wantMessagesSigned', False): raise OneLogin_Saml2_ValidationError( 'The %s is not signed. Rejected.' % saml_type, OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE) return True idp_data = self.get_settings().get_idp_data() exists_x509cert = 'x509cert' in idp_data and idp_data['x509cert'] exists_multix509sign = 'x509certMulti' in idp_data and \ 'signing' in idp_data['x509certMulti'] and \ idp_data['x509certMulti']['signing'] if not (exists_x509cert or exists_multix509sign): error_msg = 'In order to validate the sign on the %s, the x509cert of the IdP is required' % saml_type self.__errors.append(error_msg) raise OneLogin_Saml2_Error(error_msg, OneLogin_Saml2_Error.CERT_NOT_FOUND) sign_alg = data.get('SigAlg', OneLogin_Saml2_Constants.RSA_SHA1) if isinstance(sign_alg, bytes): sign_alg = sign_alg.decode('utf8') lowercase_urlencoding = False if 'lowercase_urlencoding' in self.__request_data.keys(): lowercase_urlencoding = self.__request_data[ 'lowercase_urlencoding'] signed_query = self.__build_sign_query( data[saml_type], data.get('RelayState', None), sign_alg, saml_type, lowercase_urlencoding) if exists_multix509sign: for cert in idp_data['x509certMulti']['signing']: if OneLogin_Saml2_Utils.validate_binary_sign( signed_query, OneLogin_Saml2_Utils.b64decode(signature), cert, sign_alg): return True raise OneLogin_Saml2_ValidationError( 'Signature validation failed. %s rejected' % saml_type, OneLogin_Saml2_ValidationError.INVALID_SIGNATURE) else: cert = idp_data['x509cert'] if not OneLogin_Saml2_Utils.validate_binary_sign( signed_query, OneLogin_Saml2_Utils.b64decode(signature), cert, sign_alg, self.__settings.is_debug_active()): raise OneLogin_Saml2_ValidationError( 'Signature validation failed. %s rejected' % saml_type, OneLogin_Saml2_ValidationError.INVALID_SIGNATURE) return True except Exception as e: self.__error_reason = str(e) if raise_exceptions: raise e return False
def 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 __init__(self, settings): """ Constructs the AuthnRequest object. Arguments are: * (OneLogin_Saml2_Settings) settings. Setting data """ self.__settings = settings sp_data = self.__settings.get_sp_data() idp_data = self.__settings.get_idp_data() security = self.__settings.get_security_data() uid = OneLogin_Saml2_Utils.generate_unique_id() issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML( OneLogin_Saml2_Utils.now()) destination = idp_data['singleSignOnService']['url'] name_id_policy_format = sp_data['NameIDFormat'] if 'wantNameIdEncrypted' in security and security[ 'wantNameIdEncrypted']: name_id_policy_format = OneLogin_Saml2_Constants.NAMEID_ENCRYPTED provider_name_str = '' organization_data = settings.get_organization() if isinstance(organization_data, dict) and organization_data: langs = organization_data.keys() if 'en-US' in langs: lang = 'en-US' else: lang = langs[0] if 'displayname' in organization_data[lang] and organization_data[ lang]['displayname'] is not None: provider_name_str = 'ProviderName="%s"' % organization_data[ lang]['displayname'] request = """<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="%(id)s" Version="2.0" %(provider_name)s IssueInstant="%(issue_instant)s" Destination="%(destination)s" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="%(assertion_url)s"> <saml:Issuer>%(entity_id)s</saml:Issuer> <samlp:NameIDPolicy Format="%(name_id_policy)s" AllowCreate="true" /> <samlp:RequestedAuthnContext Comparison="exact"> <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef> </samlp:RequestedAuthnContext> </samlp:AuthnRequest>""" % \ { 'id': uid, 'provider_name': provider_name_str, 'issue_instant': issue_instant, 'destination': destination, 'assertion_url': sp_data['assertionConsumerService']['url'], 'entity_id': sp_data['entityId'], 'name_id_policy': name_id_policy_format, } self.__authn_request = request
def index(request): req = prepare_django_request(request) auth = init_saml_auth(req) errors = [] error_reason = None not_auth_warn = False success_slo = False attributes = False paint_logout = False if 'sso' in req['get_data']: # return HttpResponseRedirect(auth.login()) # If AuthNRequest ID need to be stored in order to later validate it, do instead sso_built_url = auth.login() request.session['AuthNRequestID'] = auth.get_last_request_id() return HttpResponseRedirect(sso_built_url) elif 'sso2' in req['get_data']: return_to = OneLogin_Saml2_Utils.get_self_url(req) + reverse('attrs') return HttpResponseRedirect(auth.login(return_to)) elif 'slo' in req['get_data']: name_id = session_index = name_id_format = name_id_nq = name_id_spnq = None if 'samlNameId' in request.session: name_id = request.session['samlNameId'] if 'samlSessionIndex' in request.session: session_index = request.session['samlSessionIndex'] if 'samlNameIdFormat' in request.session: name_id_format = request.session['samlNameIdFormat'] if 'samlNameIdNameQualifier' in request.session: name_id_nq = request.session['samlNameIdNameQualifier'] if 'samlNameIdSPNameQualifier' in request.session: name_id_spnq = request.session['samlNameIdSPNameQualifier'] return HttpResponseRedirect( auth.logout(name_id=name_id, session_index=session_index, nq=name_id_nq, name_id_format=name_id_format, spnq=name_id_spnq)) # If LogoutRequest ID need to be stored in order to later validate it, do instead # slo_built_url = auth.logout(name_id=name_id, session_index=session_index) # request.session['LogoutRequestID'] = auth.get_last_request_id() # return HttpResponseRedirect(slo_built_url) elif 'acs' in req['get_data']: request_id = None if 'AuthNRequestID' in request.session: request_id = request.session['AuthNRequestID'] print('Request ID: ', request_id) auth.process_response(request_id=request_id) errors = auth.get_errors() not_auth_warn = not auth.is_authenticated() print('Error: ', errors) print('Not auth warn: ', not_auth_warn) # print('Auth', auth) # print(type(auth)) # print('Attr', auth.get_attributes()) if not errors: if 'AuthNRequestID' in request.session: del request.session['AuthNRequestID'] request.session['samlUserdata'] = auth.get_attributes() request.session['samlNameId'] = auth.get_nameid() request.session['samlNameIdFormat'] = auth.get_nameid_format() request.session['samlNameIdNameQualifier'] = auth.get_nameid_nq() request.session[ 'samlNameIdSPNameQualifier'] = auth.get_nameid_spnq() request.session['samlSessionIndex'] = auth.get_session_index() if 'RelayState' in req[ 'post_data'] and OneLogin_Saml2_Utils.get_self_url( req) != req['post_data']['RelayState']: return HttpResponseRedirect( auth.redirect_to(req['post_data']['RelayState'])) elif auth.get_settings().is_debug_active(): error_reason = auth.get_last_error_reason() print('Error reason (ACS): ', error_reason) elif 'sls' in req['get_data']: request_id = None if 'LogoutRequestID' in request.session: request_id = request.session['LogoutRequestID'] dscb = lambda: request.session.flush() url = auth.process_slo(request_id=request_id, delete_session_cb=dscb) errors = auth.get_errors() if len(errors) == 0: if url is not None: return HttpResponseRedirect(url) else: success_slo = True elif auth.get_settings().is_debug_active(): error_reason = auth.get_last_error_reason() pritn('Error reason (SLS): ', error_reason) if 'samlUserdata' in request.session: paint_logout = True if len(request.session['samlUserdata']) > 0: attributes = request.session['samlUserdata'].items() return render( request, 'index.html', { 'errors': errors, 'error_reason': error_reason, 'not_auth_warn': not_auth_warn, 'success_slo': success_slo, 'attributes': attributes, 'paint_logout': paint_logout })
def _parse_metadata_xml(xml, entity_id): """ Given an XML document containing SAML 2.0 metadata, parse it and return a tuple of (public_key, sso_url, expires_at) for the specified entityID. Raises MetadataParseError if anything is wrong. """ if xml.tag == etree.QName(SAML_XML_NS, 'EntityDescriptor'): entity_desc = xml else: if xml.tag != etree.QName(SAML_XML_NS, 'EntitiesDescriptor'): raise MetadataParseError( Text( u"Expected root element to be <EntitiesDescriptor>, not {}" ).format(xml.tag)) entity_desc = xml.find(".//{}[@entityID='{}']".format( etree.QName(SAML_XML_NS, 'EntityDescriptor'), entity_id)) if entity_desc is None: raise MetadataParseError( u"Can't find EntityDescriptor for entityID {}".format( entity_id)) expires_at = None if "validUntil" in xml.attrib: expires_at = dateutil.parser.parse(xml.attrib["validUntil"]) if "cacheDuration" in xml.attrib: cache_expires = OneLogin_Saml2_Utils.parse_duration( xml.attrib["cacheDuration"]) cache_expires = datetime.datetime.fromtimestamp(cache_expires, tz=pytz.utc) if expires_at is None or cache_expires < expires_at: expires_at = cache_expires sso_desc = entity_desc.find(etree.QName(SAML_XML_NS, "IDPSSODescriptor")) if sso_desc is None: raise MetadataParseError("IDPSSODescriptor missing") if 'urn:oasis:names:tc:SAML:2.0:protocol' not in sso_desc.get( "protocolSupportEnumeration"): raise MetadataParseError("This IdP does not support SAML 2.0") # Now we just need to get the public_key and sso_url public_key = sso_desc.findtext("./{}//{}".format( etree.QName(SAML_XML_NS, "KeyDescriptor"), "{http://www.w3.org/2000/09/xmldsig#}X509Certificate")) if not public_key: raise MetadataParseError( "Public Key missing. Expected an <X509Certificate>") public_key = public_key.replace(" ", "") binding_elements = sso_desc.iterfind("./{}".format( etree.QName(SAML_XML_NS, "SingleSignOnService"))) sso_bindings = { element.get('Binding'): element.get('Location') for element in binding_elements } try: # The only binding supported by python-saml and python-social-auth is HTTP-Redirect: sso_url = sso_bindings[ 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'] except KeyError: raise MetadataParseError( "Unable to find SSO URL with HTTP-Redirect binding.") return public_key, sso_url, expires_at
def parse(idp_metadata, required_sso_binding=OneLogin_Saml2_Constants. BINDING_HTTP_REDIRECT, required_slo_binding=OneLogin_Saml2_Constants. BINDING_HTTP_REDIRECT, entity_id=None): """ Parse the Identity Provider metadata and return a dict with extracted data. If there are multiple <IDPSSODescriptor> tags, parse only the first. Parse only those SSO endpoints with the same binding as given by the `required_sso_binding` parameter. Parse only those SLO endpoints with the same binding as given by the `required_slo_binding` parameter. If the metadata specifies multiple SSO endpoints with the required binding, extract only the first (the same holds true for SLO endpoints). :param idp_metadata: XML of the Identity Provider Metadata. :type idp_metadata: string :param required_sso_binding: Parse only POST or REDIRECT SSO endpoints. :type required_sso_binding: one of OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT or OneLogin_Saml2_Constants.BINDING_HTTP_POST :param required_slo_binding: Parse only POST or REDIRECT SLO endpoints. :type required_slo_binding: one of OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT or OneLogin_Saml2_Constants.BINDING_HTTP_POST :param entity_id: Specify the entity_id of the EntityDescriptor that you want to parse a XML that contains multiple EntityDescriptor. :type entity_id: string :returns: settings dict with extracted data :rtype: dict """ data = {} dom = fromstring(idp_metadata) entity_desc_path = '//md:EntityDescriptor' if entity_id: entity_desc_path += "[@entityID='%s']" % entity_id entity_descriptor_nodes = OneLogin_Saml2_Utils.query( dom, entity_desc_path) idp_entity_id = want_authn_requests_signed = idp_name_id_format = idp_sso_url = idp_slo_url = certs = None if len(entity_descriptor_nodes) > 0: entity_descriptor_node = entity_descriptor_nodes[0] idp_descriptor_nodes = OneLogin_Saml2_Utils.query( entity_descriptor_node, './md:IDPSSODescriptor') if len(idp_descriptor_nodes) > 0: idp_descriptor_node = idp_descriptor_nodes[0] idp_entity_id = entity_descriptor_node.get('entityID', None) want_authn_requests_signed = entity_descriptor_node.get( 'WantAuthnRequestsSigned', None) name_id_format_nodes = OneLogin_Saml2_Utils.query( idp_descriptor_node, './md:NameIDFormat') if len(name_id_format_nodes) > 0: idp_name_id_format = OneLogin_Saml2_Utils.element_text( name_id_format_nodes[0]) sso_nodes = OneLogin_Saml2_Utils.query( idp_descriptor_node, "./md:SingleSignOnService[@Binding='%s']" % required_sso_binding) if len(sso_nodes) > 0: idp_sso_url = sso_nodes[0].get('Location', None) slo_nodes = OneLogin_Saml2_Utils.query( idp_descriptor_node, "./md:SingleLogoutService[@Binding='%s']" % required_slo_binding) if len(slo_nodes) > 0: idp_slo_url = slo_nodes[0].get('Location', None) signing_nodes = OneLogin_Saml2_Utils.query( idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'encryption'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate" ) encryption_nodes = OneLogin_Saml2_Utils.query( idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'signing'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate" ) if len(signing_nodes) > 0 or len(encryption_nodes) > 0: certs = {} if len(signing_nodes) > 0: certs['signing'] = [] for cert_node in signing_nodes: certs['signing'].append(''.join( OneLogin_Saml2_Utils.element_text( cert_node).split())) if len(encryption_nodes) > 0: certs['encryption'] = [] for cert_node in encryption_nodes: certs['encryption'].append(''.join( OneLogin_Saml2_Utils.element_text( cert_node).split())) data['idp'] = {} if idp_entity_id is not None: data['idp']['entityId'] = idp_entity_id if idp_sso_url is not None: data['idp']['singleSignOnService'] = {} data['idp']['singleSignOnService']['url'] = idp_sso_url data['idp']['singleSignOnService'][ 'binding'] = required_sso_binding if idp_slo_url is not None: data['idp']['singleLogoutService'] = {} data['idp']['singleLogoutService']['url'] = idp_slo_url data['idp']['singleLogoutService'][ 'binding'] = required_slo_binding if certs is not None: if (len(certs) == 1 and (('signing' in certs and len(certs['signing']) == 1) or ('encryption' in certs and len(certs['encryption']) == 1))) or \ (('signing' in certs and len(certs['signing']) == 1) and ('encryption' in certs and len(certs['encryption']) == 1 and certs['signing'][0] == certs['encryption'][0])): if 'signing' in certs: data['idp']['x509cert'] = certs['signing'][0] else: data['idp']['x509cert'] = certs['encryption'][0] else: data['idp']['x509certMulti'] = certs if want_authn_requests_signed is not None: data['security'] = {} data['security'][ 'authnRequestsSigned'] = want_authn_requests_signed if idp_name_id_format: data['sp'] = {} data['sp']['NameIDFormat'] = idp_name_id_format return data
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): """ 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 instead of $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'] if sign_alg != OneLogin_Saml2_Constants.RSA_SHA1: raise Exception( 'Invalid signAlg in the recieved Logout Request') 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): raise Exception( 'Signature validation failed. Logout Request rejected') return True except Exception as err: # pylint: disable=R0801 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 lowercase_urlencoding = False try: idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data['entityId'] get_data = request_data['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(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if not isinstance(res, Document): raise OneLogin_Saml2_ValidationError( 'Invalid SAML Logout Response. 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 if request_id is not None and self.document.documentElement.hasAttribute('InResponseTo'): in_response_to = self.document.documentElement.getAttribute('InResponseTo') if request_id != in_response_to: 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 if self.document.documentElement.hasAttribute('Destination'): destination = self.document.documentElement.getAttribute('Destination') if destination != '': if 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 security['wantValidMessageSignature']: 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 ) 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 = 'SAMLResponse=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLResponse', 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)) exists_x509cert = 'x509cert' in idp_data and idp_data['x509cert'] exists_multix509sign = 'x509certMulti' in idp_data and \ 'signing' in idp_data['x509certMulti'] and \ idp_data['x509certMulti']['signing'] if not (exists_x509cert or exists_multix509sign): raise OneLogin_Saml2_Error( 'In order to validate the sign on the Logout Response, the x509cert of the IdP is required', OneLogin_Saml2_Error.CERT_NOT_FOUND ) if exists_multix509sign: for cert in idp_data['x509certMulti']['signing']: if OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg): return True raise OneLogin_Saml2_ValidationError( 'Signature validation failed. Logout Response rejected', OneLogin_Saml2_ValidationError.INVALID_SIGNATURE ) else: cert = idp_data['x509cert'] if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg): raise OneLogin_Saml2_ValidationError( 'Signature validation failed. Logout Response rejected', OneLogin_Saml2_ValidationError.INVALID_SIGNATURE ) return True # pylint: disable=R0801 except Exception as err: self.__error = err.__str__() debug = self.__settings.is_debug_active() if debug: print err.__str__() if raise_exceptions: raise err return False
def index(): req = prepare_flask_request(request) auth = init_saml_auth(req) errors = [] not_auth_warn = False success_slo = False attributes = False paint_logout = False if 'sso' in request.args: return redirect(auth.login()) # If AuthNRequest ID need to be stored in order to later validate it, do instead # sso_built_url = auth.login() # request.session['AuthNRequestID'] = auth.get_last_request_id() # return redirect(sso_built_url) elif 'sso2' in request.args: return_to = '%sattrs/' % request.host_url return redirect(auth.login(return_to)) elif 'slo' in request.args: name_id = session_index = name_id_format = name_id_nq = name_id_spnq = None if 'samlNameId' in session: name_id = session['samlNameId'] if 'samlSessionIndex' in session: session_index = session['samlSessionIndex'] if 'samlNameIdFormat' in session: name_id_format = session['samlNameIdFormat'] if 'samlNameIdNameQualifier' in session: name_id_nq = session['samlNameIdNameQualifier'] if 'samlNameIdSPNameQualifier' in session: name_id_spnq = session['samlNameIdSPNameQualifier'] return redirect( auth.logout(name_id=name_id, session_index=session_index, nq=name_id_nq, name_id_format=name_id_format, spnq=name_id_spnq)) # If LogoutRequest ID need to be stored in order to later validate it, do instead # slo_built_url = auth.logout(name_id=name_id, session_index=session_index) # session['LogoutRequestID'] = auth.get_last_request_id() # return redirect(slo_built_url) elif 'acs' in request.args: request_id = None if 'AuthNRequestID' in session: request_id = session['AuthNRequestID'] auth.process_response(request_id=request_id) errors = auth.get_errors() not_auth_warn = not auth.is_authenticated() if len(errors) == 0: if 'AuthNRequestID' in session: del session['AuthNRequestID'] session['samlUserdata'] = auth.get_attributes() session['samlNameIdFormat'] = auth.get_nameid_format() session['samlNameIdNameQualifier'] = auth.get_nameid_nq() session['samlNameIdSPNameQualifier'] = auth.get_nameid_spnq() session['samlSessionIndex'] = auth.get_session_index() self_url = OneLogin_Saml2_Utils.get_self_url(req) if 'RelayState' in request.form and self_url != request.form[ 'RelayState']: return redirect(auth.redirect_to(request.form['RelayState'])) elif 'sls' in request.args: request_id = None if 'LogoutRequestID' in session: request_id = session['LogoutRequestID'] dscb = lambda: session.clear() url = auth.process_slo(request_id=request_id, delete_session_cb=dscb) errors = auth.get_errors() if len(errors) == 0: if url is not None: return redirect(url) else: success_slo = True if 'samlUserdata' in session: paint_logout = True if len(session['samlUserdata']) > 0: attributes = session['samlUserdata'].items() return render_template('index.html', errors=errors, not_auth_warn=not_auth_warn, success_slo=success_slo, attributes=attributes, paint_logout=paint_logout)
def __init__(self, settings, request=None, name_id=None, session_index=None): """ Constructs the Logout Request object. :param settings: Setting data :type request_data: OneLogin_Saml2_Settings :param request: Optional. A LogoutRequest to be loaded instead build one. :type request: 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 """ self.__settings = settings self.__error = None if request is None: sp_data = self.__settings.get_sp_data() idp_data = self.__settings.get_idp_data() security = self.__settings.get_security_data() uid = OneLogin_Saml2_Utils.generate_unique_id() issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML( OneLogin_Saml2_Utils.now()) cert = None if 'nameIdEncrypted' in security and security['nameIdEncrypted']: cert = idp_data['x509cert'] if name_id is not None: nameIdFormat = sp_data['NameIDFormat'] else: name_id = idp_data['entityId'] nameIdFormat = OneLogin_Saml2_Constants.NAMEID_ENTITY name_id_obj = OneLogin_Saml2_Utils.generate_name_id( name_id, sp_data['entityId'], nameIdFormat, cert) if session_index: session_index_str = '<samlp:SessionIndex>%s</samlp:SessionIndex>' % session_index else: session_index_str = '' logout_request = """<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="%(id)s" Version="2.0" IssueInstant="%(issue_instant)s" Destination="%(single_logout_url)s"> <saml:Issuer>%(entity_id)s</saml:Issuer> %(name_id)s %(session_index)s </samlp:LogoutRequest>""" % \ { 'id': uid, 'issue_instant': issue_instant, 'single_logout_url': idp_data['singleLogoutService']['url'], 'entity_id': sp_data['entityId'], 'name_id': name_id_obj, 'session_index': session_index_str, } else: decoded = b64decode(request) # We try to inflate try: inflated = decompress(decoded, -15) logout_request = inflated except Exception: logout_request = decoded self.__logout_request = logout_request
def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_policy=True): """ Constructs the AuthnRequest object. :param settings: OSetting data :type return_to: OneLogin_Saml2_Settings :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 """ self.__settings = settings sp_data = self.__settings.get_sp_data() idp_data = self.__settings.get_idp_data() security = self.__settings.get_security_data() uid = OneLogin_Saml2_Utils.generate_unique_id() self.__id = uid issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML( OneLogin_Saml2_Utils.now()) destination = idp_data['singleSignOnService']['url'] provider_name_str = '' organization_data = settings.get_organization() if isinstance(organization_data, dict) and organization_data: langs = organization_data.keys() if 'en-US' in langs: lang = 'en-US' else: lang = langs[0] if 'displayname' in organization_data[lang] and organization_data[ lang]['displayname'] is not None: provider_name_str = "\n" + ' ProviderName="%s"' % organization_data[ lang]['displayname'] force_authn_str = '' if force_authn is True: force_authn_str = "\n" + ' ForceAuthn="true"' is_passive_str = '' if is_passive is True: is_passive_str = "\n" + ' IsPassive="true"' nameid_policy_str = '' if set_nameid_policy: name_id_policy_format = sp_data['NameIDFormat'] if 'wantNameIdEncrypted' in security and security[ 'wantNameIdEncrypted']: name_id_policy_format = OneLogin_Saml2_Constants.NAMEID_ENCRYPTED nameid_policy_str = """ <samlp:NameIDPolicy Format="%s" AllowCreate="true" />""" % name_id_policy_format requested_authn_context_str = '' if 'requestedAuthnContext' in security.keys( ) and security['requestedAuthnContext'] is not False: authn_comparison = 'exact' if 'requestedAuthnContextComparison' in security.keys(): authn_comparison = security['requestedAuthnContextComparison'] if security['requestedAuthnContext'] is True: requested_authn_context_str = "\n" + """ <samlp:RequestedAuthnContext Comparison="%s"> <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef> </samlp:RequestedAuthnContext>""" % authn_comparison else: requested_authn_context_str = "\n" + ' <samlp:RequestedAuthnContext Comparison="%s">' % authn_comparison for authn_context in security['requestedAuthnContext']: requested_authn_context_str += '<saml:AuthnContextClassRef>%s</saml:AuthnContextClassRef>' % authn_context requested_authn_context_str += ' </samlp:RequestedAuthnContext>' attr_consuming_service_str = '' if 'attributeConsumingService' in sp_data and sp_data[ 'attributeConsumingService']: attr_consuming_service_str = 'AttributeConsumingServiceIndex="1"' request = """<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="%(id)s" Version="2.0"%(provider_name)s%(force_authn_str)s%(is_passive_str)s IssueInstant="%(issue_instant)s" Destination="%(destination)s" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="%(assertion_url)s" %(attr_consuming_service_str)s> <saml:Issuer>%(entity_id)s</saml:Issuer>%(nameid_policy_str)s%(requested_authn_context_str)s </samlp:AuthnRequest>""" % \ { 'id': uid, 'provider_name': provider_name_str, 'force_authn_str': force_authn_str, 'is_passive_str': is_passive_str, 'issue_instant': issue_instant, 'destination': destination, 'assertion_url': sp_data['assertionConsumerService']['url'], 'entity_id': sp_data['entityId'], 'nameid_policy_str': nameid_policy_str, 'requested_authn_context_str': requested_authn_context_str, 'attr_consuming_service_str': attr_consuming_service_str } self.__authn_request = request
def fixed_time(): return OneLogin_Saml2_Utils.parse_SAML_to_time( '2015-05-09T03:57:22Z')
def format_sp_cert_new(self): """ Formats the SP cert. """ self.__sp['x509certNew'] = OneLogin_Saml2_Utils.format_cert( self.__sp['x509certNew'])
def saml_authorized(): errors = [] if not current_app.config.get('SAML_ENABLED'): current_app.logger.error("SAML authentication is disabled.") abort(400) req = saml.prepare_flask_request(request) auth = saml.init_saml_auth(req) auth.process_response() current_app.logger.debug(auth.get_attributes()) errors = auth.get_errors() if len(errors) == 0: session['samlUserdata'] = auth.get_attributes() session['samlNameId'] = auth.get_nameid() session['samlSessionIndex'] = auth.get_session_index() self_url = OneLogin_Saml2_Utils.get_self_url(req) self_url = self_url + req['script_name'] if 'RelayState' in request.form and self_url != request.form[ 'RelayState']: return redirect(auth.redirect_to(request.form['RelayState'])) if current_app.config.get('SAML_ATTRIBUTE_USERNAME', False): username = session['samlUserdata'][ current_app.config['SAML_ATTRIBUTE_USERNAME']][0].lower() else: username = session['samlNameId'].lower() user = User.query.filter_by(username=username).first() if not user: # create user user = User(username=username, plain_text_password=None, email=session['samlNameId']) user.create_local_user() session['user_id'] = user.id email_attribute_name = current_app.config.get('SAML_ATTRIBUTE_EMAIL', 'email') givenname_attribute_name = current_app.config.get( 'SAML_ATTRIBUTE_GIVENNAME', 'givenname') surname_attribute_name = current_app.config.get( 'SAML_ATTRIBUTE_SURNAME', 'surname') name_attribute_name = current_app.config.get('SAML_ATTRIBUTE_NAME', None) account_attribute_name = current_app.config.get( 'SAML_ATTRIBUTE_ACCOUNT', None) admin_attribute_name = current_app.config.get('SAML_ATTRIBUTE_ADMIN', None) group_attribute_name = current_app.config.get('SAML_ATTRIBUTE_GROUP', None) admin_group_name = current_app.config.get('SAML_GROUP_ADMIN_NAME', None) group_to_account_mapping = create_group_to_account_mapping() if email_attribute_name in session['samlUserdata']: user.email = session['samlUserdata'][email_attribute_name][ 0].lower() if givenname_attribute_name in session['samlUserdata']: user.firstname = session['samlUserdata'][givenname_attribute_name][ 0] if surname_attribute_name in session['samlUserdata']: user.lastname = session['samlUserdata'][surname_attribute_name][0] if name_attribute_name in session['samlUserdata']: name = session['samlUserdata'][name_attribute_name][0].split(' ') user.firstname = name[0] user.lastname = ' '.join(name[1:]) if group_attribute_name: user_groups = session['samlUserdata'].get(group_attribute_name, []) else: user_groups = [] if admin_attribute_name or group_attribute_name: user_accounts = set(user.get_accounts()) saml_accounts = [] for group_mapping in group_to_account_mapping: mapping = group_mapping.split('=') group = mapping[0] account_name = mapping[1] if group in user_groups: account = handle_account(account_name) saml_accounts.append(account) for account_name in session['samlUserdata'].get( account_attribute_name, []): account = handle_account(account_name) saml_accounts.append(account) saml_accounts = set(saml_accounts) for account in saml_accounts - user_accounts: account.add_user(user) history = History(msg='Adding {0} to account {1}'.format( user.username, account.name), created_by='SAML Assertion') history.add() for account in user_accounts - saml_accounts: account.remove_user(user) history = History(msg='Removing {0} from account {1}'.format( user.username, account.name), created_by='SAML Assertion') history.add() if admin_attribute_name and 'true' in session['samlUserdata'].get( admin_attribute_name, []): uplift_to_admin(user) elif admin_group_name in user_groups: uplift_to_admin(user) elif admin_attribute_name or group_attribute_name: if user.role.name != 'User': user.role_id = Role.query.filter_by(name='User').first().id history = History(msg='Demoting {0} to user'.format( user.username), created_by='SAML Assertion') history.add() user.plain_text_password = None user.update_profile() session['authentication_type'] = 'SAML' return authenticate_user(user, 'SAML') else: return render_template('errors/SAML.html', errors=errors)
def process_signed_elements(self): """ Verifies the signature nodes: - Checks that are Response or Assertion - Check that IDs and reference URI are unique and consistent. :returns: The signed elements tag names :rtype: list """ sign_nodes = self.__query('//ds:Signature') signed_elements = [] verified_seis = [] verified_ids = [] response_tag = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP assertion_tag = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML for sign_node in sign_nodes: signed_element = sign_node.getparent().tag if signed_element != response_tag and signed_element != assertion_tag: raise OneLogin_Saml2_ValidationError( 'Invalid Signature Element %s SAML Response rejected' % signed_element, OneLogin_Saml2_ValidationError.WRONG_SIGNED_ELEMENT) if not sign_node.getparent().get('ID'): raise OneLogin_Saml2_ValidationError( 'Signed Element must contain an ID. SAML Response rejected', OneLogin_Saml2_ValidationError. ID_NOT_FOUND_IN_SIGNED_ELEMENT) id_value = sign_node.getparent().get('ID') if id_value in verified_ids: raise OneLogin_Saml2_ValidationError( 'Duplicated ID. SAML Response rejected', OneLogin_Saml2_ValidationError. DUPLICATED_ID_IN_SIGNED_ELEMENTS) verified_ids.append(id_value) # Check that reference URI matches the parent ID and no duplicate References or IDs ref = OneLogin_Saml2_Utils.query(sign_node, './/ds:Reference') if ref: ref = ref[0] if ref.get('URI'): sei = ref.get('URI')[1:] if sei != id_value: raise OneLogin_Saml2_ValidationError( 'Found an invalid Signed Element. SAML Response rejected', OneLogin_Saml2_ValidationError. INVALID_SIGNED_ELEMENT) if sei in verified_seis: raise OneLogin_Saml2_ValidationError( 'Duplicated Reference URI. SAML Response rejected', OneLogin_Saml2_ValidationError. DUPLICATED_REFERENCE_IN_SIGNED_ELEMENTS) verified_seis.append(sei) signed_elements.append(signed_element) if signed_elements: if not self.validate_signed_elements(signed_elements, raise_exceptions=True): raise OneLogin_Saml2_ValidationError( 'Found an unexpected Signature Element. SAML Response rejected', OneLogin_Saml2_ValidationError.UNEXPECTED_SIGNED_ELEMENTS) return signed_elements
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 #return True 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 has the SUCCESS status self.check_status() # 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) 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( '/saml2:Subject/saml2: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 the response has all of the AuthnContexts that we provided in the request. # Only check if failOnAuthnContextMismatch is true and requestedAuthnContext is set to a list. requested_authn_contexts = security['requestedAuthnContext'] if security[ 'failOnAuthnContextMismatch'] and requested_authn_contexts and requested_authn_contexts is not True: authn_contexts = self.get_authn_contexts() unmatched_contexts = set( requested_authn_contexts).difference(authn_contexts) if unmatched_contexts: raise OneLogin_Saml2_ValidationError( 'The AuthnContext "%s" didn\'t include requested context "%s"' % (', '.join(authn_contexts), ', '.join(unmatched_contexts)), OneLogin_Saml2_ValidationError. AUTHN_CONTEXT_MISMATCH) # Checks that there is at least one AttributeStatement if required attribute_statement_nodes = self.__query_assertion( '/saml2: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( '/saml2:AttributeStatement/saml2: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 (expected %(idpEntityId)s, got %(issuer)s)' % { 'idpEntityId': idp_entity_id, 'issuer': issuer }, 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( '/saml2:Subject/saml2: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( 'saml2: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 if nooa: self.valid_scd_not_on_or_after = OneLogin_Saml2_Utils.parse_SAML_to_time( nooa) 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) multicerts = None if 'x509certMulti' in idp_data and 'signing' in idp_data[ 'x509certMulti'] and idp_data['x509certMulti'][ 'signing']: multicerts = idp_data['x509certMulti']['signing'] # 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, multicerts=multicerts, raise_exceptions=False): raise OneLogin_Saml2_ValidationError( 'Signature validation failed. SAML Response rejected (signed response)', 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, multicerts=multicerts, raise_exceptions=False): raise OneLogin_Saml2_ValidationError( 'Signature validation failed. SAML Response rejected(signed assertion )', 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): """ 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 lowercase_urlencoding = False try: idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data['entityId'] get_data = request_data['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( self.document, '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() # Check if the InResponseTo of the Logout Response matches the ID of the Logout Request (requestId) if provided if request_id is not None and self.document.documentElement.hasAttribute( 'InResponseTo'): in_response_to = self.document.documentElement.getAttribute( 'InResponseTo') if request_id != in_response_to: 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 if self.document.documentElement.hasAttribute('Destination'): destination = self.document.documentElement.getAttribute( 'Destination') if destination != '': if 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' ) 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 = 'SAMLResponse=%s' % OneLogin_Saml2_Utils.get_encoded_parameter( get_data, 'SAMLResponse', 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 Response, 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 Response rejected' ) return True # pylint: disable=R0801 except Exception as err: self.__error = err.__str__() debug = self.__settings.is_debug_active() if debug: print err.__str__() return False
def __decrypt_assertion(self, xml): """ Decrypts the Assertion :raises: Exception if no private key available :param xml: Encrypted Assertion :type xml: Element :returns: Decrypted Assertion :rtype: Element """ key = self.__settings.get_sp_key() debug = self.__settings.is_debug_active() if not key: raise OneLogin_Saml2_Error( 'No private key available to decrypt the assertion, check settings', OneLogin_Saml2_Error.PRIVATE_KEY_NOT_FOUND) encrypted_assertion_nodes = OneLogin_Saml2_XML.query( xml, '/saml2p:Response/saml2:EncryptedAssertion') if encrypted_assertion_nodes: encrypted_data_nodes = OneLogin_Saml2_XML.query( encrypted_assertion_nodes[0], '//saml2:EncryptedAssertion/xenc:EncryptedData') if encrypted_data_nodes: keyinfo = OneLogin_Saml2_XML.query( encrypted_assertion_nodes[0], '//saml2:EncryptedAssertion/xenc:EncryptedData/ds:KeyInfo') if not keyinfo: raise OneLogin_Saml2_ValidationError( 'No KeyInfo present, invalid Assertion', OneLogin_Saml2_ValidationError. KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA) keyinfo = keyinfo[0] children = keyinfo.getchildren() if not children: raise OneLogin_Saml2_ValidationError( 'KeyInfo has no children nodes, invalid Assertion', OneLogin_Saml2_ValidationError. CHILDREN_NODE_NOT_FOUND_IN_KEYINFO) for child in children: if 'RetrievalMethod' in child.tag: if child.attrib[ 'Type'] != 'http://www.w3.org/2001/04/xmlenc#EncryptedKey': raise OneLogin_Saml2_ValidationError( 'Unsupported Retrieval Method found', OneLogin_Saml2_ValidationError. UNSUPPORTED_RETRIEVAL_METHOD) uri = child.attrib['URI'] if not uri.startswith('#'): break uri = uri.split('#')[1] encrypted_key = OneLogin_Saml2_XML.query( encrypted_assertion_nodes[0], './xenc:EncryptedKey[@Id=$tagid]', None, uri) if encrypted_key: keyinfo.append(encrypted_key[0]) encrypted_data = encrypted_data_nodes[0] decrypted = OneLogin_Saml2_Utils.decrypt_element( encrypted_data, key, debug=debug, inplace=True) xml.replace(encrypted_assertion_nodes[0], decrypted) return xml
def testSignMetadata(self): """ Tests the signMetadata method of the OneLogin_Saml2_Metadata """ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) sp_data = settings.get_sp_data() security = settings.get_security_data() metadata = OneLogin_Saml2_Metadata.builder( sp_data, security['authnRequestsSigned'], security['wantAssertionsSigned'] ) self.assertIsNotNone(metadata) cert_path = settings.get_cert_path() key = self.file_contents(join(cert_path, 'sp.key')) cert = self.file_contents(join(cert_path, 'sp.crt')) signed_metadata = OneLogin_Saml2_Metadata.sign_metadata(metadata, key, cert) self.assertTrue(OneLogin_Saml2_Utils.validate_metadata_sign(signed_metadata, cert)) self.assertIn('<md:SPSSODescriptor', signed_metadata) self.assertIn('entityID="http://stuff.com/endpoints/metadata.php"', signed_metadata) self.assertIn('ID="ONELOGIN_', signed_metadata) self.assertIn('AuthnRequestsSigned="false"', signed_metadata) self.assertIn('WantAssertionsSigned="false"', signed_metadata) self.assertIn('<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"', signed_metadata) self.assertIn('Location="http://stuff.com/endpoints/endpoints/acs.php"', signed_metadata) self.assertIn('<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"', signed_metadata) self.assertIn(' Location="http://stuff.com/endpoints/endpoints/sls.php"/>', signed_metadata) self.assertIn('<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>', signed_metadata) self.assertIn('<ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>', signed_metadata) self.assertIn('<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>', signed_metadata) self.assertIn('<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>', signed_metadata) self.assertIn('<ds:Reference', signed_metadata) self.assertIn('<ds:KeyInfo><ds:X509Data>\n<ds:X509Certificate>', signed_metadata) with self.assertRaisesRegexp(Exception, 'Empty string supplied as input'): OneLogin_Saml2_Metadata.sign_metadata('', key, cert) signed_metadata_2 = OneLogin_Saml2_Metadata.sign_metadata(metadata, key, cert, OneLogin_Saml2_Constants.RSA_SHA256, OneLogin_Saml2_Constants.SHA384) self.assertTrue(OneLogin_Saml2_Utils.validate_metadata_sign(signed_metadata_2, cert)) self.assertIn('<md:SPSSODescriptor', signed_metadata_2) self.assertIn('entityID="http://stuff.com/endpoints/metadata.php"', signed_metadata_2) self.assertIn('ID="ONELOGIN_', signed_metadata_2) self.assertIn('AuthnRequestsSigned="false"', signed_metadata_2) self.assertIn('WantAssertionsSigned="false"', signed_metadata_2) self.assertIn('<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"', signed_metadata_2) self.assertIn('Location="http://stuff.com/endpoints/endpoints/acs.php"', signed_metadata_2) self.assertIn('<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"', signed_metadata_2) self.assertIn(' Location="http://stuff.com/endpoints/endpoints/sls.php"/>', signed_metadata_2) self.assertIn('<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>', signed_metadata_2) self.assertIn('<ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>', signed_metadata_2) self.assertIn('<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#sha384"/>', signed_metadata_2) self.assertIn('<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>', signed_metadata_2) self.assertIn('<ds:Reference', signed_metadata_2) self.assertIn('<ds:KeyInfo><ds:X509Data>\n<ds:X509Certificate>', signed_metadata_2)
def logout(self, return_to=None, name_id=None, session_index=None, nq=None, name_id_format=None, spnq=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 :param spnq: SP Name Qualifier :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 = self.logout_request_class( self._settings, name_id=name_id, session_index=session_index, nq=nq, name_id_format=name_id_format, spnq=spnq) 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 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 = [] self.__error_reason = None get_data = 'get_data' in self.__request_data and self.__request_data[ 'get_data'] if get_data and 'SAMLResponse' in get_data: logout_response = OneLogin_Saml2_Logout_Response( self.__settings, get_data['SAMLResponse']) self.__last_response = logout_response.get_xml() if not self.validate_response_signature(get_data): self.__errors.append('invalid_logout_response_signature') self.__errors.append( 'Signature validation failed. Logout Response rejected') elif 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') else: self.__last_message_id = logout_response.id if not keep_local_session: OneLogin_Saml2_Utils.delete_local_session( delete_session_cb) elif get_data and 'SAMLRequest' in get_data: logout_request = OneLogin_Saml2_Logout_Request( self.__settings, get_data['SAMLRequest']) self.__last_request = logout_request.get_xml() if not self.validate_request_signature(get_data): self.__errors.append("invalid_logout_request_signature") self.__errors.append( 'Signature validation failed. Logout Request rejected') elif 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 self.__last_message_id = logout_request.id response_builder = OneLogin_Saml2_Logout_Response( self.__settings) response_builder.build(in_response_to) self.__last_response = response_builder.get_xml() 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'] security = self.__settings.get_security_data() if security['logoutResponseSigned']: self.add_response_signature(parameters, 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 sso_saml_acs(request, idp_slug): """ ACS stands for "Assertion Consumer Service". The Identity Provider will send its response to this view after authenticating a user. This is often referred to as the "Entity ID" in the IdP's Service Provider configuration. In this view we verify the received SAML 2.0 response and then log in the user to CommCare HQ. """ request_id = request.session.get('AuthNRequestID') error_template = 'sso/acs_errors.html' try: request.saml2_auth.process_response(request_id=request_id) errors = request.saml2_auth.get_errors() except OneLogin_Saml2_Error as e: if e.code == OneLogin_Saml2_Error.SAML_RESPONSE_NOT_FOUND: return redirect("sso_saml_login", idp_slug=idp_slug) errors = [e] if errors: return render( request, error_template, { 'saml_error_reason': request.saml2_auth.get_last_error_reason() or errors[0], 'idp_type': "Azure AD", # we will update this later, 'docs_link': get_documentation_url(request.idp), }) if not request.saml2_auth.is_authenticated(): return render(request, 'sso/sso_request_denied.html', {}) if 'AuthNRequestID' in request.session: del request.session['AuthNRequestID'] store_saml_data_in_session(request) user = auth.authenticate( request=request, username=request.session['samlNameId'], idp_slug=idp_slug, is_handshake_successful=True, ) # we add the messages to the django messages framework here since # that middleware was not available for SsoBackend if hasattr(request, 'sso_new_user_messages'): for success_message in request.sso_new_user_messages['success']: messages.success(request, success_message) for error_message in request.sso_new_user_messages['error']: messages.error(request, error_message) if user: auth.login(request, user) # activate new project if needed async_signup = AsyncSignupRequest.get_by_username(user.username) if async_signup and async_signup.project_name: try: request_new_domain(request, async_signup.project_name, is_new_user=True, is_new_sso_user=True) except NameUnavailableException: # this should never happen, but in the off chance it does # we don't want to throw a 500 on this view messages.error( request, _("We were unable to create your requested project " "because the name was already taken." "Please contact support.")) AsyncSignupRequest.clear_data_for_username(user.username) relay_state = request.saml2_request_data['post_data'].get('RelayState') if relay_state not in [ OneLogin_Saml2_Utils.get_self_url(request.saml2_request_data), get_saml_login_url(request.idp), ]: # redirect to next=<relay_state> return HttpResponseRedirect( request.saml2_auth.redirect_to(relay_state)) return redirect("homepage") return render(request, error_template, { 'login_error': getattr(request, 'sso_login_error', None), })
def format_sp_key(self): """ Formats the private key. """ self.__sp['privateKey'] = OneLogin_Saml2_Utils.format_private_key(self.__sp['privateKey'])
def __init__(self, settings, request=None, name_id=None, session_index=None, nq=None, name_id_format=None, spnq=None): """ Constructs the Logout Request object. :param settings: Setting data :type settings: OneLogin_Saml2_Settings :param request: Optional. A LogoutRequest to be loaded instead build one. :type request: 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 :param spnq: SP Name Qualifier :type: string """ self.__settings = settings self.__error = None self.id = None if request is None: sp_data = self.__settings.get_sp_data() idp_data = self.__settings.get_idp_data() security = self.__settings.get_security_data() uid = OneLogin_Saml2_Utils.generate_unique_id() self.id = uid issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(OneLogin_Saml2_Utils.now()) cert = None if security['nameIdEncrypted']: exists_multix509enc = 'x509certMulti' in idp_data and \ 'encryption' in idp_data['x509certMulti'] and \ idp_data['x509certMulti']['encryption'] if exists_multix509enc: cert = idp_data['x509certMulti']['encryption'][0] else: cert = idp_data['x509cert'] if name_id is not None: if not name_id_format and sp_data['NameIDFormat'] != OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED: name_id_format = sp_data['NameIDFormat'] else: name_id = idp_data['entityId'] name_id_format = OneLogin_Saml2_Constants.NAMEID_ENTITY # From saml-core-2.0-os 8.3.6, when the entity Format is used: # "The NameQualifier, SPNameQualifier, and SPProvidedID attributes # MUST be omitted. if name_id_format and name_id_format == OneLogin_Saml2_Constants.NAMEID_ENTITY: nq = None spnq = None # NameID Format UNSPECIFIED omitted if name_id_format and name_id_format == OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED: name_id_format = None name_id_obj = OneLogin_Saml2_Utils.generate_name_id( name_id, spnq, name_id_format, cert, False, nq ) if session_index: session_index_str = '<samlp:SessionIndex>%s</samlp:SessionIndex>' % session_index else: session_index_str = '' logout_request = OneLogin_Saml2_Templates.LOGOUT_REQUEST % \ { 'id': uid, 'issue_instant': issue_instant, 'single_logout_url': idp_data['singleLogoutService']['url'], 'entity_id': sp_data['entityId'], 'name_id': name_id_obj, 'session_index': session_index_str, } else: logout_request = OneLogin_Saml2_Utils.decode_base64_and_inflate(request, ignore_zip=True) self.id = self.get_id(logout_request) self.__logout_request = compat.to_string(logout_request)
def format_idp_cert(self): """ Formats the IdP cert. """ self.__idp['x509cert'] = OneLogin_Saml2_Utils.format_cert( self.__idp['x509cert'])
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 testIsValidSign(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutRequest """ request_data = { 'http_host': 'example.com', 'script_name': 'index.html', 'get_data': { 'SAMLRequest': 'lVLBitswEP0Vo7tjeWzJtki8LIRCYLvbNksPewmyPc6K2pJqyXQ/v1LSQlroQi/DMJr33rwZbZ2cJysezNms/gt+X9H55G2etBOXlx1ZFy2MdMoJLWd0wvfieP/xQcCGCrsYb3ozkRvI+wjpHC5eGU2Sw35HTg3lA8hqZFwWFcMKsStpxbEsxoLXeQN9OdY1VAgk+YqLC8gdCUQB7tyKB+281D6UaF6mtEiBPudcABcMXkiyD26Ulv6CevXeOpFlVvlunb5ttEmV3ZjlnGn8YTRO5qx0NuBs8kzpAd829tXeucmR5NH4J/203I8el6gFRUqbFPJnyEV51Wq30by4TLW0/9ZyarYTxt4sBsjUYLMZvRykl1Fxm90SXVkfwx4P++T4KSafVzmpUcVJ/sfSrQZJPphllv79W8WKGtLx0ir8IrVTqD1pT2MH3QAMSs4KTvui71jeFFiwirOmprwPkYW063+5uRq4urHiiC4e8hCX3J5wqAEGaPpw9XB5JmkBdeDqSlkz6CmUXdl0Qae5kv2F/1384wu3PwE=', 'RelayState': '_1037fbc88ec82ce8e770b2bed1119747bb812a07e6', 'SigAlg': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', 'Signature': 'XCwCyI5cs7WhiJlB5ktSlWxSBxv+6q2xT3c8L7dLV6NQG9LHWhN7gf8qNsahSXfCzA0Ey9dp5BQ0EdRvAk2DIzKmJY6e3hvAIEp1zglHNjzkgcQmZCcrkK9Czi2Y1WkjOwR/WgUTUWsGJAVqVvlRZuS3zk3nxMrLH6f7toyvuJc=' } } settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) request = OneLogin_Saml2_Utils.decode_base64_and_inflate( request_data['get_data']['SAMLRequest']) settings.set_strict(False) logout_request = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) self.assertTrue(logout_request.is_valid(request_data)) relayState = request_data['get_data']['RelayState'] del request_data['get_data']['RelayState'] self.assertFalse(logout_request.is_valid(request_data)) request_data['get_data']['RelayState'] = relayState settings.set_strict(True) logout_request2 = OneLogin_Saml2_Logout_Request( settings, b64encode(request)) self.assertFalse(logout_request2.is_valid(request_data)) self.assertIn('The LogoutRequest was received at', logout_request2.get_error()) settings.set_strict(False) old_signature = request_data['get_data']['Signature'] request_data['get_data'][ 'Signature'] = 'vfWbbc47PkP3ejx4bjKsRX7lo9Ml1WRoE5J5owF/0mnyKHfSY6XbhO1wwjBV5vWdrUVX+xp6slHyAf4YoAsXFS0qhan6txDiZY4Oec6yE+l10iZbzvie06I4GPak4QrQ4gAyXOSzwCrRmJu4gnpeUxZ6IqKtdrKfAYRAcVf3333=' logout_request3 = OneLogin_Saml2_Logout_Request( settings, b64encode(request)) self.assertFalse(logout_request3.is_valid(request_data)) self.assertIn('Signature validation failed. Logout Request rejected', logout_request3.get_error()) request_data['get_data']['Signature'] = old_signature old_signature_algorithm = request_data['get_data']['SigAlg'] del request_data['get_data']['SigAlg'] self.assertTrue(logout_request3.is_valid(request_data)) request_data['get_data'][ 'RelayState'] = 'http://example.com/relaystate' self.assertFalse(logout_request3.is_valid(request_data)) self.assertIn('Signature validation failed. Logout Request rejected', logout_request3.get_error()) settings.set_strict(True) request_2 = request.replace( 'https://pitbulk.no-ip.org/newonelogin/demo1/index.php?sls', current_url) request_2 = request_2.replace( 'https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php', 'http://idp.example.com/') request_data['get_data'][ 'SAMLRequest'] = OneLogin_Saml2_Utils.deflate_and_base64_encode( request_2) logout_request4 = OneLogin_Saml2_Logout_Request( settings, b64encode(request_2)) self.assertFalse(logout_request4.is_valid(request_data)) self.assertIn('Signature validation failed. Logout Request rejected', logout_request4.get_error()) settings.set_strict(False) logout_request5 = OneLogin_Saml2_Logout_Request( settings, b64encode(request_2)) self.assertFalse(logout_request5.is_valid(request_data)) self.assertIn('Signature validation failed. Logout Request rejected', logout_request5.get_error()) request_data['get_data'][ 'SigAlg'] = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1' self.assertFalse(logout_request5.is_valid(request_data)) self.assertIn('Signature validation failed. Logout Request rejected', logout_request5.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'] logout_request6 = OneLogin_Saml2_Logout_Request( settings, b64encode(request_2)) self.assertFalse(logout_request6.is_valid(request_data)) self.assertIn( 'The Message of the Logout Request is not signed and the SP require it', logout_request6.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) logout_request7 = OneLogin_Saml2_Logout_Request( settings_2, b64encode(request_2)) self.assertFalse(logout_request7.is_valid(request_data)) self.assertEqual( 'In order to validate the sign on the Logout Request, the x509cert of the IdP is required', logout_request7.get_error())
def get_slo_response(self): fixture = self.get_fixture('/sso/slo_response.xml') return saml_utils.deflate_and_base64_encode(fixture)