def imp(self, spec): # This serves as a backwards compatibility if type(spec) is dict: # Old style... for key, vals in spec.items(): for val in vals: if isinstance(val, dict): if not self.check_validity: val["check_validity"] = False self.load(key, **val) else: self.load(key, val) else: for item in spec: try: key = item['class'] except (KeyError, AttributeError): raise SAMLError("Misconfiguration in metadata %s" % item) mod, clas = key.rsplit('.', 1) try: mod = importlib.import_module(mod) MDloader = getattr(mod, clas) except (ImportError, AttributeError): raise SAMLError("Unknown metadata loader %s" % key) # Separately handle MDExtern if MDloader == MetaDataExtern: kwargs = {'http': self.http, 'security': self.security} else: kwargs = {} if self.filter: kwargs["filter"] = self.filter for key in item['metadata']: # Separately handle MetaDataFile and directory if MDloader == MetaDataFile and os.path.isdir(key[0]): files = [ f for f in os.listdir(key[0]) if isfile(join(key[0], f)) ] for fil in files: _fil = join(key[0], fil) _md = MetaDataFile(self.attrc, _fil) _md.load() self.metadata[_fil] = _md if _md.to_old: self.to_old[_fil] = _md.to_old return if len(key) == 2: kwargs["cert"] = key[1] _md = MDloader(self.attrc, key[0], **kwargs) _md.load() self.metadata[key[0]] = _md if _md.to_old: self.to_old[key[0]] = _md.to_old
def operation(self, url, idp_entity_id, op, **opargs): """ This is the method that should be used by someone that wants to authenticate using SAML ECP :param url: The page that access is sought for :param idp_entity_id: The entity ID of the IdP that should be used for authentication :param op: Which HTTP operation (GET/POST/PUT/DELETE) :param opargs: Arguments to the HTTP call :return: The page """ sp_url = self._sp # ******************************************** # Phase 1 - First conversation with the SP # ******************************************** # headers needed to indicate to the SP that I'm ECP enabled opargs["headers"] = self.add_paos_headers(opargs["headers"]) response = self.send(sp_url, op, **opargs) logger.debug("[Op] SP response: %s" % response) print(response.text) if response.status_code != 200: raise SAMLError("Request to SP failed: %s" % response.text) # The response might be a AuthnRequest instance in a SOAP envelope # body. If so it's the start of the ECP conversation # Two SOAP header blocks; paos:Request and ecp:Request # may also contain a ecp:RelayState SOAP header block # If channel-binding was part of the PAOS header any number of # <cb:ChannelBindings> header blocks may also be present # if 'holder-of-key' option then one or more <ecp:SubjectConfirmation> # header blocks may also be present try: respdict = self.parse_soap_message(response.text) self.ecp_conversation(respdict, idp_entity_id) # should by now be authenticated so this should go smoothly response = self.send(url, op, **opargs) except (soap.XmlParseError, AssertionError, KeyError): raise if response.status_code >= 400: raise SAMLError("Error performing operation: %s" % (response.text, )) return response
def __init__(self, attrc, url=None, security=None, cert=None, http=None, **kwargs): """ :params attrc: :params url: Location of the metadata :params security: SecurityContext() :params cert: CertificMDloaderate used to sign the metadata :params http: """ super(MetaDataExtern, self).__init__(attrc, **kwargs) if not url: raise SAMLError('URL not specified.') else: self.url = url # No cert is only an error if the metadata is unsigned self.cert = cert self.security = security self.http = http
def parse_sp_ecp_response(respdict): if respdict is None: raise SAMLError("Unexpected reply from the SP") logger.debug("[P1] SP response dict: %s", respdict) # AuthnRequest in the body or not authn_request = respdict["body"] assert authn_request.c_tag == "AuthnRequest" # ecp.RelayState among headers _relay_state = None _paos_request = None for item in respdict["header"]: if item.c_tag == "RelayState" and item.c_namespace == ecp.NAMESPACE: _relay_state = item if item.c_tag == "Request" and item.c_namespace == paos.NAMESPACE: _paos_request = item if _paos_request is None: raise BadRequest("Missing request") _rc_url = _paos_request.response_consumer_url return { "authn_request": authn_request, "rc_url": _rc_url, "relay_state": _relay_state }
def __call__(self, **kwargs): if not self.methods: raise SAMLError("No authentication methods defined") elif len(self.methods) == 1: return self.methods[0] else: pass # TODO
def parse_cookie(name, seed, kaka): """Parses and verifies a cookie value :param seed: A seed used for the HMAC signature :param kaka: The cookie :return: A tuple consisting of (payload, timestamp) """ if not kaka: return None cookie_obj = SimpleCookie(kaka) morsel = cookie_obj.get(name) if morsel: parts = morsel.value.split("|") if len(parts) != 3: return None # verify the cookie signature sig = cookie_signature(seed, parts[0], parts[1]) if sig != parts[2]: raise SAMLError("Invalid cookie signature") try: return parts[0].strip(), parts[1] except KeyError: return None else: return None
def load(self, *args, **kwargs): if self.filter: _args = {"filter": self.filter} else: _args = {} typ = args[0] if typ == "local": key = args[1] # if library read every file in the library if os.path.isdir(key): files = [f for f in os.listdir(key) if isfile(join(key, f))] for fil in files: _fil = join(key, fil) _md = MetaDataFile(self.attrc, _fil, **_args) _md.load() self.metadata[_fil] = _md return else: # else it's just a plain old file so read it _md = MetaDataFile(self.attrc, key, **_args) elif typ == "inline": self.ii += 1 key = self.ii kwargs.update(_args) _md = InMemoryMetaData(self.attrc, args[1]) elif typ == "remote": if "url" not in kwargs: raise ValueError( "Remote metadata must be structured as a dict containing the key 'url'" ) key = kwargs["url"] for _key in ["node_name", "check_validity"]: try: _args[_key] = kwargs[_key] except KeyError: pass if "cert" not in kwargs: kwargs["cert"] = "" _md = MetaDataExtern(self.attrc, kwargs["url"], self.security, kwargs["cert"], self.http, **_args) elif typ == "mdfile": key = args[1] _md = MetaDataMD(self.attrc, args[1], **_args) elif typ == "loader": key = args[1] _md = MetaDataLoader(self.attrc, args[1], **_args) elif typ == "mdq": key = args[1] _md = MetaDataMDX(args[1]) else: raise SAMLError("Unknown metadata type '%s'" % typ) _md.load() self.metadata[key] = _md
def entities_descriptor(eds, valid_for, name, ident, sign, secc, sign_alg=None, digest_alg=None): entities = md.EntitiesDescriptor(entity_descriptor=eds) if valid_for: entities.valid_until = in_a_while(hours=valid_for) if name: entities.name = name if ident: entities.id = ident if sign: if not ident: ident = sid() if not secc.key_file: raise SAMLError("If you want to do signing you should define " + "a key to sign with") if not secc.my_cert: raise SAMLError("If you want to do signing you should define " + "where your public key are") entities.signature = pre_signature_part(ident, secc.my_cert, 1, sign_alg=sign_alg, digest_alg=digest_alg) entities.id = ident xmldoc = secc.sign_statement("%s" % entities, class_name(entities)) entities = md.entities_descriptor_from_string(xmldoc) else: xmldoc = None return entities, xmldoc
def get_nameid(self, userid, nformat, sp_name_qualifier, name_qualifier): _id = self.create_id(nformat, name_qualifier, sp_name_qualifier) if nformat == NAMEID_FORMAT_EMAILADDRESS: if not self.domain: raise SAMLError("Can't issue email nameids, unknown domain") _id = "%s@%s" % (_id, self.domain) # if nformat == NAMEID_FORMAT_PERSISTENT: # _id = userid nameid = NameID(format=nformat, sp_name_qualifier=sp_name_qualifier, name_qualifier=name_qualifier, text=_id) self.store(userid, nameid) return nameid
def ecp_conversation(self, respdict, idp_entity_id=None): """ :param respdict: :param idp_entity_id: :return: """ args = self.parse_sp_ecp_response(respdict) # ********************** # Phase 2 - talk to the IdP # ********************** idp_response = self.phase2(idp_entity_id=idp_entity_id, **args) # ********************************** # Phase 3 - back to the SP # ********************************** ht_args = self.use_soap(idp_response, args["rc_url"], [args["relay_state"]]) ht_args["headers"][0] = ('Content-Type', MIME_PAOS) logger.debug("[P3] Post to SP: %s", ht_args["data"]) # POST the package from the IdP to the SP response = self.send(**ht_args) if response.status_code == 302: # ignore where the SP is redirecting us to and go for the # url I started off with. pass else: raise SAMLError("Error POSTing package to SP: %s" % response.text) logger.debug("[P3] SP response: %s", response.text) self.done_ecp = True logger.debug("Done ECP") return None
def nim_args(self, local_policy=None, sp_name_qualifier="", name_id_policy=None, name_qualifier=""): """ :param local_policy: :param sp_name_qualifier: :param name_id_policy: :param name_qualifier: :return: """ logger.debug("local_policy: %s, name_id_policy: %s", local_policy, name_id_policy) if name_id_policy and name_id_policy.sp_name_qualifier: sp_name_qualifier = name_id_policy.sp_name_qualifier else: sp_name_qualifier = sp_name_qualifier if name_id_policy and name_id_policy.format: nameid_format = name_id_policy.format elif local_policy: nameid_format = local_policy.get_nameid_format(sp_name_qualifier) else: raise SAMLError("Unknown NameID format") if not name_qualifier: name_qualifier = self.name_qualifier return { "nformat": nameid_format, "sp_name_qualifier": sp_name_qualifier, "name_qualifier": name_qualifier }
def __init__(self, attrc, filename=None, cert=None, **kwargs): super(MetaDataFile, self).__init__(attrc, **kwargs) if not filename: raise SAMLError('No file specified.') self.filename = filename self.cert = cert
def parse_authn_request_response(self, xmlstr, binding, outstanding=None, outstanding_certs=None, conv_info=None): """ Deal with an AuthnResponse :param xmlstr: The reply as a xml string :param binding: Which binding that was used for the transport :param outstanding: A dictionary with session IDs as keys and the original web request from the user before redirection as values. :param outstanding_certs: :param conv_info: Information about the conversation. :return: An response.AuthnResponse or None """ if not getattr(self.config, 'entityid', None): raise SAMLError("Missing entity_id specification") if not xmlstr: return None kwargs = { "outstanding_queries": outstanding, "outstanding_certs": outstanding_certs, "allow_unsolicited": self.allow_unsolicited, "want_assertions_signed": self.want_assertions_signed, "want_assertions_or_response_signed": self.want_assertions_or_response_signed, "want_response_signed": self.want_response_signed, "return_addrs": self.service_urls(binding=binding), "entity_id": self.config.entityid, "attribute_converters": self.config.attribute_converters, "allow_unknown_attributes": self.config.allow_unknown_attributes, 'conv_info': conv_info, "valid_destination_regex": self.valid_destination_regex, } try: resp = self._parse_response(xmlstr, AuthnResponse, "assertion_consumer_service", binding, **kwargs) except StatusError as err: logger.error("SAML status error: %s", err) raise except UnravelError: return None except Exception as err: logger.error("XML parse error: %s", err) raise if not isinstance(resp, AuthnResponse): logger.error("Response type not supported: %s", saml2_tophat.class_name(resp)) return None if (resp.assertion and len(resp.response.encrypted_assertion) == 0 and resp.assertion.subject.name_id): self.users.add_information_about_person(resp.session_info()) logger.info("--- ADDED person info ----") return resp
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): """ 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 = destinations(srvs)[0] if binding == BINDING_SOAP: return self._use_soap(destination, "attribute_query", consent=consent, extensions=extensions, sign=sign, 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, subject_id, attribute, mid, consent, extensions, sign, 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, "%s" % query, destination, relay_state, sign=sign) else: raise SAMLError("Unsupported binding")
def phase2(self, authn_request, rc_url, idp_entity_id, headers=None, sign=False, **kwargs): """ Doing the second phase of the ECP conversation, the conversation with the IdP happens. :param authn_request: The AuthenticationRequest :param rc_url: The assertion consumer service url of the SP :param idp_entity_id: The EntityID of the IdP :param headers: Possible extra headers :param sign: If the message should be signed :return: The response from the IdP """ _, destination = self.pick_binding("single_sign_on_service", [BINDING_SOAP], "idpsso", entity_id=idp_entity_id) ht_args = self.apply_binding(BINDING_SOAP, authn_request, destination, sign=sign) if headers: ht_args["headers"].extend(headers) logger.debug("[P2] Sending request: %s", ht_args["data"]) # POST the request to the IdP response = self.send(**ht_args) logger.debug("[P2] Got IdP response: %s", response) if response.status_code != 200: raise SAMLError("Request to IdP failed (%s): %s" % (response.status_code, response.text)) # SAMLP response in a SOAP envelope body, ecp response in headers respdict = self.parse_soap_message(response.text) if respdict is None: raise SAMLError("Unexpected reply from the IdP") logger.debug("[P2] IdP response dict: %s", respdict) idp_response = respdict["body"] assert idp_response.c_tag == "Response" logger.debug("[P2] IdP AUTHN response: %s", idp_response) _ecp_response = None for item in respdict["header"]: if item.c_tag == "Response" and item.c_namespace == ecp.NAMESPACE: _ecp_response = item _acs_url = _ecp_response.assertion_consumer_service_url if rc_url != _acs_url: error = ("response_consumer_url '%s' does not match" % rc_url, "assertion_consumer_service_url '%s" % _acs_url) # Send an error message to the SP _ = self.send(rc_url, "POST", data=soap.soap_fault(error)) # Raise an exception so the user knows something went wrong raise SAMLError(error) return idp_response
def entity_descriptor(confd): mycert = None enc_cert = None if confd.cert_file is not None: mycert = [] mycert.append("".join(read_cert(confd.cert_file))) if confd.additional_cert_files is not None: for _cert_file in confd.additional_cert_files: mycert.append("".join(read_cert(_cert_file))) if confd.encryption_keypairs is not None: enc_cert = [] for _encryption in confd.encryption_keypairs: enc_cert.append("".join(read_cert(_encryption["cert_file"]))) entd = md.EntityDescriptor() entd.entity_id = confd.entityid if confd.valid_for: entd.valid_until = in_a_while(hours=int(confd.valid_for)) if confd.organization is not None: entd.organization = do_organization_info(confd.organization) if confd.contact_person is not None: entd.contact_person = do_contact_persons_info(confd.contact_person) if confd.assurance_certification: if not entd.extensions: entd.extensions = md.Extensions() ava = [AttributeValue(text=c) for c in confd.assurance_certification] attr = Attribute( attribute_value=ava, name="urn:oasis:names:tc:SAML:attribute:assurance-certification", ) _add_attr_to_entity_attributes(entd.extensions, attr) if confd.entity_category: if not entd.extensions: entd.extensions = md.Extensions() ava = [AttributeValue(text=c) for c in confd.entity_category] attr = Attribute(attribute_value=ava, name="http://macedir.org/entity-category") _add_attr_to_entity_attributes(entd.extensions, attr) if confd.entity_category_support: if not entd.extensions: entd.extensions = md.Extensions() ava = [AttributeValue(text=c) for c in confd.entity_category_support] attr = Attribute(attribute_value=ava, name="http://macedir.org/entity-category-support") _add_attr_to_entity_attributes(entd.extensions, attr) for item in algorithm_support_in_metadata(confd.xmlsec_binary): if not entd.extensions: entd.extensions = md.Extensions() entd.extensions.add_extension_element(item) conf_sp_type = confd.getattr('sp_type', 'sp') conf_sp_type_in_md = confd.getattr('sp_type_in_metadata', 'sp') if conf_sp_type and conf_sp_type_in_md is True: if not entd.extensions: entd.extensions = md.Extensions() item = sp_type.SPType(text=conf_sp_type) entd.extensions.add_extension_element(item) serves = confd.serves if not serves: raise SAMLError( 'No service type ("sp","idp","aa") provided in the configuration') if "sp" in serves: confd.context = "sp" entd.spsso_descriptor = do_spsso_descriptor(confd, mycert, enc_cert) if "idp" in serves: confd.context = "idp" entd.idpsso_descriptor = do_idpsso_descriptor(confd, mycert, enc_cert) if "aa" in serves: confd.context = "aa" entd.attribute_authority_descriptor = do_aa_descriptor( confd, mycert, enc_cert) if "pdp" in serves: confd.context = "pdp" entd.pdp_descriptor = do_pdp_descriptor(confd, mycert, enc_cert) if "aq" in serves: confd.context = "aq" entd.authn_authority_descriptor = do_aq_descriptor( confd, mycert, enc_cert) return entd
def do_uiinfo(_uiinfo): uii = mdui.UIInfo() for attr in [ 'display_name', 'description', "information_url", 'privacy_statement_url' ]: try: val = _uiinfo[attr] except KeyError: continue aclass = uii.child_class(attr) inst = getattr(uii, attr) if isinstance(val, six.string_types): ainst = aclass(text=val) inst.append(ainst) elif isinstance(val, dict): ainst = aclass() ainst.text = val["text"] ainst.lang = val["lang"] inst.append(ainst) else: for value in val: if isinstance(value, six.string_types): ainst = aclass(text=value) inst.append(ainst) elif isinstance(value, dict): ainst = aclass() ainst.text = value["text"] ainst.lang = value["lang"] inst.append(ainst) try: _attr = "logo" val = _uiinfo[_attr] inst = getattr(uii, _attr) # dictionary or list of dictionaries if isinstance(val, dict): logo = mdui.Logo() for attr, value in val.items(): if attr in logo.keys(): setattr(logo, attr, value) inst.append(logo) elif isinstance(val, list): for logga in val: if not isinstance(logga, dict): raise SAMLError("Configuration error !!") logo = mdui.Logo() for attr, value in logga.items(): if attr in logo.keys(): setattr(logo, attr, value) inst.append(logo) except KeyError: pass try: _attr = "keywords" val = _uiinfo[_attr] inst = getattr(uii, _attr) # list of six.string_types, dictionary or list of dictionaries if isinstance(val, list): for value in val: keyw = mdui.Keywords() if isinstance(value, six.string_types): keyw.text = value elif isinstance(value, dict): keyw.text = " ".join(value["text"]) try: keyw.lang = value["lang"] except KeyError: pass else: raise SAMLError("Configuration error: ui_info keywords") inst.append(keyw) elif isinstance(val, dict): keyw = mdui.Keywords() keyw.text = " ".join(val["text"]) try: keyw.lang = val["lang"] except KeyError: pass inst.append(keyw) else: raise SAMLError("Configuration Error: ui_info keywords") except KeyError: pass return uii