def do_assertion_id_request(self, assertion_ids, entity_id, consent=None, extensions=None, sign=False): srvs = self.metadata.assertion_id_request_service( entity_id, BINDING_SOAP) if not srvs: raise NoServiceDefined("%s: %s" % (entity_id, "assertion_id_request_service")) if isinstance(assertion_ids, six.string_types): assertion_ids = [assertion_ids] _id_refs = [AssertionIDRef(_id) for _id in assertion_ids] for destination in locations(srvs): res = self._use_soap(destination, "assertion_id_request", assertion_id_refs=_id_refs, consent=consent, extensions=extensions, sign=sign) if res: return res return None
def test_switch_1(): mds = MetadataStore(ATTRCONV, sec_config, disable_ssl_certificate_validation=True) mds.imp(METADATACONF["5"]) assert len(mds.keys()) > 160 idps = mds.with_descriptor("idpsso") print(idps.keys()) idpsso = mds.single_sign_on_service( 'https://aai-demo-idp.switch.ch/idp/shibboleth') assert len(idpsso) == 1 print(idpsso) assert list(locations(idpsso)) == [ 'https://aai-demo-idp.switch.ch/idp/profile/SAML2/Redirect/SSO' ] assert len(idps) > 30 aas = mds.with_descriptor("attribute_authority") print(aas.keys()) aad = aas['https://aai-demo-idp.switch.ch/idp/shibboleth'] print(aad.keys()) assert len(aad["attribute_authority_descriptor"]) == 1 assert len(aad["idpsso_descriptor"]) == 1 sps = mds.with_descriptor("spsso") dual = [eid for eid, ent in idps.items() if eid in sps] print(len(dual)) assert len(dual) == 0
def do_authz_decision_query(self, entity_id, action, subject_id, nameid_format, evidence=None, resource=None, sp_name_qualifier=None, name_qualifier=None, consent=None, extensions=None, sign=False): subject = saml.Subject( name_id=saml.NameID(text=subject_id, format=nameid_format, sp_name_qualifier=sp_name_qualifier, name_qualifier=name_qualifier)) srvs = self.metadata.authz_service(entity_id, BINDING_SOAP) for dest in locations(srvs): resp = self._use_soap(dest, "authz_decision_query", action=action, evidence=evidence, resource=resource, subject=subject) if resp: return resp return None
def test_swami_1(): UMU_IDP = 'https://idp.umu.se/saml2/idp/metadata.php' mds = MetadataStore(ATTRCONV, sec_config, disable_ssl_certificate_validation=True) mds.imp(METADATACONF["1"]) assert len(mds) == 1 # One source idps = mds.with_descriptor("idpsso") assert idps.keys() idpsso = mds.single_sign_on_service(UMU_IDP) assert len(idpsso) == 1 assert list(locations(idpsso)) == [ 'https://idp.umu.se/saml2/idp/SSOService.php' ] _name = name(mds[UMU_IDP]) assert _name == u'UmeƄ University (SAML2)' certs = mds.certs(UMU_IDP, "idpsso", "signing") assert len(certs) == 1 sps = mds.with_descriptor("spsso") assert len(sps) == 108 wants = mds.attribute_requirement('https://connect8.sunet.se/shibboleth') lnamn = [d_to_local_name(mds.attrc, attr) for attr in wants["optional"]] assert _eq(lnamn, ['eduPersonPrincipalName', 'mail', 'givenName', 'sn', 'eduPersonScopedAffiliation']) wants = mds.attribute_requirement('https://beta.lobber.se/shibboleth') assert wants["required"] == [] lnamn = [d_to_local_name(mds.attrc, attr) for attr in wants["optional"]] assert _eq(lnamn, ['eduPersonPrincipalName', 'mail', 'givenName', 'sn', 'eduPersonScopedAffiliation', 'eduPersonEntitlement'])
def do_authn_query(self, entity_id, consent=None, extensions=None, sign=False): srvs = self.metadata.authn_request_service(entity_id, BINDING_SOAP) for destination in locations(srvs): resp = self._use_soap(destination, "authn_query", consent=consent, extensions=extensions, sign=sign) if resp: return resp return None
def _sso_location(self, entityid=None, binding=BINDING_HTTP_REDIRECT): if entityid: # verify that it's in the metadata srvs = self.metadata.single_sign_on_service(entityid, binding) if srvs: return next(locations(srvs), None) else: logger.info("_sso_location: %s, %s", entityid, binding) raise IdpUnspecified("No IdP to send to given the premises") # get the idp location from the metadata. If there is more than one # IdP in the configuration raise exception eids = self.metadata.with_descriptor("idpsso") if len(eids) > 1: raise IdpUnspecified("Too many IdPs to choose from: %s" % eids) try: srvs = self.metadata.single_sign_on_service( list(eids.keys())[0], binding) return next(locations(srvs), None) except IndexError: raise IdpUnspecified("No IdP to send to given the premises")
def test_incommon_1(): mds = MetadataStore(ATTRCONV, sec_config, disable_ssl_certificate_validation=True) mds.imp(METADATACONF["2"]) print(mds.entities()) assert mds.entities() > 1700 idps = mds.with_descriptor("idpsso") print(idps.keys()) assert len(idps) > 300 # ~ 18% try: _ = mds.single_sign_on_service('urn:mace:incommon:uiuc.edu') except UnknownPrincipal: pass idpsso = mds.single_sign_on_service('urn:mace:incommon:alaska.edu') assert len(idpsso) == 1 print(idpsso) assert list(locations(idpsso)) == [ 'https://idp.alaska.edu/idp/profile/SAML2/Redirect/SSO' ] sps = mds.with_descriptor("spsso") acs_sp = [] for nam, desc in sps.items(): if "attribute_consuming_service" in desc: acs_sp.append(nam) assert len(acs_sp) == 0 # Look for attribute authorities aas = mds.with_descriptor("attribute_authority") print(aas.keys()) assert len(aas) == 180
def do_attribute_query( self, entityid, subject_id, attribute=None, sp_name_qualifier=None, name_qualifier=None, nameid_format=None, real_id=None, consent=None, extensions=None, sign=False, binding=BINDING_SOAP, nsprefix=None, sign_alg=None, digest_alg=None, ): """ Does a attribute request to an attribute authority, this is by default done over SOAP. :param entityid: To whom the query should be sent :param subject_id: The identifier of the subject :param attribute: A dictionary of attributes and values that is asked for :param sp_name_qualifier: The unique identifier of the service provider or affiliation of providers for whom the identifier was generated. :param name_qualifier: The unique identifier of the identity provider that generated the identifier. :param nameid_format: The format of the name ID :param real_id: The identifier which is the key to this entity in the identity database :param binding: Which binding to use :param nsprefix: Namespace prefixes preferred before those automatically produced. :return: The attributes returned if BINDING_SOAP was used. HTTP args if BINDING_HTT_POST was used. """ if real_id: response_args = {"real_id": real_id} else: response_args = {} if not binding: binding, destination = self.pick_binding( "attribute_service", None, "attribute_authority", entity_id=entityid ) else: srvs = self.metadata.attribute_service(entityid, binding) if srvs is []: raise SAMLError("No attribute service support at entity") destination = next(locations(srvs), None) if binding == BINDING_SOAP: return self._use_soap( destination, "attribute_query", consent=consent, extensions=extensions, sign=sign, sign_alg=sign_alg, digest_alg=digest_alg, subject_id=subject_id, attribute=attribute, sp_name_qualifier=sp_name_qualifier, name_qualifier=name_qualifier, format=nameid_format, response_args=response_args, ) elif binding == BINDING_HTTP_POST: mid = sid() query = self.create_attribute_query( destination, name_id=subject_id, attribute=attribute, message_id=mid, consent=consent, extensions=extensions, sign=sign, sign_alg=sign_alg, digest_alg=digest_alg, nsprefix=nsprefix, ) self.state[query.id] = { "entity_id": entityid, "operation": "AttributeQuery", "subject_id": subject_id, "sign": sign, } relay_state = self._relay_state(query.id) return self.apply_binding( binding, str(query), destination, relay_state, sign=False, sigalg=sign_alg, ) else: raise SAMLError("Unsupported binding")
def do_logout( self, name_id, entity_ids, reason, expire, sign=None, expected_binding=None, sign_alg=None, digest_alg=None, **kwargs, ): """ :param name_id: Identifier of the Subject (a NameID instance) :param entity_ids: List of entity ids for the IdPs that have provided information concerning the subject :param reason: The reason for doing the logout :param expire: Try to logout before this time. :param sign: Whether to sign the request or not :param expected_binding: Specify the expected binding then not try it all :param kwargs: Extra key word arguments. :return: """ # check time if not not_on_or_after(expire): # I've run out of time # Do the local logout anyway self.local_logout(name_id) return 0, "504 Gateway Timeout", [], [] not_done = entity_ids[:] responses = {} for entity_id in entity_ids: logger.debug("Logout from '%s'", entity_id) # for all where I can use the SOAP binding, do those first for binding in [BINDING_SOAP, BINDING_HTTP_POST, BINDING_HTTP_REDIRECT]: if expected_binding and binding != expected_binding: continue try: srvs = self.metadata.single_logout_service( entity_id, binding, "idpsso" ) except: srvs = None if not srvs: logger.debug("No SLO '%s' service", binding) continue destination = next(locations(srvs), None) logger.info("destination to provider: %s", destination) try: session_info = self.users.get_info_from( name_id, entity_id, False ) session_indexes = [session_info['session_index']] except KeyError: session_indexes = None sign_post = False if binding == BINDING_HTTP_REDIRECT else sign sign_redirect = False if binding == BINDING_HTTP_POST and sign else sign req_id, request = self.create_logout_request( destination, entity_id, name_id=name_id, reason=reason, expire=expire, session_indexes=session_indexes, sign=sign_post, sign_alg=sign_alg, digest_alg=digest_alg, ) relay_state = self._relay_state(req_id) http_info = self.apply_binding( binding, str(request), destination, relay_state, sign=sign_redirect, sigalg=sign_alg, ) if binding == BINDING_SOAP: response = self.send(**http_info) if response and response.status_code == 200: not_done.remove(entity_id) response = response.text logger.info("Response: %s", response) res = self.parse_logout_request_response(response, binding) responses[entity_id] = res else: logger.info("NOT OK response from %s", destination) else: self.state[req_id] = { "entity_id": entity_id, "operation": "SLO", "entity_ids": entity_ids, "name_id": code(name_id), "reason": reason, "not_on_or_after": expire, "sign": sign, } responses[entity_id] = (binding, http_info) not_done.remove(entity_id) # only try one binding break if not_done: # upstream should try later raise LogoutError("%s" % (entity_ids,)) return responses
def do_logout( self, name_id, entity_ids, reason, expire, sign=None, expected_binding=None, sign_alg=None, digest_alg=None, **kwargs, ): """ :param name_id: Identifier of the Subject (a NameID instance) :param entity_ids: List of entity ids for the IdPs that have provided information concerning the subject :param reason: The reason for doing the logout :param expire: Try to logout before this time. :param sign: Whether to sign the request or not :param expected_binding: Specify the expected binding then not try it all :param kwargs: Extra key word arguments. :return: """ # check time if not not_on_or_after(expire): # I've run out of time # Do the local logout anyway self.local_logout(name_id) return 0, "504 Gateway Timeout", [], [] not_done = entity_ids[:] responses = {} bindings_slo_preferred = self.config.preferred_binding[ "single_logout_service"] for entity_id in entity_ids: logger.debug("Logout from '%s'", entity_id) bindings_slo_supported = self.metadata.single_logout_service( entity_id=entity_id, typ="idpsso") bindings_slo_preferred_and_supported = ( binding for binding in bindings_slo_preferred if binding in bindings_slo_supported) bindings_slo_choices = filter(lambda x: x, ( expected_binding, *bindings_slo_preferred_and_supported, *bindings_slo_supported, )) binding = next(bindings_slo_choices, None) if not binding: logger.info({ "message": "Entity does not support SLO", "entity": entity_id, }) continue service_info = bindings_slo_supported[binding] service_location = next(locations(service_info), None) if not service_location: logger.info({ "message": "Entity SLO service does not have a location", "entity": entity_id, "service_location": service_location, }) continue session_info = self.users.get_info_from(name_id, entity_id, False) session_index = session_info.get('session_index') session_indexes = [session_index] if session_index else None sign = sign if sign is not None else self.logout_requests_signed sign_post = sign and (binding == BINDING_HTTP_POST or binding == BINDING_SOAP) sign_redirect = sign and binding == BINDING_HTTP_REDIRECT log_report = { "message": "Invoking SLO on entity", "entity": entity_id, "binding": binding, "location": service_location, "session_indexes": session_indexes, "sign": sign, } logger.info(log_report) req_id, request = self.create_logout_request( service_location, entity_id, name_id=name_id, reason=reason, expire=expire, session_indexes=session_indexes, sign=sign_post, sign_alg=sign_alg, digest_alg=digest_alg, ) relay_state = self._relay_state(req_id) http_info = self.apply_binding( binding, str(request), service_location, relay_state, sign=sign_redirect, sigalg=sign_alg, ) if binding == BINDING_SOAP: response = self.send(**http_info) if response and response.status_code == 200: not_done.remove(entity_id) response_text = response.text log_report_response = { **log_report, "message": "Response from SLO service", "response_text": response_text, } logger.debug(log_report_response) res = self.parse_logout_request_response( response_text, binding) responses[entity_id] = res else: log_report_response = { **log_report, "message": "Bad status_code response from SLO service", "status_code": (response and response.status_code), } logger.info(log_report_response) else: self.state[req_id] = { "entity_id": entity_id, "operation": "SLO", "entity_ids": entity_ids, "name_id": code(name_id), "reason": reason, "not_on_or_after": expire, "sign": sign, } responses[entity_id] = (binding, http_info) not_done.remove(entity_id) if not_done: # upstream should try later raise LogoutError("%s" % (entity_ids, )) return responses
def test_metadata(): conf = config.Config() conf.load_file("idp_conf_mdb") umu_idp = 'https://idp.umu.se/saml2/idp/metadata.php' # Set up a Metadata store mds = MetadataStore(ATTRCONV, conf, disable_ssl_certificate_validation=True) # Import metadata from local file. mds.imp([{ "class": "saml2.mdstore.MetaDataFile", "metadata": [(full_path("swamid-2.0.xml"), )] }]) assert len(mds) == 1 # One source try: export_mdstore_to_mongo_db(mds, "metadata", "test") except ConnectionFailure: pass else: mdmdb = MetadataMDB(ATTRCONV, "metadata", "test") # replace all metadata instances with this one mds.metadata = {"mongo_db": mdmdb} idps = mds.with_descriptor("idpsso") assert idps.keys() idpsso = mds.single_sign_on_service(umu_idp) assert len(idpsso) == 1 assert list(locations(idpsso)) == [ 'https://idp.umu.se/saml2/idp/SSOService.php' ] _name = name(mds[umu_idp]) assert _name == u'Ume\xe5 University' certs = mds.certs(umu_idp, "idpsso", "signing") assert len(certs) == 1 sps = mds.with_descriptor("spsso") assert len(sps) == 417 wants = mds.attribute_requirement( 'https://connect.sunet.se/shibboleth') assert wants["optional"] == [] lnamn = [ d_to_local_name(mds.attrc, attr) for attr in wants["required"] ] assert _eq(lnamn, [ 'eduPersonPrincipalName', 'mail', 'givenName', 'sn', 'eduPersonScopedAffiliation', 'eduPersonAffiliation' ]) wants = mds.attribute_requirement( "https://gidp.geant.net/sp/module.php/saml/sp/metadata.php/default-sp" ) # Optional lnamn = [ d_to_local_name(mds.attrc, attr) for attr in wants["optional"] ] assert _eq(lnamn, [ 'displayName', 'commonName', 'schacHomeOrganization', 'eduPersonAffiliation', 'schacHomeOrganizationType' ]) # Required lnamn = [ d_to_local_name(mds.attrc, attr) for attr in wants["required"] ] assert _eq( lnamn, ['eduPersonTargetedID', 'mail', 'eduPersonScopedAffiliation'])