def test_logout_service_global(self): settings.SAML_CONFIG = conf.create_conf( sp_host='sp.example.com', idp_hosts=['idp.example.com'], metadata_file='remote_metadata_one_idp.xml', ) self.do_login() # now simulate a global logout process initiated by another SP subject_id = views._get_subject_id(self.client.session) instant = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') saml_request = """<?xml version='1.0' encoding='UTF-8'?> <samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="_9961abbaae6d06d251226cb25e38bf8f468036e57e" Version="2.0" IssueInstant="%s" Destination="http://sp.example.com/saml2/ls/"><saml:Issuer>https://idp.example.com/simplesaml/saml2/idp/metadata.php</saml:Issuer><saml:NameID SPNameQualifier="http://sp.example.com/saml2/metadata/" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">%s</saml:NameID><samlp:SessionIndex>_1837687b7bc9faad85839dbeb319627889f3021757</samlp:SessionIndex></samlp:LogoutRequest>""" % ( instant, subject_id.text) response = self.client.get( reverse('saml2_ls'), { 'SAMLRequest': deflate_and_base64_encode(saml_request), }) self.assertEqual(response.status_code, 302) location = response['Location'] url = urlparse(location) self.assertEqual(url.hostname, 'idp.example.com') self.assertEqual(url.path, '/simplesaml/saml2/idp/SingleLogoutService.php') params = parse_qs(url.query) self.assertIn('SAMLResponse', params) saml_response = params['SAMLResponse'][0] if 'Response xmlns' not in decode_base64_and_inflate( saml_response).decode('utf-8'): raise Exception('Not a valid Response')
def test_logout_service_global(self): settings.SAML_CONFIG = conf.create_conf(sp_host='sp.example.com', idp_hosts=['idp.example.com']) self.do_login() # now simulate a global logout process initiated by another SP subject_id = views._get_subject_id(self.client.session) instant = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') saml_request = '<samlp:LogoutRequest ID="_9961abbaae6d06d251226cb25e38bf8f468036e57e" Version="2.0" IssueInstant="%s" Destination="http://sp.example.com/saml2/ls/" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"><saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://idp.example.com/simplesaml/saml2/idp/metadata.php</saml:Issuer><saml:NameID SPNameQualifier="http://sp.example.com/saml2/metadata/" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">%s</saml:NameID><samlp:SessionIndex>_1837687b7bc9faad85839dbeb319627889f3021757</samlp:SessionIndex></samlp:LogoutRequest>' % ( instant, subject_id) response = self.client.get('/ls/', { 'SAMLRequest': deflate_and_base64_encode(saml_request), }) self.assertEquals(response.status_code, 302) location = response['Location'] url = urlparse.urlparse(location) self.assertEquals(url.hostname, 'idp.example.com') self.assertEquals(url.path, '/simplesaml/saml2/idp/SingleLogoutService.php') params = urlparse.parse_qs(url.query) self.assert_('SAMLResponse' in params) saml_response = params['SAMLResponse'][0] expected_response = """<?xml version='1.0' encoding='UTF-8'?> <samlp:LogoutResponse Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="a140848e7ce2bce834d7264ecdde0151" InResponseTo="_9961abbaae6d06d251226cb25e38bf8f468036e57e" IssueInstant="2010-09-05T09:10:12Z" Version="2.0" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status></samlp:LogoutResponse>""" xml = decode_base64_and_inflate(saml_response) self.assertSAMLRequestsEquals(expected_response, xml)
def test_login_several_idps(self): settings.SAML_CONFIG = conf.create_conf( sp_host='sp.example.com', idp_hosts=['idp1.example.com', 'idp2.example.com', 'idp3.example.com'], metadata_file='remote_metadata_three_idps.xml', ) response = self.client.get(reverse('saml2_login')) # a WAYF page should be displayed self.assertContains(response, 'Where are you from?', status_code=200) for i in range(1, 4): link = '/login/?idp=https://idp%d.example.com/simplesaml/saml2/idp/metadata.php&next=/' self.assertContains(response, link % i) # click on the second idp response = self.client.get(reverse('saml2_login'), { 'idp': 'https://idp2.example.com/simplesaml/saml2/idp/metadata.php', 'next': '/', }) self.assertEqual(response.status_code, 302) location = response['Location'] url = urlparse(location) self.assertEqual(url.hostname, 'idp2.example.com') self.assertEqual(url.path, '/simplesaml/saml2/idp/SSOService.php') params = parse_qs(url.query) self.assertIn('SAMLRequest', params) self.assertIn('RelayState', params) saml_request = params['SAMLRequest'][0] self.assertIn('AuthnRequest xmlns', decode_base64_and_inflate( saml_request).decode('utf-8'))
def test_logout(self): settings.SAML_CONFIG = conf.create_conf( sp_host='sp.example.com', idp_hosts=['idp.example.com'], metadata_file='remote_metadata_one_idp.xml', ) self.do_login() response = self.client.get(reverse('saml2_logout')) self.assertEqual(response.status_code, 302) location = response['Location'] url = urlparse(location) self.assertEqual(url.hostname, 'idp.example.com') self.assertEqual(url.path, '/simplesaml/saml2/idp/SingleLogoutService.php') params = parse_qs(url.query) self.assertIn('SAMLRequest', params) saml_request = params['SAMLRequest'][0] if 'LogoutRequest xmlns' not in decode_base64_and_inflate( saml_request).decode('utf-8'): raise Exception('Not a valid LogoutRequest')
def test_logout_service_global(self): settings.SAML_CONFIG = conf.create_conf(sp_host='sp.example.com', idp_hosts=['idp.example.com']) self.do_login() # now simulate a global logout process initiated by another SP subject_id = views._get_subject_id(self.client.session) instant = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') saml_request = '<samlp:LogoutRequest ID="_9961abbaae6d06d251226cb25e38bf8f468036e57e" Version="2.0" IssueInstant="%s" Destination="http://sp.example.com/saml2/ls/" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"><saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://idp.example.com/simplesaml/saml2/idp/metadata.php</saml:Issuer><saml:NameID SPNameQualifier="http://sp.example.com/saml2/metadata/" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">%s</saml:NameID><samlp:SessionIndex>_1837687b7bc9faad85839dbeb319627889f3021757</samlp:SessionIndex></samlp:LogoutRequest>' % ( instant, subject_id) response = self.client.get( '/ls/', { 'SAMLRequest': deflate_and_base64_encode(saml_request), }) self.assertEquals(response.status_code, 302) location = response['Location'] url = urlparse.urlparse(location) self.assertEquals(url.hostname, 'idp.example.com') self.assertEquals(url.path, '/simplesaml/saml2/idp/SingleLogoutService.php') params = urlparse.parse_qs(url.query) self.assert_('SAMLResponse' in params) saml_response = params['SAMLResponse'][0] expected_response = """<?xml version='1.0' encoding='UTF-8'?> <samlp:LogoutResponse Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="a140848e7ce2bce834d7264ecdde0151" InResponseTo="_9961abbaae6d06d251226cb25e38bf8f468036e57e" IssueInstant="2010-09-05T09:10:12Z" Version="2.0" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status></samlp:LogoutResponse>""" xml = decode_base64_and_inflate(saml_response) self.assertSAMLRequestsEquals(expected_response, xml)
def unravel(self, txt, binding, msgtype="response"): #logger.debug("unravel '%s'" % txt) if binding not in [ BINDING_HTTP_REDIRECT, BINDING_HTTP_POST, BINDING_SOAP, BINDING_URI, BINDING_HTTP_ARTIFACT, None ]: raise ValueError("Don't know how to handle '%s'" % binding) else: try: if binding == BINDING_HTTP_REDIRECT: xmlstr = decode_base64_and_inflate(txt) elif binding == BINDING_HTTP_POST: xmlstr = base64.b64decode(txt) elif binding == BINDING_SOAP: func = getattr(soap, "parse_soap_enveloped_saml_%s" % msgtype) xmlstr = func(txt) elif binding == BINDING_HTTP_ARTIFACT: xmlstr = base64.b64decode(txt) else: xmlstr = txt except Exception: raise UnravelError() return xmlstr
def unravel(txt, binding, msgtype="response"): """ Will unpack the received text. Depending on the context the original response may have been transformed before transmission. :param txt: :param binding: :param msgtype: :return: """ #logger.debug("unravel '%s'" % txt) if binding not in [BINDING_HTTP_REDIRECT, BINDING_HTTP_POST, BINDING_SOAP, BINDING_URI, BINDING_HTTP_ARTIFACT, None]: raise ValueError("Don't know how to handle '%s'" % binding) else: try: if binding == BINDING_HTTP_REDIRECT: xmlstr = decode_base64_and_inflate(txt) elif binding == BINDING_HTTP_POST: xmlstr = base64.b64decode(txt) elif binding == BINDING_SOAP: func = getattr(soap, "parse_soap_enveloped_saml_%s" % msgtype) xmlstr = func(txt) elif binding == BINDING_HTTP_ARTIFACT: xmlstr = base64.b64decode(txt) else: xmlstr = txt except Exception: raise UnravelError() return xmlstr
def test_logout(self): settings.SAML_CONFIG = conf.create_conf( sp_host='sp.example.com', idp_hosts=['idp.example.com'], metadata_file='remote_metadata_one_idp.xml', ) self.do_login() response = self.client.get('/logout/') self.assertEquals(response.status_code, 302) location = response['Location'] url = urlparse.urlparse(location) self.assertEquals(url.hostname, 'idp.example.com') self.assertEquals(url.path, '/simplesaml/saml2/idp/SingleLogoutService.php') params = urlparse.parse_qs(url.query) self.assert_('SAMLRequest' in params) saml_request = params['SAMLRequest'][0] expected_request26 = """<?xml version='1.0' encoding='UTF-8'?> <samlp:LogoutRequest Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" Reason="" Version="2.0" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" SPNameQualifier="http://sp.example.com/saml2/metadata/" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">58bcc81ea14700f66aeb707a0eff1360</saml:NameID></samlp:LogoutRequest>""" expected_request27 = """<?xml version='1.0' encoding='UTF-8'?> <samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" Reason="" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" SPNameQualifier="http://sp.example.com/saml2/metadata/">58bcc81ea14700f66aeb707a0eff1360</saml:NameID></samlp:LogoutRequest>""" self.assertSAMLRequestsEquals(decode_base64_and_inflate(saml_request), { '2.6': expected_request26, '2.7': expected_request27 })
def test_logout(self): settings.SAML_CONFIG = conf.create_conf( sp_host='sp.example.com', idp_hosts=['idp.example.com'], metadata_file='remote_metadata_one_idp.xml', ) self.do_login() response = self.client.get('/logout/') self.assertEquals(response.status_code, 302) location = response['Location'] url = urlparse.urlparse(location) self.assertEquals(url.hostname, 'idp.example.com') self.assertEquals(url.path, '/simplesaml/saml2/idp/SingleLogoutService.php') params = urlparse.parse_qs(url.query) self.assert_('SAMLRequest' in params) saml_request = params['SAMLRequest'][0] expected_request26 = """<?xml version='1.0' encoding='UTF-8'?> <samlp:LogoutRequest Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" Reason="" Version="2.0" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" SPNameQualifier="http://sp.example.com/saml2/metadata/" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">58bcc81ea14700f66aeb707a0eff1360</saml:NameID></samlp:LogoutRequest>""" expected_request27 = """<?xml version='1.0' encoding='UTF-8'?> <samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" Reason="" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" SPNameQualifier="http://sp.example.com/saml2/metadata/">58bcc81ea14700f66aeb707a0eff1360</saml:NameID></samlp:LogoutRequest>""" self.assertSAMLRequestsEquals(decode_base64_and_inflate(saml_request), {'2.6': expected_request26, '2.7': expected_request27})
def test_login_several_idps(self): settings.SAML_CONFIG = conf.create_conf(sp_host='sp.example.com', idp_hosts=['idp1.example.com', 'idp2.example.com', 'idp3.example.com']) response = self.client.get('/login/') # a WAYF page should be displayed self.assertContains(response, 'Where are you from?', status_code=200) for i in range(1, 4): link = '/login/?idp=https://idp%d.example.com/simplesaml/saml2/idp/metadata.php&next=/' self.assertContains(response, link % i) # click on the second idp response = self.client.get('/login/', { 'idp': 'https://idp2.example.com/simplesaml/saml2/idp/metadata.php', 'next': '/', }) self.assertEquals(response.status_code, 302) location = response['Location'] url = urlparse.urlparse(location) self.assertEquals(url.hostname, 'idp2.example.com') self.assertEquals(url.path, '/simplesaml/saml2/idp/SSOService.php') params = urlparse.parse_qs(url.query) self.assert_('SAMLRequest' in params) self.assert_('RelayState' in params) saml_request = params['SAMLRequest'][0] expected_request = """<?xml version='1.0' encoding='UTF-8'?> <samlp:AuthnRequest AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp2.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" ProviderName="Test SP" Version="2.0" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="true" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" /></samlp:AuthnRequest>""" xml = decode_base64_and_inflate(saml_request) self.assertSAMLRequestsEquals(expected_request, xml)
def test_inflate_then_deflate(): str = """Selma Lagerlöf (1858-1940) was born in Östra Emterwik, Värmland, Sweden. She was brought up on Mårbacka, the family estate, which she did not leave until 1881, when she went to a teachers' college at Stockholm""" interm = utils.deflate_and_base64_encode(str) bis = utils.decode_base64_and_inflate(interm) assert bis == str
def test_inflate_then_deflate(): txt = """Selma Lagerlöf (1858-1940) was born in Östra Emterwik, Värmland, Sweden. She was brought up on Mårbacka, the family estate, which she did not leave until 1881, when she went to a teachers' college at Stockholm""" interm = utils.deflate_and_base64_encode(txt) bis = utils.decode_base64_and_inflate(interm) assert bis == txt
def test_Saml_handle_logout_request(self): not_on_or_after = time.time() + 3600 identity = { 'id-1': { 'https://sso.example.com/idp/metadata': (not_on_or_after, { 'authn_info': [], 'name_id': 'id-1', 'not_on_or_after': not_on_or_after, 'came_from': '/next', 'ava': { 'uid': ['123456'] } }) } } state = { 'entity_ids': ['https://sso.example.com/idp/metadata'], 'subject_id': 'id-1', 'return_to': '/next' } # modifying config in this test, make copy so as not to effect # following tests. tmp_sp_config = copy.deepcopy(sp_config) # create a response to assert upon sp = auth.Saml(tmp_sp_config) logout_request = create_logout_request( 'id-1', destination='https://foo.example.com/sp/slo', issuer_entity_id='https://sso.example.com/idp/metadata', req_entity_id='https://sso.example.com/idp/metadata') # test SAMLRequest logout with self.app.test_request_context( '/', method='GET', query_string=dict( SAMLRequest=deflate_and_base64_encode(str(logout_request)), RelayState=deflate_and_base64_encode(logout_request.id))): # first need to be logged in, let's pretend session['_saml_identity'] = identity session['_saml_subject_id'] = 'id-1' session['_saml_state'] = {logout_request.id: state} success, resp = sp.handle_logout(request, next_url='/next') self.assertTrue(success) self.assertEqual(resp.status_code, 302) self.assert_("SAMLResponse" in resp.headers['Location']) url = urlparse.urlparse(resp.headers['Location']) params = urlparse.parse_qs(url.query) self.assert_('SAMLResponse' in params) logout = samlp.logout_response_from_string( decode_base64_and_inflate(params['SAMLResponse'][0])) self.assertEqual(logout.status.status_code.value, 'urn:oasis:names:tc:SAML:2.0:status:Success') self.assertEqual(logout.destination, 'https://sso.example.com/idp/slo')
def test_Saml_authenticate(self): # modifying config in this test, make copy so as not to effect # following tests. tmp_sp_config = copy.deepcopy(sp_config) # test signed authentication request with self.app.test_request_context('/', method='GET'): sp = auth.Saml(tmp_sp_config) resp = sp.authenticate(next_url='/next') self.assertEqual(resp.status_code, 302) self.assert_('SAMLRequest' in resp.headers['Location']) url = urlparse.urlparse(resp.headers['Location']) self.assertEqual(url.hostname, 'sso.example.com') self.assertEqual(url.path, '/idp/sso') params = urlparse.parse_qs(url.query) self.assert_('SAMLRequest' in params) self.assertEqual(params['RelayState'], ['/next']) authn = samlp.authn_request_from_string( decode_base64_and_inflate(params['SAMLRequest'][0])) self.assertEqual(authn.destination, 'https://sso.example.com/idp/sso') self.assertEqual(authn.assertion_consumer_service_url, 'https://foo.example.com/sp/acs') self.assertEqual(authn.protocol_binding, BINDING_HTTP_POST) self.assertIsNotNone(authn.signature) self.assertEqual(session['_saml_outstanding_queries'], {authn.id: '/next'}) # test un-signed authentication request with self.app.test_request_context('/', method='GET'): tmp_sp_config['key_file'] = None tmp_sp_config['service']['sp']['authn_requests_signed'] = None sp = auth.Saml(tmp_sp_config) resp = sp.authenticate(next_url='/next') self.assertEqual(resp.status_code, 302) self.assert_('SAMLRequest' in resp.headers['Location']) url = urlparse.urlparse(resp.headers['Location']) params = urlparse.parse_qs(url.query) authn = samlp.authn_request_from_string( decode_base64_and_inflate(params['SAMLRequest'][0])) self.assertIsNone(authn.signature)
def test_authenticate(self): print self.client.config.idps() id, response = self.client.do_authenticate( "urn:mace:example.com:saml:roland:idp", "http://www.example.com/relay_state") assert response[0] == "Location" o = urlparse(response[1]) qdict = parse_qs(o.query) assert _leq(qdict.keys(), ['SAMLRequest', 'RelayState']) saml_request = decode_base64_and_inflate(qdict["SAMLRequest"][0]) print saml_request authnreq = samlp.authn_request_from_string(saml_request)
def logout_response(self, xmlstr, log=None, binding=BINDING_SOAP): """ Deal with a LogoutResponse :param xmlstr: The response as a xml string :param log: logging function :param binding: What type of binding this message came through. :return: None if the reply doesn't contain a valid SAML LogoutResponse, otherwise the reponse if the logout was successful and None if it was not. """ response = None if log is None: log = self.logger if xmlstr: try: # expected return address return_addr = self.config.endpoint("single_logout_service", binding=binding)[0] except Exception: if log: log.info("Not supposed to handle this!") return None try: response = LogoutResponse(self.sec, return_addr, debug=self.debug, log=log) except Exception, exc: if log: log.info("%s" % exc) return None if binding == BINDING_HTTP_REDIRECT: xmlstr = decode_base64_and_inflate(xmlstr) elif binding == BINDING_HTTP_POST: xmlstr = base64.b64decode(xmlstr) if log: log.debug("XMLSTR: %s" % xmlstr) response = response.loads(xmlstr, False) if response: response = response.verify() if not response: return None if log: log.debug(response) return self.handle_logout_response(response, log)
def test_inflate_then_deflate(): txt = """Selma Lagerlöf (1858-1940) was born in Östra Emterwik, Värmland, Sweden. She was brought up on Mårbacka, the family estate, which she did not leave until 1881, when she went to a teachers' college at Stockholm""" if not isinstance(txt, six.binary_type): txt = txt.encode('utf-8') interm = utils.deflate_and_base64_encode(txt) bis = utils.decode_base64_and_inflate(interm) if not isinstance(bis, six.binary_type): bis = bis.encode('utf-8') assert bis == txt
def do(self, message, binding, relay_state="", mtype="response"): try: txt = decode_base64_and_inflate(message) is_logout_request = 'LogoutRequest' in txt.split('>', 1)[0] except: # TODO: parse the XML correctly is_logout_request = False if is_logout_request: self.sp.parse_logout_request(message, binding) else: self.sp.parse_logout_request_response(message, binding) return finish_logout(self.environ, self.start_response)
def do(self, message, binding, relay_state="", mtype="response"): try: txt = decode_base64_and_inflate(message) is_logout_request = "LogoutRequest" in txt.split(">", 1)[0] except: # TODO: parse the XML correctly is_logout_request = False if is_logout_request: self.sp.parse_logout_request(message, binding) else: self.sp.parse_logout_request_response(message, binding) return finish_logout(self.environ, self.start_response)
def test_authenticate(self): print self.client.config.idps() (sid, response) = self.client.authenticate( "urn:mace:example.com:saml:roland:idp", "http://www.example.com/relay_state") assert sid is not None assert response[0] == "Location" o = urlparse(response[1]) qdict = parse_qs(o.query) assert _leq(qdict.keys(), ['SAMLRequest', 'RelayState']) saml_request = decode_base64_and_inflate(qdict["SAMLRequest"][0]) print saml_request authnreq = samlp.authn_request_from_string(saml_request) assert authnreq.id == sid
def http_redirect_logout_request(self, get, subject_id, log=None): """ Deal with a LogoutRequest received through HTTP redirect :param get: The request as a dictionary :param subject_id: the id of the current logged user :return: a tuple with a list of header tuples (presently only location) and a status which will be True in case of success or False otherwise. """ headers = [] success = False if log is None: log = self.logger try: saml_request = get['SAMLRequest'] except KeyError: return None if saml_request: xml = decode_base64_and_inflate(saml_request) request = samlp.logout_request_from_string(xml) if log: log.debug(request) if request.name_id.text == subject_id: status = samlp.STATUS_SUCCESS success = self.local_logout(subject_id) else: status = samlp.STATUS_REQUEST_DENIED response, destination = self .make_logout_response( request.issuer.text, request.id, status) if log: log.info("RESPONSE: {0:>s}".format(response)) if 'RelayState' in get: rstate = get['RelayState'] else: rstate = "" (headers, _body) = http_redirect_message(str(response), destination, rstate, 'SAMLResponse') return headers, success
def test_login_one_idp(self): # monkey patch SAML configuration settings.SAML_CONFIG = conf.create_conf( sp_host='sp.example.com', idp_hosts=['idp.example.com'], metadata_file='remote_metadata_one_idp.xml', ) response = self.client.get(reverse('saml2_login')) self.assertEquals(response.status_code, 302) location = response['Location'] url = urlparse(location) self.assertEquals(url.hostname, 'idp.example.com') self.assertEquals(url.path, '/simplesaml/saml2/idp/SSOService.php') params = parse_qs(url.query) self.assert_('SAMLRequest' in params) self.assert_('RelayState' in params) saml_request = params['SAMLRequest'][0] if PY_VERSION < (2, 7): expected_request = """<?xml version='1.0' encoding='UTF-8'?> <samlp:AuthnRequest AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="false" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" /></samlp:AuthnRequest>""" elif PY_VERSION < (3, ): expected_request = """<?xml version='1.0' encoding='UTF-8'?> <samlp:AuthnRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="false" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" /></samlp:AuthnRequest>""" else: expected_request = """<samlp:AuthnRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="false" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" /></samlp:AuthnRequest>""" self.assertSAMLRequestsEquals( decode_base64_and_inflate(saml_request).decode('utf-8'), expected_request) # if we set a next arg in the login view, it is preserverd # in the RelayState argument next = '/another-view/' response = self.client.get(reverse('saml2_login'), {'next': next}) self.assertEquals(response.status_code, 302) location = response['Location'] url = urlparse(location) self.assertEquals(url.hostname, 'idp.example.com') self.assertEquals(url.path, '/simplesaml/saml2/idp/SSOService.php') params = parse_qs(url.query) self.assert_('SAMLRequest' in params) self.assert_('RelayState' in params) self.assertEquals(params['RelayState'][0], next)
def test_Saml_handle_logout_request(self): not_on_or_after = time.time()+3600 identity = {'id-1': { 'https://sso.example.com/idp/metadata': ( not_on_or_after, { 'authn_info': [], 'name_id': 'id-1', 'not_on_or_after': not_on_or_after, 'came_from': '/next', 'ava': {'uid': ['123456']} } ) }} state = { 'entity_ids': ['https://sso.example.com/idp/metadata'], 'subject_id': 'id-1', 'return_to': '/next' } # modifying config in this test, make copy so as not to effect # following tests. tmp_sp_config = copy.deepcopy(sp_config) # create a response to assert upon sp = auth.Saml(tmp_sp_config) logout_request = create_logout_request('id-1', destination='https://foo.example.com/sp/slo', issuer_entity_id='https://sso.example.com/idp/metadata', req_entity_id='https://sso.example.com/idp/metadata') # test SAMLRequest logout with self.app.test_request_context('/', method='GET', query_string=dict( SAMLRequest=deflate_and_base64_encode(str(logout_request)), RelayState=deflate_and_base64_encode(logout_request.id))): # first need to be logged in, let's pretend session['_saml_identity'] = identity session['_saml_subject_id'] = 'id-1' session['_saml_state'] = {logout_request.id: state} success, resp = sp.handle_logout(request, next_url='/next') self.assertTrue(success) self.assertEqual(resp.status_code, 302) self.assert_("SAMLResponse" in resp.headers['Location']) url = urlparse.urlparse(resp.headers['Location']) params = urlparse.parse_qs(url.query) self.assert_('SAMLResponse' in params) logout = samlp.logout_response_from_string( decode_base64_and_inflate(params['SAMLResponse'][0])) self.assertEqual(logout.status.status_code.value, 'urn:oasis:names:tc:SAML:2.0:status:Success') self.assertEqual(logout.destination, 'https://sso.example.com/idp/slo')
def test_login_one_idp(self): # monkey patch SAML configuration settings.SAML_CONFIG = conf.create_conf( sp_host='sp.example.com', idp_hosts=['idp.example.com'], metadata_file='remote_metadata_one_idp.xml', ) response = self.client.get(reverse('saml2_login')) self.assertEquals(response.status_code, 302) location = response['Location'] url = urlparse(location) self.assertEquals(url.hostname, 'idp.example.com') self.assertEquals(url.path, '/simplesaml/saml2/idp/SSOService.php') params = parse_qs(url.query) self.assert_('SAMLRequest' in params) self.assert_('RelayState' in params) saml_request = params['SAMLRequest'][0] if PY_VERSION < (2, 7): expected_request = """<?xml version='1.0' encoding='UTF-8'?> <samlp:AuthnRequest AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="false" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" /></samlp:AuthnRequest>""" elif PY_VERSION < (3,): expected_request = """<?xml version='1.0' encoding='UTF-8'?> <samlp:AuthnRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="false" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" /></samlp:AuthnRequest>""" else: expected_request = """<samlp:AuthnRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="false" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" /></samlp:AuthnRequest>""" self.assertSAMLRequestsEquals( decode_base64_and_inflate(saml_request).decode('utf-8'), expected_request) # if we set a next arg in the login view, it is preserverd # in the RelayState argument next = '/another-view/' response = self.client.get(reverse('saml2_login'), {'next': next}) self.assertEquals(response.status_code, 302) location = response['Location'] url = urlparse(location) self.assertEquals(url.hostname, 'idp.example.com') self.assertEquals(url.path, '/simplesaml/saml2/idp/SSOService.php') params = parse_qs(url.query) self.assert_('SAMLRequest' in params) self.assert_('RelayState' in params) self.assertEquals(params['RelayState'][0], next)
def unravel(self, txt, binding, msgtype="response"): #logger.debug("unravel '%s'" % txt) if binding == BINDING_HTTP_REDIRECT: xmlstr = decode_base64_and_inflate(txt) elif binding == BINDING_HTTP_POST: xmlstr = base64.b64decode(txt) elif binding == BINDING_SOAP: func = getattr(soap, "parse_soap_enveloped_saml_%s" % msgtype) xmlstr = func(txt) elif binding == BINDING_URI or binding is None: xmlstr = txt else: raise ValueError("Don't know how to handle '%s'" % binding) return xmlstr
def test_logout_service_local(self): settings.SAML_CONFIG = conf.create_conf( sp_host='sp.example.com', idp_hosts=['idp.example.com'], metadata_file='remote_metadata_one_idp.xml', ) self.do_login() response = self.client.get(reverse('saml2_logout')) self.assertEquals(response.status_code, 302) location = response['Location'] url = urlparse(location) self.assertEquals(url.hostname, 'idp.example.com') self.assertEquals(url.path, '/simplesaml/saml2/idp/SingleLogoutService.php') params = parse_qs(url.query) self.assert_('SAMLRequest' in params) saml_request = params['SAMLRequest'][0] if PY_VERSION < (2, 7): expected_request = """<?xml version='1.0' encoing='UTF-8'?> <samlp:LogoutRequest Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" Reason="" Version="2.0" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" SPNameQualifier="http://sp.example.com/saml2/metadata/" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">58bcc81ea14700f66aeb707a0eff1360</saml:NameID></samlp:LogoutRequest>""" elif PY_VERSION < (3, ): expected_request = """<?xml version='1.0' encoding='UTF-8'?> <samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" Reason="" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" SPNameQualifier="http://sp.example.com/saml2/metadata/">58bcc81ea14700f66aeb707a0eff1360</saml:NameID><samlp:SessionIndex>a0123456789abcdef0123456789abcdef</samlp:SessionIndex></samlp:LogoutRequest>""" else: expected_request = """<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" Reason="" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" SPNameQualifier="http://sp.example.com/saml2/metadata/">58bcc81ea14700f66aeb707a0eff1360</saml:NameID><samlp:SessionIndex>a0123456789abcdef0123456789abcdef</samlp:SessionIndex></samlp:LogoutRequest>""" self.assertSAMLRequestsEquals( decode_base64_and_inflate(saml_request).decode('utf-8'), expected_request) # now simulate a logout response sent by the idp request_id = re.findall(r' ID="(.*?)" ', xml)[0] instant = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') saml_response = """<?xml version='1.0' encoding='UTF-8'?> <samlp:LogoutResponse xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="http://sp.example.com/saml2/ls/" ID="a140848e7ce2bce834d7264ecdde0151" InResponseTo="%s" IssueInstant="%s" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">https://idp.example.com/simplesaml/saml2/idp/metadata.php</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status></samlp:LogoutResponse>""" % ( request_id, instant) response = self.client.get( reverse('saml2_ls'), { 'SAMLResponse': deflate_and_base64_encode(saml_response), }) self.assertContains(response, "Logged out", status_code=200) self.assertEquals(self.client.session.keys(), [])
def _loads(self, xmldata, decode=True): if decode: logger.debug("Expected to decode and inflate xml data") decoded_xml = s_utils.decode_base64_and_inflate(xmldata) else: decoded_xml = xmldata # own copy self.xmlstr = decoded_xml[:] logger.info("xmlstr: %s" % (self.xmlstr, )) try: self.message = self.signature_check(decoded_xml) except TypeError: raise except Exception, excp: logger.info("EXCEPTION: %s", excp)
def _loads(self, xmldata, decode=True): if decode: logger.debug("Expected to decode and inflate xml data") decoded_xml = s_utils.decode_base64_and_inflate(xmldata) else: decoded_xml = xmldata # own copy self.xmlstr = decoded_xml[:] logger.info("xmlstr: %s" % (self.xmlstr,)) try: self.message = self.signature_check(decoded_xml) except TypeError: raise except Exception, excp: logger.info("EXCEPTION: %s", excp)
def test_login_several_idps(self): settings.SAML_CONFIG = conf.create_conf( sp_host='sp.example.com', idp_hosts=[ 'idp1.example.com', 'idp2.example.com', 'idp3.example.com' ], metadata_file='remote_metadata_three_idps.xml', ) response = self.client.get(reverse('saml2_login')) # a WAYF page should be displayed self.assertContains(response, 'Where are you from?', status_code=200) for i in range(1, 4): link = '/login/?idp=https://idp%d.example.com/simplesaml/saml2/idp/metadata.php&next=/' self.assertContains(response, link % i) # click on the second idp response = self.client.get( reverse('saml2_login'), { 'idp': 'https://idp2.example.com/simplesaml/saml2/idp/metadata.php', 'next': '/', }) self.assertEquals(response.status_code, 302) location = response['Location'] url = urlparse(location) self.assertEquals(url.hostname, 'idp2.example.com') self.assertEquals(url.path, '/simplesaml/saml2/idp/SSOService.php') params = parse_qs(url.query) self.assert_('SAMLRequest' in params) self.assert_('RelayState' in params) saml_request = params['SAMLRequest'][0] if PY_VERSION < (2, 7): expected_request = """<?xml version='1.0' encoding='UTF-8'?> <samlp:AuthnRequest AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp2.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="false" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" /></samlp:AuthnRequest>""" elif PY_VERSION < (3, ): expected_request = """<?xml version='1.0' encoding='UTF-8'?> <samlp:AuthnRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp2.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="false" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" /></samlp:AuthnRequest>""" else: expected_request = """<samlp:AuthnRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp2.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="false" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" /></samlp:AuthnRequest>""" self.assertSAMLRequestsEquals( decode_base64_and_inflate(saml_request).decode('utf-8'), expected_request)
def test_logout_service_local(self): settings.SAML_CONFIG = conf.create_conf( sp_host='sp.example.com', idp_hosts=['idp.example.com'], metadata_file='remote_metadata_one_idp.xml', ) self.do_login() response = self.client.get(reverse('saml2_logout')) self.assertEquals(response.status_code, 302) location = response['Location'] url = urlparse(location) self.assertEquals(url.hostname, 'idp.example.com') self.assertEquals(url.path, '/simplesaml/saml2/idp/SingleLogoutService.php') params = parse_qs(url.query) self.assert_('SAMLRequest' in params) saml_request = params['SAMLRequest'][0] if PY_VERSION < (2, 7): expected_request = """<?xml version='1.0' encoing='UTF-8'?> <samlp:LogoutRequest Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" Reason="" Version="2.0" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" SPNameQualifier="http://sp.example.com/saml2/metadata/" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">58bcc81ea14700f66aeb707a0eff1360</saml:NameID></samlp:LogoutRequest>""" elif PY_VERSION < (3,): expected_request = """<?xml version='1.0' encoding='UTF-8'?> <samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" Reason="" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" SPNameQualifier="http://sp.example.com/saml2/metadata/">58bcc81ea14700f66aeb707a0eff1360</saml:NameID><samlp:SessionIndex>a0123456789abcdef0123456789abcdef</samlp:SessionIndex></samlp:LogoutRequest>""" else: expected_request = """<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" Reason="" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" SPNameQualifier="http://sp.example.com/saml2/metadata/">58bcc81ea14700f66aeb707a0eff1360</saml:NameID><samlp:SessionIndex>a0123456789abcdef0123456789abcdef</samlp:SessionIndex></samlp:LogoutRequest>""" self.assertSAMLRequestsEquals(decode_base64_and_inflate(saml_request).decode('utf-8'), expected_request) # now simulate a logout response sent by the idp request_id = re.findall(r' ID="(.*?)" ', xml)[0] instant = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') saml_response = """<?xml version='1.0' encoding='UTF-8'?> <samlp:LogoutResponse xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="http://sp.example.com/saml2/ls/" ID="a140848e7ce2bce834d7264ecdde0151" InResponseTo="%s" IssueInstant="%s" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">https://idp.example.com/simplesaml/saml2/idp/metadata.php</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status></samlp:LogoutResponse>""" % ( request_id, instant) response = self.client.get(reverse('saml2_ls'), { 'SAMLResponse': deflate_and_base64_encode(saml_response), }) self.assertContains(response, "Logged out", status_code=200) self.assertEquals(self.client.session.keys(), [])
def logout_request_response(self, xmlstr, binding=BINDING_SOAP): """ Deal with a LogoutResponse :param xmlstr: The response as a xml string :param binding: What type of binding this message came through. :return: None if the reply doesn't contain a valid SAML LogoutResponse, otherwise the reponse if the logout was successful and None if it was not. """ response = None if xmlstr: if binding == BINDING_HTTP_REDIRECT: try: # expected return address return_addr = self.config.endpoint("single_logout_service", binding=binding)[0] except Exception: logger.info("Not supposed to handle this!") return None else: return_addr = None try: response = LogoutResponse(self.sec, return_addr) except Exception, exc: logger.info("%s" % exc) return None if binding == BINDING_HTTP_REDIRECT: xmlstr = decode_base64_and_inflate(xmlstr) elif binding == BINDING_HTTP_POST: xmlstr = base64.b64decode(xmlstr) logger.debug("XMLSTR: %s" % xmlstr) response = response.loads(xmlstr, False) if response: response = response.verify() if not response: return None logger.debug(response)
def http_redirect_logout_request(self, get, subject_id): """ Deal with a LogoutRequest received through HTTP redirect :param get: The request as a dictionary :param subject_id: the id of the current logged user :return: a tuple with a list of header tuples (presently only location) and a status which will be True in case of success or False otherwise. """ headers = [] success = False try: saml_request = get['SAMLRequest'] except KeyError: return None if saml_request: xml = decode_base64_and_inflate(saml_request) request = samlp.logout_request_from_string(xml) logger.debug(request) if request.name_id.text == subject_id: status = samlp.STATUS_SUCCESS success = self.local_logout(subject_id) else: status = samlp.STATUS_REQUEST_DENIED response, destination = self.make_logout_response( request.issuer.text, request.id, status) logger.info("RESPONSE: {0:>s}".format(response)) if 'RelayState' in get: rstate = get['RelayState'] else: rstate = "" (headers, _body) = http_redirect_message(str(response), destination, rstate, 'SAMLResponse') return headers, success
def test_logout_service_local(self): settings.SAML_CONFIG = conf.create_conf( sp_host='sp.example.com', idp_hosts=['idp.example.com'], metadata_file='remote_metadata_one_idp.xml', ) self.do_login() response = self.client.get(reverse('saml2_logout')) self.assertEqual(response.status_code, 302) location = response['Location'] url = urlparse(location) self.assertEqual(url.hostname, 'idp.example.com') self.assertEqual(url.path, '/simplesaml/saml2/idp/SingleLogoutService.php') params = parse_qs(url.query) self.assertIn('SAMLRequest', params) saml_request = params['SAMLRequest'][0] self.assertIn('LogoutRequest xmlns', decode_base64_and_inflate(saml_request).decode('utf-8'), 'Not a valid LogoutRequest') # now simulate a logout response sent by the idp expected_request = """<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="XXXXXXXXXXXXXXXXXXXXXX" Version="2.0" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" Reason=""><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID SPNameQualifier="http://sp.example.com/saml2/metadata/" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">1f87035b4c1325b296a53d92097e6b3fa36d7e30ee82e3fcb0680d60243c1f03</saml:NameID><samlp:SessionIndex>a0123456789abcdef0123456789abcdef</samlp:SessionIndex></samlp:LogoutRequest>""" request_id = re.findall(r' ID="(.*?)" ', expected_request)[0] instant = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') saml_response = """<?xml version='1.0' encoding='UTF-8'?> <samlp:LogoutResponse xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="http://sp.example.com/saml2/ls/" ID="a140848e7ce2bce834d7264ecdde0151" InResponseTo="%s" IssueInstant="%s" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">https://idp.example.com/simplesaml/saml2/idp/metadata.php</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status></samlp:LogoutResponse>""" % ( request_id, instant) response = self.client.get( reverse('saml2_ls'), { 'SAMLResponse': deflate_and_base64_encode(saml_response), }) self.assertContains(response, "Logged out", status_code=200) self.assertListEqual(list(self.client.session.keys()), [])
def test_authenticate_no_args(self): id, response = self.client.do_authenticate(relay_state="http://www.example.com/relay_state") assert response[0] == "Location" o = urlparse(response[1]) qdict = parse_qs(o.query) assert _leq(qdict.keys(), ['SAMLRequest', 'RelayState']) saml_request = decode_base64_and_inflate(qdict["SAMLRequest"][0]) assert qdict["RelayState"][0] == "http://www.example.com/relay_state" print saml_request authnreq = samlp.authn_request_from_string(saml_request) print authnreq.keyswv() assert authnreq.destination == "http://localhost:8088/sso" assert authnreq.assertion_consumer_service_url == "http://lingon.catalogix.se:8087/" assert authnreq.provider_name == "urn:mace:example.com:saml:roland:sp" assert authnreq.protocol_binding == BINDING_HTTP_REDIRECT name_id_policy = authnreq.name_id_policy assert name_id_policy.allow_create == "false" assert name_id_policy.format == NAMEID_FORMAT_PERSISTENT issuer = authnreq.issuer assert issuer.text == "urn:mace:example.com:saml:roland:sp"
def http_redirect_logout_request_check_session_index(self, get, session_index, log=None): """ Deal with a LogoutRequest received through HTTP redirect :param get: The request as a dictionary :param subject_id: the id of the current logged user :return: a tuple with a list of header tuples (presently only location) """ msg = {} try: saml_request = get['SAMLRequest'] except KeyError: return None if saml_request: xml = decode_base64_and_inflate(saml_request) logger.info('logout request: %s' % xml) request = samlp.logout_request_from_string(xml) logger.debug(request) if request.session_index[0].text == session_index: status = samlp.STATUS_SUCCESS else: status = samlp.STATUS_REQUEST_DENIED response, destination = self .make_logout_response( request.issuer.text, request.id, status) logger.info("RESPONSE: {0:>s}".format(response)) if 'RelayState' in get: rstate = get['RelayState'] else: rstate = "" msg = http_redirect_message(str(response), destination, rstate, 'SAMLResponse') return msg
def test_login_one_idp(self): # monkey patch SAML configuration settings.SAML_CONFIG = conf.create_conf( sp_host='sp.example.com', idp_hosts=['idp.example.com'], metadata_file='remote_metadata_one_idp.xml', ) response = self.client.get(reverse('saml2_login')) self.assertEqual(response.status_code, 302) location = response['Location'] url = urlparse(location) self.assertEqual(url.hostname, 'idp.example.com') self.assertEqual(url.path, '/simplesaml/saml2/idp/SSOService.php') params = parse_qs(url.query) self.assertIn('SAMLRequest', params) self.assertIn('RelayState', params) saml_request = params['SAMLRequest'][0] if 'AuthnRequest xmlns' not in decode_base64_and_inflate( saml_request).decode('utf-8'): raise Exception('Not a valid AuthnRequest') # if we set a next arg in the login view, it is preserverd # in the RelayState argument next = '/another-view/' response = self.client.get(reverse('saml2_login'), {'next': next}) self.assertEqual(response.status_code, 302) location = response['Location'] url = urlparse(location) self.assertEqual(url.hostname, 'idp.example.com') self.assertEqual(url.path, '/simplesaml/saml2/idp/SSOService.php') params = parse_qs(url.query) self.assertIn('SAMLRequest', params) self.assertIn('RelayState', params) self.assertEqual(params['RelayState'][0], next)
def unravel(txt, binding, msgtype="response"): """ Will unpack the received text. Depending on the context the original response may have been transformed before transmission. :param txt: :param binding: :param msgtype: :return: """ if binding not in [BINDING_HTTP_REDIRECT, BINDING_HTTP_POST, None]: raise UnknownBinding("Don't know how to handle '%s'" % binding) else: try: if binding == BINDING_HTTP_REDIRECT: xmlstr = decode_base64_and_inflate(txt) elif binding == BINDING_HTTP_POST: xmlstr = base64.b64decode(txt) else: xmlstr = txt except Exception: raise UnravelError("Unravelling binding '%s' failed" % binding) return xmlstr
def test_authenticate_no_args(self): (sid, request) = self.client.authenticate(relay_state="http://www.example.com/relay_state") assert sid is not None assert request[0] == "Location" o = urlparse(request[1]) qdict = parse_qs(o.query) assert _leq(qdict.keys(), ['SAMLRequest', 'RelayState']) saml_request = decode_base64_and_inflate(qdict["SAMLRequest"][0]) assert qdict["RelayState"][0] == "http://www.example.com/relay_state" print saml_request authnreq = samlp.authn_request_from_string(saml_request) print authnreq.keyswv() assert authnreq.id == sid assert authnreq.destination == "http://localhost:8088/sso" assert authnreq.assertion_consumer_service_url == "http://lingon.catalogix.se:8087/" assert authnreq.provider_name == "urn:mace:example.com:saml:roland:sp" assert authnreq.protocol_binding == BINDING_HTTP_POST name_id_policy = authnreq.name_id_policy assert name_id_policy.allow_create == "true" assert name_id_policy.format == "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" issuer = authnreq.issuer assert issuer.text == "urn:mace:example.com:saml:roland:sp"
def create_logout_response(subject_id, destination, issuer_entity_id, req_entity_id, sign=True): config = IdPConfig() config.load(idp_config) idp_server = Server(config=config) # construct a request logout_request = create_logout_request( subject_id=subject_id, destination=destination, issuer_entity_id=issuer_entity_id, req_entity_id=req_entity_id) #idp_server.ident = Identifier(auth.AuthDictCache(dict(), '_ident')) resp, headers, message = idp_server.logout_response( request=logout_request, bindings=[BINDING_HTTP_REDIRECT], sign=sign) location = dict(headers).get('Location') url = urlparse.urlparse(location) params = urlparse.parse_qs(url.query) logout_response_xml = decode_base64_and_inflate(params['SAMLResponse'][0]) response = samlp.logout_response_from_string(logout_response_xml) return response.in_response_to, logout_response_xml
def http_redirect_logout_request_check_session_index( self, get, session_index, log=None): """ Deal with a LogoutRequest received through HTTP redirect :param get: The request as a dictionary :param subject_id: the id of the current logged user :return: a tuple with a list of header tuples (presently only location) """ msg = {} try: saml_request = get['SAMLRequest'] except KeyError: return None if saml_request: xml = decode_base64_and_inflate(saml_request) logger.info('logout request: %s' % xml) request = samlp.logout_request_from_string(xml) logger.debug(request) if request.session_index[0].text == session_index: status = samlp.STATUS_SUCCESS else: status = samlp.STATUS_REQUEST_DENIED response, destination = self.make_logout_response( request.issuer.text, request.id, status) logger.info("RESPONSE: {0:>s}".format(response)) if 'RelayState' in get: rstate = get['RelayState'] else: rstate = "" msg = http_redirect_message(str(response), destination, rstate, 'SAMLResponse') return msg
def create_logout_response(subject_id, destination, issuer_entity_id, req_entity_id, sign=True): config = IdPConfig() config.load(idp_config) idp_server = Server(config=config) # construct a request logout_request = create_logout_request(subject_id=subject_id, destination=destination, issuer_entity_id=issuer_entity_id, req_entity_id=req_entity_id) #idp_server.ident = Identifier(auth.AuthDictCache(dict(), '_ident')) resp, headers, message = idp_server.logout_response( request=logout_request, bindings=[BINDING_HTTP_REDIRECT], sign=sign) location = dict(headers).get('Location') url = urlparse.urlparse(location) params = urlparse.parse_qs(url.query) logout_response_xml = decode_base64_and_inflate(params['SAMLResponse'][0]) response = samlp.logout_response_from_string(logout_response_xml) return response.in_response_to, logout_response_xml
def test_authenticate_no_args(self): (sid, request) = self.client.authenticate( relay_state="http://www.example.com/relay_state") assert sid is not None assert request[0] == "Location" o = urlparse(request[1]) qdict = parse_qs(o.query) assert _leq(qdict.keys(), ['SAMLRequest', 'RelayState']) saml_request = decode_base64_and_inflate(qdict["SAMLRequest"][0]) assert qdict["RelayState"][0] == "http://www.example.com/relay_state" print saml_request authnreq = samlp.authn_request_from_string(saml_request) print authnreq.keyswv() assert authnreq.id == sid assert authnreq.destination == "http://localhost:8088/sso" assert authnreq.assertion_consumer_service_url == "http://lingon.catalogix.se:8087/" assert authnreq.provider_name == "urn:mace:example.com:saml:roland:sp" assert authnreq.protocol_binding == BINDING_HTTP_POST name_id_policy = authnreq.name_id_policy assert name_id_policy.allow_create == "true" assert name_id_policy.format == "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" issuer = authnreq.issuer assert issuer.text == "urn:mace:example.com:saml:roland:sp"
def unravel(self, txt, binding, msgtype="response"): #logger.debug("unravel '%s'" % txt) if binding not in [BINDING_HTTP_REDIRECT, BINDING_HTTP_POST, BINDING_SOAP, BINDING_URI, BINDING_HTTP_ARTIFACT, None]: raise ValueError("Don't know how to handle '%s'" % binding) else: try: if binding == BINDING_HTTP_REDIRECT: xmlstr = decode_base64_and_inflate(txt) elif binding == BINDING_HTTP_POST: xmlstr = base64.b64decode(txt) elif binding == BINDING_SOAP: func = getattr(soap, "parse_soap_enveloped_saml_%s" % msgtype) xmlstr = func(txt) elif binding == BINDING_HTTP_ARTIFACT: xmlstr = base64.b64decode(txt) else: xmlstr = txt except Exception: raise UnravelError() return xmlstr
def test_logout_response_http_redirect(self, unravel, verified): # See: https://github.com/italia/spid-testenv2/issues/88 with patch('testenv.server.IdpServer._sp_single_logout_service', return_value=_sp_single_logout_service( self.idp_server.server, 'https://spid.test:8000', BINDING_HTTP_REDIRECT)) as mocked: response = self.test_client.get( '/slo-test?SAMLRequest=b64encodedrequest&SigAlg={}&Signature=sign' .format(quote(SIG_RSA_SHA256)), follow_redirects=False) self.assertEqual(response.status_code, 302) response_location = response.headers.get('Location') url = urlparse(response_location) query = parse_qs(url.query) self.assertIn('Signature', query) saml_response = query.get('SAMLResponse')[0] response = decode_base64_and_inflate(saml_response) xml = ET.fromstring(response) signatures = xml.findall( './/{http://www.w3.org/2000/09/xmldsig#}Signature') self.assertEqual(0, len(signatures)) self.assertEqual(len(self.idp_server.ticket), 0) self.assertEqual(len(self.idp_server.responses), 0)
def _parse_authn_request(self, enc_request): """ enc_request is an encrypted and base64 encoded request """ xmlstr = decode_base64_and_inflate(urllib.unquote(enc_request)) return authn_request_from_string(xmlstr)
def test_Saml_logout(self): not_on_or_after = time.time()+3600 identity = {'id-1': { 'https://sso.example.com/idp/metadata': ( not_on_or_after, { 'authn_info': [], 'name_id': 'id-1', 'not_on_or_after': not_on_or_after, 'came_from': '/next', 'ava': {'uid': ['123456']} } ) }} # modifying config in this test, make copy so as not to effect # following tests. tmp_sp_config = copy.deepcopy(sp_config) with self.app.test_request_context('/', method='GET'): sp = auth.Saml(tmp_sp_config) # first need to be logged in, let's pretend session['_saml_identity'] = identity session['_saml_subject_id'] = 'id-1' resp = sp.logout(next_url='/next') self.assertEqual(resp.status_code, 302) self.assert_("SAMLRequest" in resp.headers['Location']) url = urlparse.urlparse(resp.headers['Location']) self.assertEqual(url.hostname, 'sso.example.com') self.assertEqual(url.path, '/idp/slo') params = urlparse.parse_qs(url.query) self.assert_('SAMLRequest' in params) logout = samlp.logout_request_from_string( decode_base64_and_inflate(params['SAMLRequest'][0])) self.assertEqual(logout.destination, 'https://sso.example.com/idp/slo') self.assertEqual(logout.name_id.text, 'id-1') self.assertIsNotNone(logout.signature) # check the caches still contain data self.assertEqual(session['_saml_identity'], identity) self.assertEqual(session['_saml_subject_id'], 'id-1') # verify state cache self.assert_(logout.id in session['_saml_state']) self.assertEqual(session['_saml_state'][logout.id]['entity_id'], 'https://sso.example.com/idp/metadata') self.assertEqual(session['_saml_state'][logout.id]['operation'], 'SLO') self.assertEqual(session['_saml_state'][logout.id]['subject_id'], 'id-1') self.assertEqual(session['_saml_state'][logout.id]['return_to'], '/next') self.assertTrue(session['_saml_state'][logout.id]['sign']) # test unsigned logout request with self.app.test_request_context('/', method='GET'): tmp_sp_config['key_file'] = None tmp_sp_config['service']['sp']['logout_requests_signed'] = 'false' sp = auth.Saml(tmp_sp_config) # first need to be logged in, let's pretend session['_saml_identity'] = identity session['_saml_subject_id'] = 'id-1' resp = sp.logout(next_url='/next') self.assertEqual(resp.status_code, 302) self.assert_("SAMLRequest" in resp.headers['Location']) url = urlparse.urlparse(resp.headers['Location']) params = urlparse.parse_qs(url.query) self.assert_('SAMLRequest' in params) logout = samlp.logout_request_from_string( decode_base64_and_inflate(params['SAMLRequest'][0])) self.assertIsNone(logout.signature) # verify state cache shows signing off self.assertFalse(session['_saml_state'][logout.id]['sign'])
def _parse_logout_response(self, enc_response): xmlstr = decode_base64_and_inflate(urllib.unquote(enc_response)) return logout_response_from_string(xmlstr)
def test_Saml_logout(self): not_on_or_after = time.time() + 3600 identity = { 'id-1': { 'https://sso.example.com/idp/metadata': (not_on_or_after, { 'authn_info': [], 'name_id': 'id-1', 'not_on_or_after': not_on_or_after, 'came_from': '/next', 'ava': { 'uid': ['123456'] } }) } } # modifying config in this test, make copy so as not to effect # following tests. tmp_sp_config = copy.deepcopy(sp_config) with self.app.test_request_context('/', method='GET'): sp = auth.Saml(tmp_sp_config) # first need to be logged in, let's pretend session['_saml_identity'] = identity session['_saml_subject_id'] = 'id-1' resp = sp.logout(next_url='/next') self.assertEqual(resp.status_code, 302) self.assert_("SAMLRequest" in resp.headers['Location']) url = urlparse.urlparse(resp.headers['Location']) self.assertEqual(url.hostname, 'sso.example.com') self.assertEqual(url.path, '/idp/slo') params = urlparse.parse_qs(url.query) self.assert_('SAMLRequest' in params) logout = samlp.logout_request_from_string( decode_base64_and_inflate(params['SAMLRequest'][0])) self.assertEqual(logout.destination, 'https://sso.example.com/idp/slo') self.assertEqual(logout.name_id.text, 'id-1') self.assertIsNotNone(logout.signature) # check the caches still contain data self.assertEqual(session['_saml_identity'], identity) self.assertEqual(session['_saml_subject_id'], 'id-1') # verify state cache self.assert_(logout.id in session['_saml_state']) self.assertEqual(session['_saml_state'][logout.id]['entity_id'], 'https://sso.example.com/idp/metadata') self.assertEqual(session['_saml_state'][logout.id]['operation'], 'SLO') self.assertEqual(session['_saml_state'][logout.id]['subject_id'], 'id-1') self.assertEqual(session['_saml_state'][logout.id]['return_to'], '/next') self.assertTrue(session['_saml_state'][logout.id]['sign']) # test unsigned logout request with self.app.test_request_context('/', method='GET'): tmp_sp_config['key_file'] = None tmp_sp_config['service']['sp']['logout_requests_signed'] = 'false' sp = auth.Saml(tmp_sp_config) # first need to be logged in, let's pretend session['_saml_identity'] = identity session['_saml_subject_id'] = 'id-1' resp = sp.logout(next_url='/next') self.assertEqual(resp.status_code, 302) self.assert_("SAMLRequest" in resp.headers['Location']) url = urlparse.urlparse(resp.headers['Location']) params = urlparse.parse_qs(url.query) self.assert_('SAMLRequest' in params) logout = samlp.logout_request_from_string( decode_base64_and_inflate(params['SAMLRequest'][0])) self.assertIsNone(logout.signature) # verify state cache shows signing off self.assertFalse(session['_saml_state'][logout.id]['sign'])