def __init__(self, config=None, identity_cache=None, state_cache=None, virtual_organization="", config_file="", msg_cb=None): """ :param config: A saml2.config.Config instance :param identity_cache: Where the class should store identity information :param state_cache: Where the class should keep state information :param virtual_organization: A specific virtual organization """ Entity.__init__(self, "sp", config, config_file, virtual_organization, msg_cb=msg_cb) self.users = Population(identity_cache) self.lock = threading.Lock() # for server state storage if state_cache is None: self.state = {} # in memory storage else: self.state = state_cache attribute_defaults = { "logout_requests_signed": False, "allow_unsolicited": False, "authn_requests_signed": False, "want_assertions_signed": False, "want_response_signed": True, "want_assertions_or_response_signed": False } for attr, val_default in attribute_defaults.items(): val_config = self.config.getattr(attr, "sp") if val_config is None: val = val_default else: val = val_config if val == 'true': val = True setattr(self, attr, val) if self.entity_type == "sp" and not any([ self.want_assertions_signed, self.want_response_signed, self.want_assertions_or_response_signed, ]): logger.warning( "The SAML service provider accepts unsigned SAML Responses " "and Assertions. This configuration is insecure.") self.artifact2response = {}
def __init__(self, config=None, identity_cache=None, state_cache=None, virtual_organization="", config_file=""): """ :param config: A saml2.config.Config instance :param identity_cache: Where the class should store identity information :param state_cache: Where the class should keep state information :param virtual_organization: A specific virtual organization """ Entity.__init__(self, "sp", config, config_file, virtual_organization) self.users = Population(identity_cache) self.lock = threading.Lock() # for server state storage if state_cache is None: self.state = {} # in memory storage else: self.state = state_cache self.logout_requests_signed = False self.allow_unsolicited = False self.authn_requests_signed = False self.want_assertions_signed = False self.want_response_signed = False for foo in ["allow_unsolicited", "authn_requests_signed", "logout_requests_signed", "want_assertions_signed", "want_response_signed"]: v = self.config.getattr(foo, "sp") if v is True or v == 'true': setattr(self, foo, True) self.artifact2response = {}
def __init__(self, config=None, identity_cache=None, state_cache=None, virtual_organization=None, config_file=""): """ :param config: A saml2.config.Config instance :param identity_cache: Where the class should store identity information :param state_cache: Where the class should keep state information :param virtual_organization: Which if any virtual organization this SP belongs to """ self.users = Population(identity_cache) # for server state storage if state_cache is None: self.state = {} # in memory storage else: self.state = state_cache if config: self.config = config elif config_file: self.config = config_factory("sp", config_file) else: raise Exception("Missing configuration") self.metadata = self.config.metadata self.config.setup_logger() # we copy the config.debug variable in an internal # field for convenience and because we may need to # change it during the tests self.debug = self.config.debug self.sec = security_context(self.config) if virtual_organization: self.vorg = VirtualOrg(self, virtual_organization) else: self.vorg = None if "allow_unsolicited" in self.config: self.allow_unsolicited = self.config.allow_unsolicited else: self.allow_unsolicited = False if getattr(self.config, 'authn_requests_signed', 'false') == 'true': self.authn_requests_signed_default = True else: self.authn_requests_signed_default = False if getattr(self.config, 'logout_requests_signed', 'false') == 'true': self.logout_requests_signed_default = True else: self.logout_requests_signed_default = False
def __init__(self, config=None, identity_cache=None, state_cache=None, virtual_organization="", config_file=""): """ :param config: A saml2.config.Config instance :param identity_cache: Where the class should store identity information :param state_cache: Where the class should keep state information :param virtual_organization: A specific virtual organization """ Entity.__init__(self, "sp", config, config_file, virtual_organization) self.users = Population(identity_cache) # for server state storage if state_cache is None: self.state = {} # in memory storage else: self.state = state_cache for foo in ["allow_unsolicited", "authn_requests_signed", "logout_requests_signed"]: if self.config.getattr(foo, "sp") == 'true': setattr(self, foo, True) else: setattr(self, foo, False) self.artifact2response = {}
def __init__(self, config=None, identity_cache=None, state_cache=None, virtual_organization=None, config_file="", logger=None): """ :param config: A saml2.config.Config instance :param identity_cache: Where the class should store identity information :param state_cache: Where the class should keep state information :param virtual_organization: Which if any virtual organization this SP belongs to """ self.users = Population(identity_cache) # for server state storage if state_cache is None: self.state = {} # in memory storage else: self.state = state_cache if config: self.config = config elif config_file: self.config = config_factory("sp", config_file) else: raise Exception("Missing configuration") self.metadata = self.config.metadata if logger is None: self.logger = self.config.setup_logger() else: self.logger = logger # we copy the config.debug variable in an internal # field for convenience and because we may need to # change it during the tests self.debug = self.config.debug self.sec = security_context(self.config, log=self.logger, debug=self.debug) if virtual_organization: self.vorg = VirtualOrg(self, virtual_organization) else: self.vorg = None if "allow_unsolicited" in self.config: self.allow_unsolicited = self.config.allow_unsolicited else: self.allow_unsolicited = False if getattr(self.config, 'authn_requests_signed', 'false') == 'true': self.authn_requests_signed_default = True else: self.authn_requests_signed_default = False if getattr(self.config, 'logout_requests_signed', 'false') == 'true': self.logout_requests_signed_default = True else: self.logout_requests_signed_default = False
def __init__(self, config=None, identity_cache=None, state_cache=None, virtual_organization="", config_file=""): """ :param config: A saml2.config.Config instance :param identity_cache: Where the class should store identity information :param state_cache: Where the class should keep state information :param virtual_organization: A specific virtual organization """ Entity.__init__(self, "sp", config, config_file, virtual_organization) self.users = Population(identity_cache) self.lock = threading.Lock() # for server state storage if state_cache is None: self.state = {} # in memory storage else: self.state = state_cache self.logout_requests_signed = False self.allow_unsolicited = False self.authn_requests_signed = False self.want_assertions_signed = False self.want_response_signed = False for attribute in ["allow_unsolicited", "authn_requests_signed", "logout_requests_signed", "want_assertions_signed", "want_response_signed"]: v = self.config.getattr(attribute, "sp") if v is True or v == 'true': setattr(self, attribute, True) self.artifact2response = {}
def __init__(self, config=None, identity_cache=None, state_cache=None, virtual_organization="",config_file=""): """ :param config: A saml2.config.Config instance :param identity_cache: Where the class should store identity information :param state_cache: Where the class should keep state information :param virtual_organization: A specific virtual organization """ self.users = Population(identity_cache) # for server state storage if state_cache is None: self.state = {} # in memory storage else: self.state = state_cache if config: self.config = config elif config_file: self.config = config_factory("sp", config_file) else: raise Exception("Missing configuration") if self.config.vorg: for vo in self.config.vorg.values(): vo.sp = self self.metadata = self.config.metadata self.config.setup_logger() # we copy the config.debug variable in an internal # field for convenience and because we may need to # change it during the tests self.debug = self.config.debug self.sec = security_context(self.config) if virtual_organization: if isinstance(virtual_organization, basestring): self.vorg = self.config.vorg[virtual_organization] elif isinstance(virtual_organization, VirtualOrg): self.vorg = virtual_organization else: self.vorg = {} for foo in ["allow_unsolicited", "authn_requests_signed", "logout_requests_signed"]: if self.config.getattr("sp", foo) == 'true': setattr(self, foo, True) else: setattr(self, foo, False) # extra randomness self.seed = rndstr(32) self.logout_requests_signed_default = True self.allow_unsolicited = self.config.getattr("allow_unsolicited", "sp")
def __init__(self, config=None, identity_cache=None, state_cache=None, virtual_organization="", config_file="", msg_cb=None): """ :param config: A saml2.config.Config instance :param identity_cache: Where the class should store identity information :param state_cache: Where the class should keep state information :param virtual_organization: A specific virtual organization """ Entity.__init__(self, "sp", config, config_file, virtual_organization, msg_cb=msg_cb) self.users = Population(identity_cache) self.lock = threading.Lock() # for server state storage if state_cache is None: self.state = {} # in memory storage else: self.state = state_cache attribute_defaults = { "logout_requests_signed": False, "allow_unsolicited": False, "authn_requests_signed": False, "want_assertions_signed": False, "want_response_signed": True, "want_assertions_or_response_signed" : False } for attr, val_default in attribute_defaults.items(): val_config = self.config.getattr(attr, "sp") if val_config is None: val = val_default else: val = val_config if val == 'true': val = True setattr(self, attr, val) if self.entity_type == "sp" and not any( [ self.want_assertions_signed, self.want_response_signed, self.want_assertions_or_response_signed, ] ): logger.warning( "The SAML service provider accepts unsigned SAML Responses " "and Assertions. This configuration is insecure." ) self.artifact2response = {}
class Saml2Client(object): """ The basic pySAML2 service provider class """ def __init__( self, config=None, debug=0, identity_cache=None, state_cache=None, virtual_organization=None, config_file="", logger=None, ): """ :param config: A saml2.config.Config instance :param debug: Whether debugging should be done even if the configuration says otherwise :param identity_cache: Where the class should store identity information :param state_cache: Where the class should keep state information :param virtual_organization: Which if any virtual organization this SP belongs to """ self.users = Population(identity_cache) # for server state storage if state_cache is None: self.state = {} # in memory storage else: self.state = state_cache self.sec = None if config: self.config = config elif config_file: self.config = config_factory("sp", config_file) else: raise Exception("Missing configuration") self.metadata = self.config.metadata if logger is None: self.logger = self.config.setup_logger() else: self.logger = logger if not debug and self.config: self.debug = self.config.debug else: self.debug = debug self.sec = security_context(self.config, log=self.logger, debug=self.debug) if virtual_organization: self.vorg = VirtualOrg(self, virtual_organization) else: self.vorg = None if "allow_unsolicited" in self.config: self.allow_unsolicited = self.config.allow_unsolicited else: self.allow_unsolicited = False if "verify_signatures" in self.config: self.verify_signatures = self.config.verify_signatures else: self.verify_signatures = True if getattr(self.config, "authn_requests_signed", "false") == "true": self.authn_requests_signed_default = True else: self.authn_requests_signed_default = False if getattr(self.config, "logout_requests_signed", "false") == "true": self.logout_requests_signed_default = True else: self.logout_requests_signed_default = False def _relay_state(self, session_id): vals = [session_id, str(int(time.time()))] if self.config.secret is None: vals.append(signature("", vals)) else: vals.append(signature(self.config.secret, vals)) return "|".join(vals) def _init_request(self, request, destination): # request.id = sid() request.version = VERSION request.issue_instant = instant() request.destination = destination return request # def idp_entry(self, name=None, location=None, provider_id=None): # """ Create an IDP entry # # :param name: The name of the IdP # :param location: The location of the IdP # :param provider_id: The identifier of the provider # :return: A IdPEntry instance # """ # res = samlp.IDPEntry() # if name: # res.name = name # if location: # res.loc = location # if provider_id: # res.provider_id = provider_id # # return res # # def scoping_from_metadata(self, entityid, location=None): # """ Set the scope of the assertion # # :param entityid: The EntityID of the server # :param location: The location of the server # :return: A samlp.Scoping instance # """ # name = self.metadata.name(entityid) # idp_ent = self.idp_entry(name, location) # return samlp.Scoping(idp_list=samlp.IDPList(idp_entry=[idp_ent])) def response(self, post, outstanding, log=None, decode=True, asynchop=True): """ Deal with an AuthnResponse or LogoutResponse :param post: The reply as a dictionary :param outstanding: A dictionary with session IDs as keys and the original web request from the user before redirection as values. :param log: where loggin should go. :param decode: Whether the response is Base64 encoded or not :param asynchop: Whether the response was return over a asynchronous connection. SOAP for instance is synchronous :return: An response.AuthnResponse or response.LogoutResponse instance """ # If the request contains a samlResponse, try to validate it try: saml_response = post["SAMLResponse"] except KeyError: return None try: _ = self.config.entityid except KeyError: raise Exception("Missing entity_id specification") if log is None: log = self.logger reply_addr = self.service_url() resp = None if saml_response: try: resp = response_factory( saml_response, self.config, reply_addr, outstanding, log, debug=self.debug, decode=decode, asynchop=asynchop, allow_unsolicited=self.allow_unsolicited, ) except Exception, exc: raise if log: log.error("%s" % exc) return None if self.debug: if log: log.info(">> %s", resp) resp = resp.verify() if isinstance(resp, AuthnResponse): self.users.add_information_about_person(resp.session_info()) if log: log.error("--- ADDED person info ----") elif isinstance(resp, LogoutResponse): self.handle_logout_response(resp, log) elif log: log.error("Other response type: %s" % saml2.class_name(resp)) return resp
class Base(object): """ The basic pySAML2 service provider class """ def __init__(self, config=None, identity_cache=None, state_cache=None, virtual_organization="",config_file=""): """ :param config: A saml2.config.Config instance :param identity_cache: Where the class should store identity information :param state_cache: Where the class should keep state information :param virtual_organization: A specific virtual organization """ self.users = Population(identity_cache) # for server state storage if state_cache is None: self.state = {} # in memory storage else: self.state = state_cache if config: self.config = config elif config_file: self.config = config_factory("sp", config_file) else: raise Exception("Missing configuration") if self.config.vorg: for vo in self.config.vorg.values(): vo.sp = self self.metadata = self.config.metadata self.config.setup_logger() # we copy the config.debug variable in an internal # field for convenience and because we may need to # change it during the tests self.debug = self.config.debug self.sec = security_context(self.config) if virtual_organization: if isinstance(virtual_organization, basestring): self.vorg = self.config.vorg[virtual_organization] elif isinstance(virtual_organization, VirtualOrg): self.vorg = virtual_organization else: self.vorg = {} for foo in ["allow_unsolicited", "authn_requests_signed", "logout_requests_signed"]: if self.config.getattr("sp", foo) == 'true': setattr(self, foo, True) else: setattr(self, foo, False) # extra randomness self.seed = rndstr(32) self.logout_requests_signed_default = True self.allow_unsolicited = self.config.getattr("allow_unsolicited", "sp") # # Private methods # def _relay_state(self, session_id): vals = [session_id, str(int(time.time()))] if self.config.secret is None: vals.append(signature("", vals)) else: vals.append(signature(self.config.secret, vals)) return "|".join(vals) def _issuer(self, entityid=None): """ Return an Issuer instance """ if entityid: if isinstance(entityid, saml.Issuer): return entityid else: return saml.Issuer(text=entityid, format=saml.NAMEID_FORMAT_ENTITY) else: return saml.Issuer(text=self.config.entityid, format=saml.NAMEID_FORMAT_ENTITY) def _sso_location(self, entityid=None, binding=BINDING_HTTP_REDIRECT): if entityid: # verify that it's in the metadata try: return self.config.single_sign_on_services(entityid, binding)[0] except IndexError: logger.info("_sso_location: %s, %s" % (entityid, binding)) raise IdpUnspecified("No IdP to send to given the premises") # get the idp location from the configuration alternative the # metadata. If there is more than one IdP in the configuration # raise exception eids = self.config.idps() if len(eids) > 1: raise IdpUnspecified("Too many IdPs to choose from: %s" % eids) try: loc = self.config.single_sign_on_services(eids.keys()[0], binding)[0] return loc except IndexError: raise IdpUnspecified("No IdP to send to given the premises") def _my_name(self): return self.config.name # # Public API # def add_vo_information_about_user(self, subject_id): """ Add information to the knowledge I have about the user. This is for Virtual organizations. :param subject_id: The subject identifier :return: A possibly extended knowledge. """ ava = {} try: (ava, _) = self.users.get_identity(subject_id) except KeyError: pass # is this a Virtual Organization situation if self.vorg: if self.vorg.do_aggregation(subject_id): # Get the extended identity ava = self.users.get_identity(subject_id)[0] return ava #noinspection PyUnusedLocal def is_session_valid(self, _session_id): """ Place holder. Supposed to check if the session is still valid. """ return True def service_url(self, binding=BINDING_HTTP_POST): _res = self.config.endpoint("assertion_consumer_service", binding, "sp") if _res: return _res[0] else: return None def _message(self, request_cls, destination=None, id=0, consent=None, extensions=None, sign=False, **kwargs): """ Some parameters appear in all requests so simplify by doing it in one place :param request_cls: The specific request type :param destination: The recipient :param id: A message identifier :param consent: Whether the principal have given her consent :param extensions: Possible extensions :param kwargs: Key word arguments specific to one request type :return: An instance of the request_cls """ if not id: id = sid(self.seed) req = request_cls(id=id, version=VERSION, issue_instant=instant(), issuer=self._issuer(), **kwargs) if destination: req.destination = destination if consent: req.consent = consent if extensions: req.extensions = extensions if sign: req.signature = pre_signature_part(req.id, self.sec.my_cert, 1) to_sign = [(class_name(req), req.id)] else: to_sign = [] logger.info("REQUEST: %s" % req) return signed_instance_factory(req, self.sec, to_sign) def create_authn_request(self, destination, vorg="", scoping=None, binding=saml2.BINDING_HTTP_POST, nameid_format=NAMEID_FORMAT_TRANSIENT, service_url_binding=None, id=0, consent=None, extensions=None, sign=None, allow_create=False): """ Creates an authentication request. :param destination: Where the request should be sent. :param vorg: The virtual organization the service belongs to. :param scoping: The scope of the request :param binding: The protocol to use for the Response !! :param nameid_format: Format of the NameID :param service_url_binding: Where the reply should be sent dependent on reply binding. :param id: The identifier for this request :param consent: Whether the principal have given her consent :param extensions: Possible extensions :param sign: Whether the request should be signed or not. :param allow_create: If the identity provider is allowed, in the course of fulfilling the request, to create a new identifier to represent the principal. :return: <samlp:AuthnRequest> instance """ if service_url_binding is None: service_url = self.service_url(binding) else: service_url = self.service_url(service_url_binding) if binding == BINDING_PAOS: my_name = None location = None else: my_name = self._my_name() if allow_create: allow_create="true" else: allow_create="false" # Profile stuff, should be configurable if nameid_format is None or nameid_format == NAMEID_FORMAT_TRANSIENT: name_id_policy = samlp.NameIDPolicy(allow_create=allow_create, format=NAMEID_FORMAT_TRANSIENT) else: name_id_policy = samlp.NameIDPolicy(allow_create=allow_create, format=nameid_format) if vorg: try: name_id_policy.sp_name_qualifier = vorg name_id_policy.format = saml.NAMEID_FORMAT_PERSISTENT except KeyError: pass return self._message(AuthnRequest, destination, id, consent, extensions, sign, assertion_consumer_service_url=service_url, protocol_binding=binding, name_id_policy=name_id_policy, provider_name=my_name, scoping=scoping) def create_attribute_query(self, destination, subject_id, attribute=None, sp_name_qualifier=None, name_qualifier=None, nameid_format=None, id=0, consent=None, extensions=None, sign=False, **kwargs): """ Constructs an AttributeQuery :param destination: 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. The key are one of 4 variants: 3-tuple of name_format,name and friendly_name, 2-tuple of name_format and name, 1-tuple with name or just the name as a string. :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 id: The identifier of the session :param consent: Whether the principal have given her consent :param extensions: Possible extensions :param sign: Whether the query should be signed or not. :return: An AttributeQuery instance """ subject = saml.Subject( name_id = saml.NameID(text=subject_id, format=nameid_format, sp_name_qualifier=sp_name_qualifier, name_qualifier=name_qualifier)) if attribute: attribute = do_attributes(attribute) return self._message(AttributeQuery, destination, id, consent, extensions, sign, subject=subject, attribute=attribute) def create_logout_request(self, destination, issuer_entity_id, subject_id=None, name_id=None, reason=None, expire=None, id=0, consent=None, extensions=None, sign=False): """ Constructs a LogoutRequest :param destination: Destination of the request :param issuer_entity_id: The entity ID of the IdP the request is target at. :param subject_id: The identifier of the subject :param name_id: A NameID instance identifying the subject :param reason: An indication of the reason for the logout, in the form of a URI reference. :param expire: The time at which the request expires, after which the recipient may discard the message. :param id: Request identifier :param consent: Whether the principal have given her consent :param extensions: Possible extensions :param sign: Whether the query should be signed or not. :return: A LogoutRequest instance """ if subject_id: name_id = saml.NameID( text = self.users.get_entityid(subject_id, issuer_entity_id, False)) if not name_id: raise Exception("Missing subject identification") return self._message(LogoutRequest, destination, id, consent, extensions, sign, name_id=name_id, reason=reason, not_on_or_after=expire) def create_logout_response(self, idp_entity_id, request_id, status_code, binding=BINDING_HTTP_REDIRECT): """ Constructs a LogoutResponse :param idp_entity_id: The entityid of the IdP that want to do the logout :param request_id: The Id of the request we are replying to :param status_code: The status code of the response :param binding: The type of binding that will be used for the response :return: A LogoutResponse instance """ destination = self.config.single_logout_services(idp_entity_id, binding)[0] status = samlp.Status( status_code=samlp.StatusCode(value=status_code)) return destination, self._message(LogoutResponse, destination, in_response_to=request_id, status=status) # MUST use SOAP for # AssertionIDRequest, SubjectQuery, # AuthnQuery, AttributeQuery, or AuthzDecisionQuery def create_authz_decision_query(self, destination, action, evidence=None, resource=None, subject=None, id=0, consent=None, extensions=None, sign=None): """ Creates an authz decision query. :param destination: The IdP endpoint :param action: The action you want to perform (has to be at least one) :param evidence: Why you should be able to perform the action :param resource: The resource you want to perform the action on :param subject: Who wants to do the thing :param id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: AuthzDecisionQuery instance """ return self._message(AuthzDecisionQuery, destination, id, consent, extensions, sign, action=action, evidence=evidence, resource=resource, subject=subject) def create_authz_decision_query_using_assertion(self, destination, assertion, action=None, resource=None, subject=None, id=0, consent=None, extensions=None, sign=False): """ Makes an authz decision query. :param destination: The IdP endpoint to send the request to :param assertion: An Assertion instance :param action: The action you want to perform (has to be at least one) :param resource: The resource you want to perform the action on :param subject: Who wants to do the thing :param id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: AuthzDecisionQuery instance """ if action: if isinstance(action, basestring): _action = [saml.Action(text=action)] else: _action = [saml.Action(text=a) for a in action] else: _action = None return self.create_authz_decision_query(destination, _action, saml.Evidence(assertion=assertion), resource, subject, id=id, consent=consent, extensions=extensions, sign=sign) def create_assertion_id_request(self, assertion_id_refs, destination=None, id=0, consent=None, extensions=None, sign=False): """ :param assertion_id_refs: :param destination: The IdP endpoint to send the request to :param id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: AssertionIDRequest instance """ id_refs = [AssertionIDRef(text=s) for s in assertion_id_refs] return self._message(AssertionIDRequest, destination, id, consent, extensions, sign, assertion_id_refs=id_refs ) def create_authn_query(self, subject, destination=None, authn_context=None, session_index="", id=0, consent=None, extensions=None, sign=False): """ :param subject: :param destination: The IdP endpoint to send the request to :param authn_context: :param session_index: :param id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: """ return self._message(AuthnQuery, destination, id, consent, extensions, sign, subject=subject, session_index=session_index, requested_auth_context=authn_context) def create_nameid_mapping_request(self, nameid_policy, nameid=None, baseid=None, encryptedid=None, destination=None, id=0, consent=None, extensions=None, sign=False): """ :param nameid_policy: :param nameid: :param baseid: :param encryptedid: :param destination: :param id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: """ # One of them must be present assert nameid or baseid or encryptedid if nameid: return self._message(NameIDMappingRequest, destination, id, consent, extensions, sign, nameid_policy=nameid_policy, nameid=nameid) elif baseid: return self._message(NameIDMappingRequest, destination, id, consent, extensions, sign, nameid_policy=nameid_policy, baseid=baseid) else: return self._message(NameIDMappingRequest, destination, id, consent, extensions, sign, nameid_policy=nameid_policy, encryptedid=encryptedid) def create_manage_nameid_request(self): pass # ======== response handling =========== def _response(self, post, outstanding, decode=True, asynchop=True): """ Deal with an AuthnResponse or LogoutResponse :param post: The reply as a dictionary :param outstanding: A dictionary with session IDs as keys and the original web request from the user before redirection as values. :param decode: Whether the response is Base64 encoded or not :param asynchop: Whether the response was return over a asynchronous connection. SOAP for instance is synchronous :return: An response.AuthnResponse or response.LogoutResponse instance """ # If the request contains a samlResponse, try to validate it try: saml_response = post['SAMLResponse'] except KeyError: return None try: _ = self.config.entityid except KeyError: raise Exception("Missing entity_id specification") reply_addr = self.service_url() resp = None if saml_response: try: resp = response_factory(saml_response, self.config, reply_addr, outstanding, decode=decode, asynchop=asynchop, allow_unsolicited=self.allow_unsolicited) except Exception, exc: logger.error("%s" % exc) return None logger.debug(">> %s", resp) resp = resp.verify() if isinstance(resp, AuthnResponse): self.users.add_information_about_person(resp.session_info()) logger.info("--- ADDED person info ----") else: logger.error("Response type not supported: %s" % ( saml2.class_name(resp),)) return resp
class Base(Entity): """ The basic pySAML2 service provider class """ def __init__(self, config=None, identity_cache=None, state_cache=None, virtual_organization="", config_file="", msg_cb=None): """ :param config: A saml2.config.Config instance :param identity_cache: Where the class should store identity information :param state_cache: Where the class should keep state information :param virtual_organization: A specific virtual organization """ Entity.__init__(self, "sp", config, config_file, virtual_organization, msg_cb=msg_cb) self.users = Population(identity_cache) self.lock = threading.Lock() # for server state storage if state_cache is None: self.state = {} # in memory storage else: self.state = state_cache attribute_defaults = { "logout_requests_signed": False, "allow_unsolicited": False, "authn_requests_signed": False, "want_assertions_signed": False, "want_response_signed": True, } for attr, val_default in attribute_defaults.items(): val_config = self.config.getattr(attr, "sp") if val_config is None: val = val_default else: val = val_config if val == 'true': val = True setattr(self, attr, val) if self.entity_type == "sp" and not any( [self.want_assertions_signed, self.want_response_signed]): logger.warning( "The SAML service provider accepts unsigned SAML Responses " "and Assertions. This configuration is insecure.") self.artifact2response = {} # # Private methods # def _relay_state(self, session_id): vals = [session_id, str(int(time.time()))] if self.config.secret is None: vals.append(signature("", vals)) else: vals.append(signature(self.config.secret, vals)) return "|".join(vals) 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 destinations(srvs)[0] 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 destinations(srvs)[0] except IndexError: raise IdpUnspecified("No IdP to send to given the premises") def sso_location(self, entityid=None, binding=BINDING_HTTP_REDIRECT): return self._sso_location(entityid, binding) def _my_name(self): return self.config.name # # Public API # def add_vo_information_about_user(self, name_id): """ Add information to the knowledge I have about the user. This is for Virtual organizations. :param name_id: The subject identifier :return: A possibly extended knowledge. """ ava = {} try: (ava, _) = self.users.get_identity(name_id) except KeyError: pass # is this a Virtual Organization situation if self.vorg: if self.vorg.do_aggregation(name_id): # Get the extended identity ava = self.users.get_identity(name_id)[0] return ava # noinspection PyUnusedLocal @staticmethod def is_session_valid(_session_id): """ Place holder. Supposed to check if the session is still valid. """ return True def service_urls(self, binding=BINDING_HTTP_POST): _res = self.config.endpoint("assertion_consumer_service", binding, "sp") if _res: return _res else: return None def create_authn_request(self, destination, vorg="", scoping=None, binding=saml2.BINDING_HTTP_POST, nameid_format=None, service_url_binding=None, message_id=0, consent=None, extensions=None, sign=None, allow_create=None, sign_prepare=False, sign_alg=None, digest_alg=None, **kwargs): """ Creates an authentication request. :param destination: Where the request should be sent. :param vorg: The virtual organization the service belongs to. :param scoping: The scope of the request :param binding: The protocol to use for the Response !! :param nameid_format: Format of the NameID :param service_url_binding: Where the reply should be sent dependent on reply binding. :param message_id: The identifier for this request :param consent: Whether the principal have given her consent :param extensions: Possible extensions :param sign: Whether the request should be signed or not. :param sign_prepare: Whether the signature should be prepared or not. :param allow_create: If the identity provider is allowed, in the course of fulfilling the request, to create a new identifier to represent the principal. :param kwargs: Extra key word arguments :return: tuple of request ID and <samlp:AuthnRequest> instance """ client_crt = None if "client_crt" in kwargs: client_crt = kwargs["client_crt"] args = {} if self.config.getattr('hide_assertion_consumer_service', 'sp'): args["assertion_consumer_service_url"] = None binding = None else: try: args["assertion_consumer_service_url"] = kwargs[ "assertion_consumer_service_urls"][0] del kwargs["assertion_consumer_service_urls"] except KeyError: try: args["assertion_consumer_service_url"] = kwargs[ "assertion_consumer_service_url"] del kwargs["assertion_consumer_service_url"] except KeyError: try: args["assertion_consumer_service_index"] = str( kwargs["assertion_consumer_service_index"]) del kwargs["assertion_consumer_service_index"] except KeyError: if service_url_binding is None: service_urls = self.service_urls(binding) else: service_urls = self.service_urls( service_url_binding) args["assertion_consumer_service_url"] = service_urls[ 0] try: args["provider_name"] = kwargs["provider_name"] except KeyError: if binding == BINDING_PAOS: pass else: args["provider_name"] = self._my_name() # Allow argument values either as class instances or as dictionaries # all of these have cardinality 0..1 _msg = AuthnRequest() for param in [ "scoping", "requested_authn_context", "conditions", "subject" ]: try: _item = kwargs[param] except KeyError: pass else: del kwargs[param] # either class instance or argument dictionary if isinstance(_item, _msg.child_class(param)): args[param] = _item elif isinstance(_item, dict): args[param] = RequestedAuthnContext(**_item) else: raise ValueError("%s or wrong type expected %s" % (_item, param)) try: args["name_id_policy"] = kwargs["name_id_policy"] del kwargs["name_id_policy"] except KeyError: if allow_create is None: allow_create = self.config.getattr( "name_id_format_allow_create", "sp") if allow_create is None: allow_create = "false" else: if allow_create is True: allow_create = "true" else: allow_create = "false" if nameid_format == "": name_id_policy = None else: if nameid_format is None: nameid_format = self.config.getattr("name_id_format", "sp") # If no nameid_format has been set in the configuration # or passed in then transient is the default. if nameid_format is None: nameid_format = NAMEID_FORMAT_TRANSIENT # If a list has been configured or passed in choose the # first since NameIDPolicy can only have one format specified. elif isinstance(nameid_format, list): nameid_format = nameid_format[0] # Allow a deployer to signal that no format should be specified # in the NameIDPolicy by passing in or configuring the string 'None'. elif nameid_format == 'None': nameid_format = None name_id_policy = samlp.NameIDPolicy(allow_create=allow_create, format=nameid_format) if name_id_policy and vorg: try: name_id_policy.sp_name_qualifier = vorg name_id_policy.format = saml.NAMEID_FORMAT_PERSISTENT except KeyError: pass args["name_id_policy"] = name_id_policy try: nsprefix = kwargs["nsprefix"] except KeyError: nsprefix = None try: force_authn = kwargs['force_authn'] except KeyError: force_authn = self.config.getattr('force_authn', 'sp') finally: if force_authn: args['force_authn'] = 'true' conf_sp_type = self.config.getattr('sp_type', 'sp') conf_sp_type_in_md = self.config.getattr('sp_type_in_metadata', 'sp') if conf_sp_type and conf_sp_type_in_md is False: if not extensions: extensions = Extensions() item = sp_type.SPType(text=conf_sp_type) extensions.add_extension_element(item) requested_attrs = self.config.getattr('requested_attributes', 'sp') if requested_attrs: if not extensions: extensions = Extensions() attributemapsmods = [] for modname in attributemaps.__all__: attributemapsmods.append(getattr(attributemaps, modname)) items = [] for attr in requested_attrs: friendly_name = attr.get('friendly_name') name = attr.get('name') name_format = attr.get('name_format') is_required = str(attr.get('required', False)).lower() if not name and not friendly_name: raise ValueError( "Missing required attribute: '{}' or '{}'".format( 'name', 'friendly_name')) if not name: for mod in attributemapsmods: try: name = mod.MAP['to'][friendly_name] except KeyError: continue else: if not name_format: name_format = mod.MAP['identifier'] break if not friendly_name: for mod in attributemapsmods: try: friendly_name = mod.MAP['fro'][name] except KeyError: continue else: if not name_format: name_format = mod.MAP['identifier'] break items.append( requested_attributes.RequestedAttribute( is_required=is_required, name_format=name_format, friendly_name=friendly_name, name=name)) item = requested_attributes.RequestedAttributes( extension_elements=items) extensions.add_extension_element(item) if kwargs: _args, extensions = self._filter_args(AuthnRequest(), extensions, **kwargs) args.update(_args) try: del args["id"] except KeyError: pass if sign is None: sign = self.authn_requests_signed if (sign and self.sec.cert_handler.generate_cert()) or \ client_crt is not None: with self.lock: self.sec.cert_handler.update_cert(True, client_crt) if client_crt is not None: sign_prepare = True return self._message(AuthnRequest, destination, message_id, consent, extensions, sign, sign_prepare, protocol_binding=binding, scoping=scoping, nsprefix=nsprefix, sign_alg=sign_alg, digest_alg=digest_alg, **args) return self._message(AuthnRequest, destination, message_id, consent, extensions, sign, sign_prepare, protocol_binding=binding, scoping=scoping, nsprefix=nsprefix, sign_alg=sign_alg, digest_alg=digest_alg, **args) def create_attribute_query(self, destination, name_id=None, attribute=None, message_id=0, consent=None, extensions=None, sign=False, sign_prepare=False, sign_alg=None, digest_alg=None, **kwargs): """ Constructs an AttributeQuery :param destination: To whom the query should be sent :param name_id: The identifier of the subject :param attribute: A dictionary of attributes and values that is asked for. The key are one of 4 variants: 3-tuple of name_format,name and friendly_name, 2-tuple of name_format and name, 1-tuple with name or just the name as a string. :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 format: The format of the name ID :param message_id: The identifier of the session :param consent: Whether the principal have given her consent :param extensions: Possible extensions :param sign: Whether the query should be signed or not. :param sign_prepare: Whether the Signature element should be added. :return: Tuple of request ID and an AttributeQuery instance """ if name_id is None: if "subject_id" in kwargs: name_id = saml.NameID(text=kwargs["subject_id"]) for key in ["sp_name_qualifier", "name_qualifier", "format"]: try: setattr(name_id, key, kwargs[key]) except KeyError: pass else: raise AttributeError("Missing required parameter") elif isinstance(name_id, six.string_types): name_id = saml.NameID(text=name_id) for key in ["sp_name_qualifier", "name_qualifier", "format"]: try: setattr(name_id, key, kwargs[key]) except KeyError: pass subject = saml.Subject(name_id=name_id) if attribute: attribute = do_attributes(attribute) try: nsprefix = kwargs["nsprefix"] except KeyError: nsprefix = None return self._message(AttributeQuery, destination, message_id, consent, extensions, sign, sign_prepare, subject=subject, attribute=attribute, nsprefix=nsprefix, sign_alg=sign_alg, digest_alg=digest_alg) # MUST use SOAP for # AssertionIDRequest, SubjectQuery, # AuthnQuery, AttributeQuery, or AuthzDecisionQuery def create_authz_decision_query(self, destination, action, evidence=None, resource=None, subject=None, message_id=0, consent=None, extensions=None, sign=None, sign_alg=None, digest_alg=None, **kwargs): """ Creates an authz decision query. :param destination: The IdP endpoint :param action: The action you want to perform (has to be at least one) :param evidence: Why you should be able to perform the action :param resource: The resource you want to perform the action on :param subject: Who wants to do the thing :param message_id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: AuthzDecisionQuery instance """ return self._message(AuthzDecisionQuery, destination, message_id, consent, extensions, sign, action=action, evidence=evidence, resource=resource, subject=subject, sign_alg=sign_alg, digest_alg=digest_alg, **kwargs) def create_authz_decision_query_using_assertion(self, destination, assertion, action=None, resource=None, subject=None, message_id=0, consent=None, extensions=None, sign=False, nsprefix=None): """ Makes an authz decision query based on a previously received Assertion. :param destination: The IdP endpoint to send the request to :param assertion: An Assertion instance :param action: The action you want to perform (has to be at least one) :param resource: The resource you want to perform the action on :param subject: Who wants to do the thing :param message_id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: AuthzDecisionQuery instance """ if action: if isinstance(action, six.string_types): _action = [saml.Action(text=action)] else: _action = [saml.Action(text=a) for a in action] else: _action = None return self.create_authz_decision_query( destination, _action, saml.Evidence(assertion=assertion), resource, subject, message_id=message_id, consent=consent, extensions=extensions, sign=sign, nsprefix=nsprefix) @staticmethod def create_assertion_id_request(assertion_id_refs, **kwargs): """ :param assertion_id_refs: :return: One ID ref """ if isinstance(assertion_id_refs, six.string_types): return 0, assertion_id_refs else: return 0, assertion_id_refs[0] def create_authn_query(self, subject, destination=None, authn_context=None, session_index="", message_id=0, consent=None, extensions=None, sign=False, nsprefix=None, sign_alg=None, digest_alg=None): """ :param subject: The subject its all about as a <Subject> instance :param destination: The IdP endpoint to send the request to :param authn_context: list of <RequestedAuthnContext> instances :param session_index: a specified session index :param message_id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: """ return self._message(AuthnQuery, destination, message_id, consent, extensions, sign, subject=subject, session_index=session_index, requested_authn_context=authn_context, nsprefix=nsprefix, sign_alg=sign_alg, digest_alg=digest_alg) def create_name_id_mapping_request(self, name_id_policy, name_id=None, base_id=None, encrypted_id=None, destination=None, message_id=0, consent=None, extensions=None, sign=False, nsprefix=None, sign_alg=None, digest_alg=None): """ :param name_id_policy: :param name_id: :param base_id: :param encrypted_id: :param destination: :param message_id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: """ # One of them must be present assert name_id or base_id or encrypted_id if name_id: return self._message(NameIDMappingRequest, destination, message_id, consent, extensions, sign, name_id_policy=name_id_policy, name_id=name_id, nsprefix=nsprefix, sign_alg=sign_alg, digest_alg=digest_alg) elif base_id: return self._message(NameIDMappingRequest, destination, message_id, consent, extensions, sign, name_id_policy=name_id_policy, base_id=base_id, nsprefix=nsprefix, sign_alg=sign_alg, digest_alg=digest_alg) else: return self._message(NameIDMappingRequest, destination, message_id, consent, extensions, sign, name_id_policy=name_id_policy, encrypted_id=encrypted_id, nsprefix=nsprefix, sign_alg=sign_alg, digest_alg=digest_alg) # ======== response handling =========== 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_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 } 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.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 # ------------------------------------------------------------------------ # SubjectQuery, AuthnQuery, RequestedAuthnContext, AttributeQuery, # AuthzDecisionQuery all get Response as response def parse_authz_decision_query_response(self, response, binding=BINDING_SOAP): """ Verify that the response is OK """ kwargs = { "entity_id": self.config.entityid, "attribute_converters": self.config.attribute_converters } return self._parse_response(response, AuthzResponse, "", binding, **kwargs) def parse_authn_query_response(self, response, binding=BINDING_SOAP): """ Verify that the response is OK """ kwargs = { "entity_id": self.config.entityid, "attribute_converters": self.config.attribute_converters } return self._parse_response(response, AuthnQueryResponse, "", binding, **kwargs) def parse_assertion_id_request_response(self, response, binding): """ Verify that the response is OK """ kwargs = { "entity_id": self.config.entityid, "attribute_converters": self.config.attribute_converters } res = self._parse_response(response, AssertionIDResponse, "", binding, **kwargs) return res # ------------------------------------------------------------------------ def parse_attribute_query_response(self, response, binding): kwargs = { "entity_id": self.config.entityid, "attribute_converters": self.config.attribute_converters } return self._parse_response(response, AttributeResponse, "attribute_consuming_service", binding, **kwargs) def parse_name_id_mapping_request_response(self, txt, binding=BINDING_SOAP): """ :param txt: SOAP enveloped SAML message :param binding: Just a placeholder, it's always BINDING_SOAP :return: parsed and verified <NameIDMappingResponse> instance """ return self._parse_response(txt, NameIDMappingResponse, "", binding) # ------------------- ECP ------------------------------------------------ def create_ecp_authn_request(self, entityid=None, relay_state="", sign=False, **kwargs): """ Makes an authentication request. :param entityid: The entity ID of the IdP to send the request to :param relay_state: A token that can be used by the SP to know where to continue the conversation with the client :param sign: Whether the request should be signed or not. :return: SOAP message with the AuthnRequest """ # ---------------------------------------- # <paos:Request> # ---------------------------------------- my_url = self.service_urls(BINDING_PAOS)[0] # must_understand and act according to the standard # paos_request = paos.Request(must_understand="1", actor=ACTOR, response_consumer_url=my_url, service=ECP_SERVICE) # ---------------------------------------- # <ecp:RelayState> # ---------------------------------------- relay_state = ecp.RelayState(actor=ACTOR, must_understand="1", text=relay_state) # ---------------------------------------- # <samlp:AuthnRequest> # ---------------------------------------- try: authn_req = kwargs["authn_req"] try: req_id = authn_req.id except AttributeError: req_id = 0 # Unknown but since it's SOAP it doesn't matter except KeyError: try: _binding = kwargs["binding"] except KeyError: _binding = BINDING_SOAP kwargs["binding"] = _binding logger.debug("entityid: %s, binding: %s", entityid, _binding) # The IDP publishes support for ECP by using the SOAP binding on # SingleSignOnService _, location = self.pick_binding("single_sign_on_service", [_binding], entity_id=entityid) req_id, authn_req = self.create_authn_request( location, service_url_binding=BINDING_PAOS, **kwargs) # ---------------------------------------- # The SOAP envelope # ---------------------------------------- soap_envelope = make_soap_enveloped_saml_thingy( authn_req, [paos_request, relay_state]) return req_id, "%s" % soap_envelope def parse_ecp_authn_response(self, txt, outstanding=None): rdict = soap.class_instances_from_soap_enveloped_saml_thingies( txt, [paos, ecp, samlp]) _relay_state = None for item in rdict["header"]: if item.c_tag == "RelayState" and \ item.c_namespace == ecp.NAMESPACE: _relay_state = item response = self.parse_authn_request_response(rdict["body"], BINDING_PAOS, outstanding) return response, _relay_state @staticmethod def can_handle_ecp_response(response): try: accept = response.headers["accept"] except KeyError: try: accept = response.headers["Accept"] except KeyError: return False if MIME_PAOS in accept: return True else: return False # ---------------------------------------------------------------------- # IDP discovery # ---------------------------------------------------------------------- @staticmethod def create_discovery_service_request(url, entity_id, **kwargs): """ Created the HTTP redirect URL needed to send the user to the discovery service. :param url: The URL of the discovery service :param entity_id: The unique identifier of the service provider :param return: The discovery service MUST redirect the user agent to this location in response to this request :param policy: A parameter name used to indicate the desired behavior controlling the processing of the discovery service :param returnIDParam: A parameter name used to return the unique identifier of the selected identity provider to the original requester. :param isPassive: A boolean value True/False that controls whether the discovery service is allowed to visibly interact with the user agent. :return: A URL """ args = {"entityID": entity_id} for key in ["policy", "returnIDParam"]: try: args[key] = kwargs[key] except KeyError: pass try: args["return"] = kwargs["return_url"] except KeyError: try: args["return"] = kwargs["return"] except KeyError: pass if "isPassive" in kwargs: if kwargs["isPassive"]: args["isPassive"] = "true" else: args["isPassive"] = "false" params = urlencode(args) return "%s?%s" % (url, params) @staticmethod def parse_discovery_service_response(url="", query="", returnIDParam="entityID"): """ Deal with the response url from a Discovery Service :param url: the url the user was redirected back to or :param query: just the query part of the URL. :param returnIDParam: This is where the identifier of the IdP is place if it was specified in the query. Default is 'entityID' :return: The IdP identifier or "" if none was given """ if url: part = urlparse(url) qsd = parse_qs(part[4]) elif query: qsd = parse_qs(query) else: qsd = {} try: return qsd[returnIDParam][0] except KeyError: return ""
class Base(Entity): """ The basic pySAML2 service provider class """ def __init__(self, config=None, identity_cache=None, state_cache=None, virtual_organization="", config_file=""): """ :param config: A saml2.config.Config instance :param identity_cache: Where the class should store identity information :param state_cache: Where the class should keep state information :param virtual_organization: A specific virtual organization """ Entity.__init__(self, "sp", config, config_file, virtual_organization) self.users = Population(identity_cache) self.lock = threading.Lock() # for server state storage if state_cache is None: self.state = {} # in memory storage else: self.state = state_cache self.logout_requests_signed = False self.allow_unsolicited = False self.authn_requests_signed = False self.want_assertions_signed = False self.want_response_signed = False for foo in ["allow_unsolicited", "authn_requests_signed", "logout_requests_signed", "want_assertions_signed", "want_response_signed"]: v = self.config.getattr(foo, "sp") if v is True or v == 'true': setattr(self, foo, True) self.artifact2response = {} # # Private methods # def _relay_state(self, session_id): vals = [session_id, str(int(time.time()))] if self.config.secret is None: vals.append(signature("", vals)) else: vals.append(signature(self.config.secret, vals)) return "|".join(vals) 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 destinations(srvs)[0] 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(eids.keys()[0], binding) return destinations(srvs)[0] except IndexError: raise IdpUnspecified("No IdP to send to given the premises") def _my_name(self): return self.config.name # # Public API # def add_vo_information_about_user(self, name_id): """ Add information to the knowledge I have about the user. This is for Virtual organizations. :param name_id: The subject identifier :return: A possibly extended knowledge. """ ava = {} try: (ava, _) = self.users.get_identity(name_id) except KeyError: pass # is this a Virtual Organization situation if self.vorg: if self.vorg.do_aggregation(name_id): # Get the extended identity ava = self.users.get_identity(name_id)[0] return ava #noinspection PyUnusedLocal def is_session_valid(self, _session_id): """ Place holder. Supposed to check if the session is still valid. """ return True def service_urls(self, binding=BINDING_HTTP_POST): _res = self.config.endpoint("assertion_consumer_service", binding, "sp") if _res: return _res else: return None def create_authn_request(self, destination, vorg="", scoping=None, binding=saml2.BINDING_HTTP_POST, nameid_format=NAMEID_FORMAT_TRANSIENT, service_url_binding=None, message_id=0, consent=None, extensions=None, sign=None, allow_create=False, sign_prepare=False, **kwargs): """ Creates an authentication request. :param destination: Where the request should be sent. :param vorg: The virtual organization the service belongs to. :param scoping: The scope of the request :param binding: The protocol to use for the Response !! :param nameid_format: Format of the NameID :param service_url_binding: Where the reply should be sent dependent on reply binding. :param message_id: The identifier for this request :param consent: Whether the principal have given her consent :param extensions: Possible extensions :param sign: Whether the request should be signed or not. :param sign_prepare: Whether the signature should be prepared or not. :param allow_create: If the identity provider is allowed, in the course of fulfilling the request, to create a new identifier to represent the principal. :param kwargs: Extra key word arguments :return: tuple of request ID and <samlp:AuthnRequest> instance """ client_crt = None if "client_crt" in kwargs: client_crt = kwargs["client_crt"] args = {} try: args["assertion_consumer_service_url"] = kwargs[ "assertion_consumer_service_urls"][0] del kwargs["assertion_consumer_service_urls"] except KeyError: try: args["assertion_consumer_service_url"] = kwargs[ "assertion_consumer_service_url"] del kwargs["assertion_consumer_service_url"] except KeyError: try: args["attribute_consuming_service_index"] = str(kwargs[ "attribute_consuming_service_index"]) del kwargs["attribute_consuming_service_index"] except KeyError: if service_url_binding is None: service_urls = self.service_urls(binding) else: service_urls = self.service_urls(service_url_binding) args["assertion_consumer_service_url"] = service_urls[0] try: args["provider_name"] = kwargs["provider_name"] except KeyError: if binding == BINDING_PAOS: pass else: args["provider_name"] = self._my_name() try: args["name_id_policy"] = kwargs["name_id_policy"] del kwargs["name_id_policy"] except KeyError: if allow_create: allow_create = "true" else: allow_create = "false" # Profile stuff, should be configurable if nameid_format is None: name_id_policy = samlp.NameIDPolicy( allow_create=allow_create, format=NAMEID_FORMAT_TRANSIENT) elif nameid_format == "": name_id_policy = None else: name_id_policy = samlp.NameIDPolicy(allow_create=allow_create, format=nameid_format) if name_id_policy and vorg: try: name_id_policy.sp_name_qualifier = vorg name_id_policy.format = saml.NAMEID_FORMAT_PERSISTENT except KeyError: pass args["name_id_policy"] = name_id_policy if kwargs: _args, extensions = self._filter_args(AuthnRequest(), extensions, **kwargs) args.update(_args) try: del args["id"] except KeyError: pass if sign is None: sign = self.authn_requests_signed if (sign and self.sec.cert_handler.generate_cert()) or \ client_crt is not None: with self.lock: self.sec.cert_handler.update_cert(True, client_crt) if client_crt is not None: sign_prepare = True return self._message(AuthnRequest, destination, message_id, consent, extensions, sign, sign_prepare, protocol_binding=binding, scoping=scoping, **args) return self._message(AuthnRequest, destination, message_id, consent, extensions, sign, sign_prepare, protocol_binding=binding, scoping=scoping, **args) def create_attribute_query(self, destination, name_id=None, attribute=None, message_id=0, consent=None, extensions=None, sign=False, sign_prepare=False, **kwargs): """ Constructs an AttributeQuery :param destination: To whom the query should be sent :param name_id: The identifier of the subject :param attribute: A dictionary of attributes and values that is asked for. The key are one of 4 variants: 3-tuple of name_format,name and friendly_name, 2-tuple of name_format and name, 1-tuple with name or just the name as a string. :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 format: The format of the name ID :param message_id: The identifier of the session :param consent: Whether the principal have given her consent :param extensions: Possible extensions :param sign: Whether the query should be signed or not. :param sign_prepare: Whether the Signature element should be added. :return: Tuple of request ID and an AttributeQuery instance """ if name_id is None: if "subject_id" in kwargs: name_id = saml.NameID(text=kwargs["subject_id"]) for key in ["sp_name_qualifier", "name_qualifier", "format"]: try: setattr(name_id, key, kwargs[key]) except KeyError: pass else: raise AttributeError("Missing required parameter") elif isinstance(name_id, basestring): name_id = saml.NameID(text=name_id) for key in ["sp_name_qualifier", "name_qualifier", "format"]: try: setattr(name_id, key, kwargs[key]) except KeyError: pass subject = saml.Subject(name_id=name_id) if attribute: attribute = do_attributes(attribute) return self._message(AttributeQuery, destination, message_id, consent, extensions, sign, sign_prepare, subject=subject, attribute=attribute) # MUST use SOAP for # AssertionIDRequest, SubjectQuery, # AuthnQuery, AttributeQuery, or AuthzDecisionQuery def create_authz_decision_query(self, destination, action, evidence=None, resource=None, subject=None, message_id=0, consent=None, extensions=None, sign=None, **kwargs): """ Creates an authz decision query. :param destination: The IdP endpoint :param action: The action you want to perform (has to be at least one) :param evidence: Why you should be able to perform the action :param resource: The resource you want to perform the action on :param subject: Who wants to do the thing :param message_id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: AuthzDecisionQuery instance """ return self._message(AuthzDecisionQuery, destination, message_id, consent, extensions, sign, action=action, evidence=evidence, resource=resource, subject=subject) def create_authz_decision_query_using_assertion(self, destination, assertion, action=None, resource=None, subject=None, message_id=0, consent=None, extensions=None, sign=False): """ Makes an authz decision query based on a previously received Assertion. :param destination: The IdP endpoint to send the request to :param assertion: An Assertion instance :param action: The action you want to perform (has to be at least one) :param resource: The resource you want to perform the action on :param subject: Who wants to do the thing :param message_id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: AuthzDecisionQuery instance """ if action: if isinstance(action, basestring): _action = [saml.Action(text=action)] else: _action = [saml.Action(text=a) for a in action] else: _action = None return self.create_authz_decision_query( destination, _action, saml.Evidence(assertion=assertion), resource, subject, message_id=message_id, consent=consent, extensions=extensions, sign=sign) def create_assertion_id_request(self, assertion_id_refs, **kwargs): """ :param assertion_id_refs: :return: One ID ref """ if isinstance(assertion_id_refs, basestring): return 0, assertion_id_refs else: return 0, assertion_id_refs[0] def create_authn_query(self, subject, destination=None, authn_context=None, session_index="", message_id=0, consent=None, extensions=None, sign=False): """ :param subject: The subject its all about as a <Subject> instance :param destination: The IdP endpoint to send the request to :param authn_context: list of <RequestedAuthnContext> instances :param session_index: a specified session index :param message_id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: """ return self._message(AuthnQuery, destination, message_id, consent, extensions, sign, subject=subject, session_index=session_index, requested_authn_context=authn_context) def create_name_id_mapping_request(self, name_id_policy, name_id=None, base_id=None, encrypted_id=None, destination=None, message_id=0, consent=None, extensions=None, sign=False): """ :param name_id_policy: :param name_id: :param base_id: :param encrypted_id: :param destination: :param message_id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: """ # One of them must be present assert name_id or base_id or encrypted_id if name_id: return self._message(NameIDMappingRequest, destination, message_id, consent, extensions, sign, name_id_policy=name_id_policy, name_id=name_id) elif base_id: return self._message(NameIDMappingRequest, destination, message_id, consent, extensions, sign, name_id_policy=name_id_policy, base_id=base_id) else: return self._message(NameIDMappingRequest, destination, message_id, consent, extensions, sign, name_id_policy=name_id_policy, encrypted_id=encrypted_id) # ======== response handling =========== def parse_authn_request_response(self, xmlstr, binding, outstanding=None, outstanding_certs=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. :return: An response.AuthnResponse or None """ try: _ = self.config.entityid except KeyError: raise SAMLError("Missing entity_id specification") resp = None if xmlstr: kwargs = { "outstanding_queries": outstanding, "outstanding_certs": outstanding_certs, "allow_unsolicited": self.allow_unsolicited, "want_assertions_signed": self.want_assertions_signed, "want_response_signed": self.want_response_signed, "return_addrs": self.service_urls(), "entity_id": self.config.entityid, "attribute_converters": self.config.attribute_converters, "allow_unknown_attributes": self.config.allow_unknown_attributes, } try: resp = self._parse_response(xmlstr, AuthnResponse, "assertion_consumer_service", binding, **kwargs) except StatusError, err: logger.error("SAML status error: %s" % err) raise except UnravelError: return None except Exception, exc: logger.error("%s" % exc) raise
class Base(Entity): """ The basic pySAML2 service provider class """ def __init__(self, config=None, identity_cache=None, state_cache=None, virtual_organization="", config_file=""): """ :param config: A saml2.config.Config instance :param identity_cache: Where the class should store identity information :param state_cache: Where the class should keep state information :param virtual_organization: A specific virtual organization """ Entity.__init__(self, "sp", config, config_file, virtual_organization) self.users = Population(identity_cache) # for server state storage if state_cache is None: self.state = {} # in memory storage else: self.state = state_cache for foo in [ "allow_unsolicited", "authn_requests_signed", "logout_requests_signed" ]: if self.config.getattr("sp", foo) == 'true': setattr(self, foo, True) else: setattr(self, foo, False) # extra randomness self.allow_unsolicited = self.config.getattr("allow_unsolicited", "sp") self.artifact2response = {} self.logout_requests_signed = False # # Private methods # def _relay_state(self, session_id): vals = [session_id, str(int(time.time()))] if self.config.secret is None: vals.append(signature("", vals)) else: vals.append(signature(self.config.secret, vals)) return "|".join(vals) 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 destinations(srvs)[0] 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( eids.keys()[0], binding) return destinations(srvs)[0] except IndexError: raise IdpUnspecified("No IdP to send to given the premises") def _my_name(self): return self.config.name # # Public API # def add_vo_information_about_user(self, name_id): """ Add information to the knowledge I have about the user. This is for Virtual organizations. :param name_id: The subject identifier :return: A possibly extended knowledge. """ ava = {} try: (ava, _) = self.users.get_identity(name_id) except KeyError: pass # is this a Virtual Organization situation if self.vorg: if self.vorg.do_aggregation(name_id): # Get the extended identity ava = self.users.get_identity(name_id)[0] return ava #noinspection PyUnusedLocal def is_session_valid(self, _session_id): """ Place holder. Supposed to check if the session is still valid. """ return True def service_url(self, binding=BINDING_HTTP_POST): _res = self.config.endpoint("assertion_consumer_service", binding, "sp") if _res: return _res[0] else: return None def create_authn_request(self, destination, vorg="", scoping=None, binding=saml2.BINDING_HTTP_POST, nameid_format=NAMEID_FORMAT_TRANSIENT, service_url_binding=None, message_id=0, consent=None, extensions=None, sign=None, allow_create=False, sign_prepare=False, **kwargs): """ Creates an authentication request. :param destination: Where the request should be sent. :param vorg: The virtual organization the service belongs to. :param scoping: The scope of the request :param binding: The protocol to use for the Response !! :param nameid_format: Format of the NameID :param service_url_binding: Where the reply should be sent dependent on reply binding. :param message_id: The identifier for this request :param consent: Whether the principal have given her consent :param extensions: Possible extensions :param sign: Whether the request should be signed or not. :param sign_prepare: Whether the signature should be prepared or not. :param allow_create: If the identity provider is allowed, in the course of fulfilling the request, to create a new identifier to represent the principal. :param kwargs: Extra key word arguments :return: <samlp:AuthnRequest> instance """ args = {} try: args["assertion_consumer_service_url"] = kwargs[ "assertion_consumer_service_url"] del kwargs["assertion_consumer_service_url"] except KeyError: try: args["attribute_consuming_service_index"] = str( kwargs["attribute_consuming_service_index"]) del kwargs["attribute_consuming_service_index"] except KeyError: if service_url_binding is None: service_url = self.service_url(binding) else: service_url = self.service_url(service_url_binding) args["assertion_consumer_service_url"] = service_url try: args["provider_name"] = kwargs["provider_name"] except KeyError: if binding == BINDING_PAOS: pass else: args["provider_name"] = self._my_name() try: args["name_id_policy"] = kwargs["name_id_policy"] del kwargs["name_id_policy"] except KeyError: if allow_create: allow_create = "true" else: allow_create = "false" # Profile stuff, should be configurable if nameid_format is None: name_id_policy = samlp.NameIDPolicy( allow_create=allow_create, format=NAMEID_FORMAT_TRANSIENT) elif nameid_format == "": name_id_policy = None else: name_id_policy = samlp.NameIDPolicy(allow_create=allow_create, format=nameid_format) if name_id_policy and vorg: try: name_id_policy.sp_name_qualifier = vorg name_id_policy.format = saml.NAMEID_FORMAT_PERSISTENT except KeyError: pass args["name_id_policy"] = name_id_policy if kwargs: _args, extensions = self._filter_args(AuthnRequest(), extensions, **kwargs) args.update(_args) try: del args["id"] except KeyError: pass return self._message(AuthnRequest, destination, message_id, consent, extensions, sign, sign_prepare, protocol_binding=binding, scoping=scoping, **args) def create_attribute_query(self, destination, name_id=None, attribute=None, message_id=0, consent=None, extensions=None, sign=False, sign_prepare=False, **kwargs): """ Constructs an AttributeQuery :param destination: To whom the query should be sent :param name_id: The identifier of the subject :param attribute: A dictionary of attributes and values that is asked for. The key are one of 4 variants: 3-tuple of name_format,name and friendly_name, 2-tuple of name_format and name, 1-tuple with name or just the name as a string. :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 format: The format of the name ID :param message_id: The identifier of the session :param consent: Whether the principal have given her consent :param extensions: Possible extensions :param sign: Whether the query should be signed or not. :param sign_prepare: Whether the Signature element should be added. :return: An AttributeQuery instance """ if name_id is None: if "subject_id" in kwargs: name_id = saml.NameID(text=kwargs["subject_id"]) for key in ["sp_name_qualifier", "name_qualifier", "format"]: try: setattr(name_id, key, kwargs[key]) except KeyError: pass else: raise AttributeError("Missing required parameter") elif isinstance(name_id, basestring): name_id = saml.NameID(text=name_id) for key in ["sp_name_qualifier", "name_qualifier", "format"]: try: setattr(name_id, key, kwargs[key]) except KeyError: pass subject = saml.Subject(name_id=name_id) if attribute: attribute = do_attributes(attribute) return self._message(AttributeQuery, destination, message_id, consent, extensions, sign, sign_prepare, subject=subject, attribute=attribute) # MUST use SOAP for # AssertionIDRequest, SubjectQuery, # AuthnQuery, AttributeQuery, or AuthzDecisionQuery def create_authz_decision_query(self, destination, action, evidence=None, resource=None, subject=None, message_id=0, consent=None, extensions=None, sign=None, **kwargs): """ Creates an authz decision query. :param destination: The IdP endpoint :param action: The action you want to perform (has to be at least one) :param evidence: Why you should be able to perform the action :param resource: The resource you want to perform the action on :param subject: Who wants to do the thing :param message_id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: AuthzDecisionQuery instance """ return self._message(AuthzDecisionQuery, destination, message_id, consent, extensions, sign, action=action, evidence=evidence, resource=resource, subject=subject) def create_authz_decision_query_using_assertion(self, destination, assertion, action=None, resource=None, subject=None, message_id=0, consent=None, extensions=None, sign=False): """ Makes an authz decision query based on a previously received Assertion. :param destination: The IdP endpoint to send the request to :param assertion: An Assertion instance :param action: The action you want to perform (has to be at least one) :param resource: The resource you want to perform the action on :param subject: Who wants to do the thing :param message_id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: AuthzDecisionQuery instance """ if action: if isinstance(action, basestring): _action = [saml.Action(text=action)] else: _action = [saml.Action(text=a) for a in action] else: _action = None return self.create_authz_decision_query( destination, _action, saml.Evidence(assertion=assertion), resource, subject, message_id=message_id, consent=consent, extensions=extensions, sign=sign) def create_assertion_id_request(self, assertion_id_refs, **kwargs): """ :param assertion_id_refs: :return: One ID ref """ # id_refs = [AssertionIDRef(text=s) for s in assertion_id_refs] # # return self._message(AssertionIDRequest, destination, id, consent, # extensions, sign, assertion_id_ref=id_refs ) if isinstance(assertion_id_refs, basestring): return assertion_id_refs else: return assertion_id_refs[0] def create_authn_query(self, subject, destination=None, authn_context=None, session_index="", message_id=0, consent=None, extensions=None, sign=False): """ :param subject: The subject its all about as a <Subject> instance :param destination: The IdP endpoint to send the request to :param authn_context: list of <RequestedAuthnContext> instances :param session_index: a specified session index :param message_id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: """ return self._message(AuthnQuery, destination, message_id, consent, extensions, sign, subject=subject, session_index=session_index, requested_authn_context=authn_context) def create_name_id_mapping_request(self, name_id_policy, name_id=None, base_id=None, encrypted_id=None, destination=None, message_id=0, consent=None, extensions=None, sign=False): """ :param name_id_policy: :param name_id: :param base_id: :param encrypted_id: :param destination: :param message_id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: """ # One of them must be present assert name_id or base_id or encrypted_id if name_id: return self._message(NameIDMappingRequest, destination, message_id, consent, extensions, sign, name_id_policy=name_id_policy, name_id=name_id) elif base_id: return self._message(NameIDMappingRequest, destination, message_id, consent, extensions, sign, name_id_policy=name_id_policy, base_id=base_id) else: return self._message(NameIDMappingRequest, destination, message_id, consent, extensions, sign, name_id_policy=name_id_policy, encrypted_id=encrypted_id) # ======== response handling =========== def parse_authn_request_response(self, xmlstr, binding, outstanding=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. :return: An response.AuthnResponse or None """ try: _ = self.config.entityid except KeyError: raise SAMLError("Missing entity_id specification") resp = None if xmlstr: kwargs = { "outstanding_queries": outstanding, "allow_unsolicited": self.allow_unsolicited, "return_addr": self.service_url(), "entity_id": self.config.entityid, "attribute_converters": self.config.attribute_converters } try: resp = self._parse_response(xmlstr, AuthnResponse, "assertion_consumer_service", binding, **kwargs) except StatusError, err: logger.error("SAML status error: %s" % err) raise except UnravelError: return None except Exception, exc: logger.error("%s" % exc) raise
class Base(HTTPBase): """ The basic pySAML2 service provider class """ def __init__(self, config=None, identity_cache=None, state_cache=None, virtual_organization="", config_file=""): """ :param config: A saml2.config.Config instance :param identity_cache: Where the class should store identity information :param state_cache: Where the class should keep state information :param virtual_organization: A specific virtual organization """ self.users = Population(identity_cache) # for server state storage if state_cache is None: self.state = {} # in memory storage else: self.state = state_cache if config: self.config = config elif config_file: self.config = config_factory("sp", config_file) else: raise Exception("Missing configuration") HTTPBase.__init__(self, self.config.verify_ssl_cert, self.config.ca_certs, self.config.key_file, self.config.cert_file) if self.config.vorg: for vo in self.config.vorg.values(): vo.sp = self self.metadata = self.config.metadata self.config.setup_logger() # we copy the config.debug variable in an internal # field for convenience and because we may need to # change it during the tests self.debug = self.config.debug self.sec = security_context(self.config) if virtual_organization: if isinstance(virtual_organization, basestring): self.vorg = self.config.vorg[virtual_organization] elif isinstance(virtual_organization, VirtualOrg): self.vorg = virtual_organization else: self.vorg = None for foo in [ "allow_unsolicited", "authn_requests_signed", "logout_requests_signed" ]: if self.config.getattr("sp", foo) == 'true': setattr(self, foo, True) else: setattr(self, foo, False) # extra randomness self.seed = rndstr(32) self.logout_requests_signed_default = True self.allow_unsolicited = self.config.getattr("allow_unsolicited", "sp") # # Private methods # def _relay_state(self, session_id): vals = [session_id, str(int(time.time()))] if self.config.secret is None: vals.append(signature("", vals)) else: vals.append(signature(self.config.secret, vals)) return "|".join(vals) def _issuer(self, entityid=None): """ Return an Issuer instance """ if entityid: if isinstance(entityid, saml.Issuer): return entityid else: return saml.Issuer(text=entityid, format=saml.NAMEID_FORMAT_ENTITY) else: return saml.Issuer(text=self.config.entityid, format=saml.NAMEID_FORMAT_ENTITY) 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 destinations(srvs)[0] 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( eids.keys()[0], binding) return destinations(srvs)[0] except IndexError: raise IdpUnspecified("No IdP to send to given the premises") def _my_name(self): return self.config.name # # Public API # def add_vo_information_about_user(self, subject_id): """ Add information to the knowledge I have about the user. This is for Virtual organizations. :param subject_id: The subject identifier :return: A possibly extended knowledge. """ ava = {} try: (ava, _) = self.users.get_identity(subject_id) except KeyError: pass # is this a Virtual Organization situation if self.vorg: if self.vorg.do_aggregation(subject_id): # Get the extended identity ava = self.users.get_identity(subject_id)[0] return ava #noinspection PyUnusedLocal def is_session_valid(self, _session_id): """ Place holder. Supposed to check if the session is still valid. """ return True def service_url(self, binding=BINDING_HTTP_POST): _res = self.config.endpoint("assertion_consumer_service", binding, "sp") if _res: return _res[0] else: return None def _message(self, request_cls, destination=None, id=0, consent=None, extensions=None, sign=False, **kwargs): """ Some parameters appear in all requests so simplify by doing it in one place :param request_cls: The specific request type :param destination: The recipient :param id: A message identifier :param consent: Whether the principal have given her consent :param extensions: Possible extensions :param kwargs: Key word arguments specific to one request type :return: An instance of the request_cls """ if not id: id = sid(self.seed) req = request_cls(id=id, version=VERSION, issue_instant=instant(), issuer=self._issuer(), **kwargs) if destination: req.destination = destination if consent: req.consent = consent if extensions: req.extensions = extensions if sign: req.signature = pre_signature_part(req.id, self.sec.my_cert, 1) to_sign = [(class_name(req), req.id)] else: to_sign = [] logger.info("REQUEST: %s" % req) return signed_instance_factory(req, self.sec, to_sign) def create_authn_request(self, destination, vorg="", scoping=None, binding=saml2.BINDING_HTTP_POST, nameid_format=NAMEID_FORMAT_TRANSIENT, service_url_binding=None, id=0, consent=None, extensions=None, sign=None, allow_create=False): """ Creates an authentication request. :param destination: Where the request should be sent. :param vorg: The virtual organization the service belongs to. :param scoping: The scope of the request :param binding: The protocol to use for the Response !! :param nameid_format: Format of the NameID :param service_url_binding: Where the reply should be sent dependent on reply binding. :param id: The identifier for this request :param consent: Whether the principal have given her consent :param extensions: Possible extensions :param sign: Whether the request should be signed or not. :param allow_create: If the identity provider is allowed, in the course of fulfilling the request, to create a new identifier to represent the principal. :return: <samlp:AuthnRequest> instance """ if service_url_binding is None: service_url = self.service_url(binding) else: service_url = self.service_url(service_url_binding) if binding == BINDING_PAOS: my_name = None location = None else: my_name = self._my_name() if allow_create: allow_create = "true" else: allow_create = "false" # Profile stuff, should be configurable if nameid_format is None or nameid_format == NAMEID_FORMAT_TRANSIENT: name_id_policy = samlp.NameIDPolicy(allow_create=allow_create, format=NAMEID_FORMAT_TRANSIENT) else: name_id_policy = samlp.NameIDPolicy(allow_create=allow_create, format=nameid_format) if vorg: try: name_id_policy.sp_name_qualifier = vorg name_id_policy.format = saml.NAMEID_FORMAT_PERSISTENT except KeyError: pass return self._message(AuthnRequest, destination, id, consent, extensions, sign, assertion_consumer_service_url=service_url, protocol_binding=binding, name_id_policy=name_id_policy, provider_name=my_name, scoping=scoping) def create_attribute_query(self, destination, subject_id, attribute=None, sp_name_qualifier=None, name_qualifier=None, nameid_format=None, id=0, consent=None, extensions=None, sign=False, **kwargs): """ Constructs an AttributeQuery :param destination: 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. The key are one of 4 variants: 3-tuple of name_format,name and friendly_name, 2-tuple of name_format and name, 1-tuple with name or just the name as a string. :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 id: The identifier of the session :param consent: Whether the principal have given her consent :param extensions: Possible extensions :param sign: Whether the query should be signed or not. :return: An AttributeQuery instance """ subject = saml.Subject( name_id=saml.NameID(text=subject_id, format=nameid_format, sp_name_qualifier=sp_name_qualifier, name_qualifier=name_qualifier)) if attribute: attribute = do_attributes(attribute) return self._message(AttributeQuery, destination, id, consent, extensions, sign, subject=subject, attribute=attribute) def create_logout_request(self, destination, issuer_entity_id, subject_id=None, name_id=None, reason=None, expire=None, id=0, consent=None, extensions=None, sign=False): """ Constructs a LogoutRequest :param destination: Destination of the request :param issuer_entity_id: The entity ID of the IdP the request is target at. :param subject_id: The identifier of the subject :param name_id: A NameID instance identifying the subject :param reason: An indication of the reason for the logout, in the form of a URI reference. :param expire: The time at which the request expires, after which the recipient may discard the message. :param id: Request identifier :param consent: Whether the principal have given her consent :param extensions: Possible extensions :param sign: Whether the query should be signed or not. :return: A LogoutRequest instance """ if subject_id: name_id = saml.NameID(text=self.users.get_entityid( subject_id, issuer_entity_id, False)) if not name_id: raise Exception("Missing subject identification") return self._message(LogoutRequest, destination, id, consent, extensions, sign, name_id=name_id, reason=reason, not_on_or_after=expire) def create_logout_response(self, idp_entity_id, request_id, status_code, binding=BINDING_HTTP_REDIRECT): """ Constructs a LogoutResponse :param idp_entity_id: The entityid of the IdP that want to do the logout :param request_id: The Id of the request we are replying to :param status_code: The status code of the response :param binding: The type of binding that will be used for the response :return: A LogoutResponse instance """ srvs = self.metadata.single_logout_services(idp_entity_id, "idpsso", binding=binding) destination = destinations(srvs)[0] status = samlp.Status(status_code=samlp.StatusCode(value=status_code)) return destination, self._message(LogoutResponse, destination, in_response_to=request_id, status=status) # MUST use SOAP for # AssertionIDRequest, SubjectQuery, # AuthnQuery, AttributeQuery, or AuthzDecisionQuery def create_authz_decision_query(self, destination, action, evidence=None, resource=None, subject=None, id=0, consent=None, extensions=None, sign=None): """ Creates an authz decision query. :param destination: The IdP endpoint :param action: The action you want to perform (has to be at least one) :param evidence: Why you should be able to perform the action :param resource: The resource you want to perform the action on :param subject: Who wants to do the thing :param id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: AuthzDecisionQuery instance """ return self._message(AuthzDecisionQuery, destination, id, consent, extensions, sign, action=action, evidence=evidence, resource=resource, subject=subject) def create_authz_decision_query_using_assertion(self, destination, assertion, action=None, resource=None, subject=None, id=0, consent=None, extensions=None, sign=False): """ Makes an authz decision query. :param destination: The IdP endpoint to send the request to :param assertion: An Assertion instance :param action: The action you want to perform (has to be at least one) :param resource: The resource you want to perform the action on :param subject: Who wants to do the thing :param id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: AuthzDecisionQuery instance """ if action: if isinstance(action, basestring): _action = [saml.Action(text=action)] else: _action = [saml.Action(text=a) for a in action] else: _action = None return self.create_authz_decision_query( destination, _action, saml.Evidence(assertion=assertion), resource, subject, id=id, consent=consent, extensions=extensions, sign=sign) def create_assertion_id_request(self, assertion_id_refs, destination=None, id=0, consent=None, extensions=None, sign=False): """ :param assertion_id_refs: :param destination: The IdP endpoint to send the request to :param id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: AssertionIDRequest instance """ id_refs = [AssertionIDRef(text=s) for s in assertion_id_refs] return self._message(AssertionIDRequest, destination, id, consent, extensions, sign, assertion_id_refs=id_refs) def create_authn_query(self, subject, destination=None, authn_context=None, session_index="", id=0, consent=None, extensions=None, sign=False): """ :param subject: :param destination: The IdP endpoint to send the request to :param authn_context: :param session_index: :param id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: """ return self._message(AuthnQuery, destination, id, consent, extensions, sign, subject=subject, session_index=session_index, requested_auth_context=authn_context) def create_nameid_mapping_request(self, nameid_policy, nameid=None, baseid=None, encryptedid=None, destination=None, id=0, consent=None, extensions=None, sign=False): """ :param nameid_policy: :param nameid: :param baseid: :param encryptedid: :param destination: :param id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: """ # One of them must be present assert nameid or baseid or encryptedid if nameid: return self._message(NameIDMappingRequest, destination, id, consent, extensions, sign, nameid_policy=nameid_policy, nameid=nameid) elif baseid: return self._message(NameIDMappingRequest, destination, id, consent, extensions, sign, nameid_policy=nameid_policy, baseid=baseid) else: return self._message(NameIDMappingRequest, destination, id, consent, extensions, sign, nameid_policy=nameid_policy, encryptedid=encryptedid) def create_manage_nameid_request(self): pass # ======== response handling =========== def _response(self, post, outstanding, decode=True, asynchop=True): """ Deal with an AuthnResponse or LogoutResponse :param post: The reply as a dictionary :param outstanding: A dictionary with session IDs as keys and the original web request from the user before redirection as values. :param decode: Whether the response is Base64 encoded or not :param asynchop: Whether the response was return over a asynchronous connection. SOAP for instance is synchronous :return: An response.AuthnResponse or response.LogoutResponse instance """ # If the request contains a samlResponse, try to validate it try: saml_response = post['SAMLResponse'] except KeyError: return None try: _ = self.config.entityid except KeyError: raise Exception("Missing entity_id specification") reply_addr = self.service_url() resp = None if saml_response: try: resp = response_factory( saml_response, self.config, reply_addr, outstanding, decode=decode, asynchop=asynchop, allow_unsolicited=self.allow_unsolicited) except Exception, exc: logger.error("%s" % exc) return None logger.debug(">> %s", resp) resp = resp.verify() if isinstance(resp, AuthnResponse): self.users.add_information_about_person(resp.session_info()) logger.info("--- ADDED person info ----") else: logger.error("Response type not supported: %s" % (saml2.class_name(resp), )) return resp
def setup_class(self): self.population = Population()
def __init__(self, config=None, identity_cache=None, state_cache=None, virtual_organization="", config_file=""): """ :param config: A saml2.config.Config instance :param identity_cache: Where the class should store identity information :param state_cache: Where the class should keep state information :param virtual_organization: A specific virtual organization """ self.users = Population(identity_cache) # for server state storage if state_cache is None: self.state = {} # in memory storage else: self.state = state_cache if config: self.config = config elif config_file: self.config = config_factory("sp", config_file) else: raise Exception("Missing configuration") HTTPBase.__init__(self, self.config.verify_ssl_cert, self.config.ca_certs, self.config.key_file, self.config.cert_file) if self.config.vorg: for vo in self.config.vorg.values(): vo.sp = self self.metadata = self.config.metadata self.config.setup_logger() # we copy the config.debug variable in an internal # field for convenience and because we may need to # change it during the tests self.debug = self.config.debug self.sec = security_context(self.config) if virtual_organization: if isinstance(virtual_organization, basestring): self.vorg = self.config.vorg[virtual_organization] elif isinstance(virtual_organization, VirtualOrg): self.vorg = virtual_organization else: self.vorg = None for foo in [ "allow_unsolicited", "authn_requests_signed", "logout_requests_signed" ]: if self.config.getattr("sp", foo) == 'true': setattr(self, foo, True) else: setattr(self, foo, False) # extra randomness self.seed = rndstr(32) self.logout_requests_signed_default = True self.allow_unsolicited = self.config.getattr("allow_unsolicited", "sp")
class TestPopulationMemoryBased(): def setup_class(self): self.population = Population() def test_add_person(self): session_info = { "name_id": "123456", "issuer": IDP_ONE, "not_on_or_after": in_a_while(minutes=15), "ava": { "givenName": "Anders", "surName": "Andersson", "mail": "*****@*****.**" } } self.population.add_information_about_person(session_info) issuers = self.population.issuers_of_info("123456") assert issuers == [IDP_ONE] subjects = self.population.subjects() assert subjects == ["123456"] # Are any of the sources gone stale stales = self.population.stale_sources_for_person("123456") assert stales == [] # are any of the possible sources not used or gone stale possible = [IDP_ONE, IDP_OTHER] stales = self.population.stale_sources_for_person("123456", possible) assert stales == [IDP_OTHER] (identity, stale) = self.population.get_identity("123456") assert stale == [] assert identity == { 'mail': '*****@*****.**', 'givenName': 'Anders', 'surName': 'Andersson' } info = self.population.get_info_from("123456", IDP_ONE) assert info.keys() == ["not_on_or_after", "name_id", "ava"] assert info["name_id"] == '123456' assert info["ava"] == { 'mail': '*****@*****.**', 'givenName': 'Anders', 'surName': 'Andersson' } def test_extend_person(self): session_info = { "name_id": "123456", "issuer": IDP_OTHER, "not_on_or_after": in_a_while(minutes=15), "ava": { "eduPersonEntitlement": "Anka" } } self.population.add_information_about_person(session_info) issuers = self.population.issuers_of_info("123456") assert _eq(issuers, [IDP_ONE, IDP_OTHER]) subjects = self.population.subjects() assert subjects == ["123456"] # Are any of the sources gone stale stales = self.population.stale_sources_for_person("123456") assert stales == [] # are any of the possible sources not used or gone stale possible = [IDP_ONE, IDP_OTHER] stales = self.population.stale_sources_for_person("123456", possible) assert stales == [] (identity, stale) = self.population.get_identity("123456") assert stale == [] assert identity == { 'mail': '*****@*****.**', 'givenName': 'Anders', 'surName': 'Andersson', "eduPersonEntitlement": "Anka" } info = self.population.get_info_from("123456", IDP_OTHER) assert info.keys() == ["not_on_or_after", "name_id", "ava"] assert info["name_id"] == '123456' assert info["ava"] == {"eduPersonEntitlement": "Anka"} def test_add_another_person(self): session_info = { "name_id": "abcdef", "issuer": IDP_ONE, "not_on_or_after": in_a_while(minutes=15), "ava": { "givenName": "Bertil", "surName": "Bertilsson", "mail": "*****@*****.**" } } self.population.add_information_about_person(session_info) issuers = self.population.issuers_of_info("abcdef") assert issuers == [IDP_ONE] subjects = self.population.subjects() assert _eq(subjects, ["123456", "abcdef"]) stales = self.population.stale_sources_for_person("abcdef") assert stales == [] # are any of the possible sources not used or gone stale possible = [IDP_ONE, IDP_OTHER] stales = self.population.stale_sources_for_person("abcdef", possible) assert stales == [IDP_OTHER] (identity, stale) = self.population.get_identity("abcdef") assert stale == [] assert identity == { "givenName": "Bertil", "surName": "Bertilsson", "mail": "*****@*****.**" } info = self.population.get_info_from("abcdef", IDP_ONE) assert info.keys() == ["not_on_or_after", "name_id", "ava"] assert info["name_id"] == 'abcdef' assert info["ava"] == { "givenName": "Bertil", "surName": "Bertilsson", "mail": "*****@*****.**" } def test_modify_person(self): session_info = { "name_id": "123456", "issuer": IDP_ONE, "not_on_or_after": in_a_while(minutes=15), "ava": { "givenName": "Arne", "surName": "Andersson", "mail": "*****@*****.**" } } self.population.add_information_about_person(session_info) issuers = self.population.issuers_of_info("123456") assert _eq(issuers, [IDP_ONE, IDP_OTHER]) subjects = self.population.subjects() assert _eq(subjects, ["123456", "abcdef"]) # Are any of the sources gone stale stales = self.population.stale_sources_for_person("123456") assert stales == [] # are any of the possible sources not used or gone stale possible = [IDP_ONE, IDP_OTHER] stales = self.population.stale_sources_for_person("123456", possible) assert stales == [] (identity, stale) = self.population.get_identity("123456") assert stale == [] assert identity == { 'mail': '*****@*****.**', 'givenName': 'Arne', 'surName': 'Andersson', "eduPersonEntitlement": "Anka" } info = self.population.get_info_from("123456", IDP_OTHER) assert info.keys() == ["not_on_or_after", "name_id", "ava"] assert info["name_id"] == '123456' assert info["ava"] == {"eduPersonEntitlement": "Anka"}
class Base(Entity): """ The basic pySAML2 service provider class """ def __init__(self, config=None, identity_cache=None, state_cache=None, virtual_organization="", config_file=""): """ :param config: A saml2.config.Config instance :param identity_cache: Where the class should store identity information :param state_cache: Where the class should keep state information :param virtual_organization: A specific virtual organization """ Entity.__init__(self, "sp", config, config_file, virtual_organization) self.users = Population(identity_cache) self.lock = threading.Lock() # for server state storage if state_cache is None: self.state = {} # in memory storage else: self.state = state_cache self.logout_requests_signed = False self.allow_unsolicited = False self.authn_requests_signed = False self.want_assertions_signed = False self.want_response_signed = False for attribute in ["allow_unsolicited", "authn_requests_signed", "logout_requests_signed", "want_assertions_signed", "want_response_signed"]: v = self.config.getattr(attribute, "sp") if v is True or v == 'true': setattr(self, attribute, True) self.artifact2response = {} # # Private methods # def _relay_state(self, session_id): vals = [session_id, str(int(time.time()))] if self.config.secret is None: vals.append(signature("", vals)) else: vals.append(signature(self.config.secret, vals)) return "|".join(vals) 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 destinations(srvs)[0] 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(next(iter(eids)), binding) return destinations(srvs)[0] except IndexError: raise IdpUnspecified("No IdP to send to given the premises") def _my_name(self): return self.config.name # # Public API # def add_vo_information_about_user(self, name_id): """ Add information to the knowledge I have about the user. This is for Virtual organizations. :param name_id: The subject identifier :return: A possibly extended knowledge. """ ava = {} try: (ava, _) = self.users.get_identity(name_id) except KeyError: pass # is this a Virtual Organization situation if self.vorg: if self.vorg.do_aggregation(name_id): # Get the extended identity ava = self.users.get_identity(name_id)[0] return ava #noinspection PyUnusedLocal def is_session_valid(self, _session_id): """ Place holder. Supposed to check if the session is still valid. """ return True def service_urls(self, binding=BINDING_HTTP_POST): _res = self.config.endpoint("assertion_consumer_service", binding, "sp") if _res: return _res else: return None def create_authn_request(self, destination, vorg="", scoping=None, binding=saml2.BINDING_HTTP_POST, nameid_format=NAMEID_FORMAT_TRANSIENT, service_url_binding=None, message_id=0, consent=None, extensions=None, sign=None, allow_create=False, sign_prepare=False, **kwargs): """ Creates an authentication request. :param destination: Where the request should be sent. :param vorg: The virtual organization the service belongs to. :param scoping: The scope of the request :param binding: The protocol to use for the Response !! :param nameid_format: Format of the NameID :param service_url_binding: Where the reply should be sent dependent on reply binding. :param message_id: The identifier for this request :param consent: Whether the principal have given her consent :param extensions: Possible extensions :param sign: Whether the request should be signed or not. :param sign_prepare: Whether the signature should be prepared or not. :param allow_create: If the identity provider is allowed, in the course of fulfilling the request, to create a new identifier to represent the principal. :param kwargs: Extra key word arguments :return: tuple of request ID and <samlp:AuthnRequest> instance """ client_crt = None if "client_crt" in kwargs: client_crt = kwargs["client_crt"] args = {} try: args["assertion_consumer_service_url"] = kwargs[ "assertion_consumer_service_urls"][0] del kwargs["assertion_consumer_service_urls"] except KeyError: try: args["assertion_consumer_service_url"] = kwargs[ "assertion_consumer_service_url"] del kwargs["assertion_consumer_service_urls"] except KeyError: try: args["attribute_consuming_service_index"] = str(kwargs[ "attribute_consuming_service_index"]) del kwargs["attribute_consuming_service_index"] except KeyError: if service_url_binding is None: service_urls = self.service_urls(binding) else: service_urls = self.service_urls(service_url_binding) args["assertion_consumer_service_url"] = service_urls[0] try: args["provider_name"] = kwargs["provider_name"] except KeyError: if binding == BINDING_PAOS: pass else: args["provider_name"] = self._my_name() try: args["name_id_policy"] = kwargs["name_id_policy"] del kwargs["name_id_policy"] except KeyError: if allow_create: allow_create = "true" else: allow_create = "false" # Profile stuff, should be configurable if nameid_format is None: name_id_policy = samlp.NameIDPolicy( allow_create=allow_create, format=NAMEID_FORMAT_TRANSIENT) elif nameid_format == "": name_id_policy = None else: name_id_policy = samlp.NameIDPolicy(allow_create=allow_create, format=nameid_format) if name_id_policy and vorg: try: name_id_policy.sp_name_qualifier = vorg name_id_policy.format = saml.NAMEID_FORMAT_PERSISTENT except KeyError: pass args["name_id_policy"] = name_id_policy if kwargs: _args, extensions = self._filter_args(AuthnRequest(), extensions, **kwargs) args.update(_args) try: del args["id"] except KeyError: pass if (sign and self.sec.cert_handler.generate_cert()) or \ client_crt is not None: with self.lock: self.sec.cert_handler.update_cert(True, client_crt) if client_crt is not None: sign_prepare = True return self._message(AuthnRequest, destination, message_id, consent, extensions, sign, sign_prepare, protocol_binding=binding, scoping=scoping, **args) return self._message(AuthnRequest, destination, message_id, consent, extensions, sign, sign_prepare, protocol_binding=binding, scoping=scoping, **args) def create_attribute_query(self, destination, name_id=None, attribute=None, message_id=0, consent=None, extensions=None, sign=False, sign_prepare=False, **kwargs): """ Constructs an AttributeQuery :param destination: To whom the query should be sent :param name_id: The identifier of the subject :param attribute: A dictionary of attributes and values that is asked for. The key are one of 4 variants: 3-tuple of name_format,name and friendly_name, 2-tuple of name_format and name, 1-tuple with name or just the name as a string. :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 format: The format of the name ID :param message_id: The identifier of the session :param consent: Whether the principal have given her consent :param extensions: Possible extensions :param sign: Whether the query should be signed or not. :param sign_prepare: Whether the Signature element should be added. :return: Tuple of request ID and an AttributeQuery instance """ if name_id is None: if "subject_id" in kwargs: name_id = saml.NameID(text=kwargs["subject_id"]) for key in ["sp_name_qualifier", "name_qualifier", "format"]: try: setattr(name_id, key, kwargs[key]) except KeyError: pass else: raise AttributeError("Missing required parameter") elif isinstance(name_id, str): name_id = saml.NameID(text=name_id) for key in ["sp_name_qualifier", "name_qualifier", "format"]: try: setattr(name_id, key, kwargs[key]) except KeyError: pass subject = saml.Subject(name_id=name_id) if attribute: attribute = do_attributes(attribute) return self._message(AttributeQuery, destination, message_id, consent, extensions, sign, sign_prepare, subject=subject, attribute=attribute) # MUST use SOAP for # AssertionIDRequest, SubjectQuery, # AuthnQuery, AttributeQuery, or AuthzDecisionQuery def create_authz_decision_query(self, destination, action, evidence=None, resource=None, subject=None, message_id=0, consent=None, extensions=None, sign=None, **kwargs): """ Creates an authz decision query. :param destination: The IdP endpoint :param action: The action you want to perform (has to be at least one) :param evidence: Why you should be able to perform the action :param resource: The resource you want to perform the action on :param subject: Who wants to do the thing :param message_id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: AuthzDecisionQuery instance """ return self._message(AuthzDecisionQuery, destination, message_id, consent, extensions, sign, action=action, evidence=evidence, resource=resource, subject=subject) def create_authz_decision_query_using_assertion(self, destination, assertion, action=None, resource=None, subject=None, message_id=0, consent=None, extensions=None, sign=False): """ Makes an authz decision query based on a previously received Assertion. :param destination: The IdP endpoint to send the request to :param assertion: An Assertion instance :param action: The action you want to perform (has to be at least one) :param resource: The resource you want to perform the action on :param subject: Who wants to do the thing :param message_id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: AuthzDecisionQuery instance """ if action: if isinstance(action, str): _action = [saml.Action(text=action)] else: _action = [saml.Action(text=a) for a in action] else: _action = None return self.create_authz_decision_query( destination, _action, saml.Evidence(assertion=assertion), resource, subject, message_id=message_id, consent=consent, extensions=extensions, sign=sign) def create_assertion_id_request(self, assertion_id_refs, **kwargs): """ :param assertion_id_refs: :return: One ID ref """ if isinstance(assertion_id_refs, str): return 0, assertion_id_refs else: return 0, assertion_id_refs[0] def create_authn_query(self, subject, destination=None, authn_context=None, session_index="", message_id=0, consent=None, extensions=None, sign=False): """ :param subject: The subject its all about as a <Subject> instance :param destination: The IdP endpoint to send the request to :param authn_context: list of <RequestedAuthnContext> instances :param session_index: a specified session index :param message_id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: """ return self._message(AuthnQuery, destination, message_id, consent, extensions, sign, subject=subject, session_index=session_index, requested_authn_context=authn_context) def create_name_id_mapping_request(self, name_id_policy, name_id=None, base_id=None, encrypted_id=None, destination=None, message_id=0, consent=None, extensions=None, sign=False): """ :param name_id_policy: :param name_id: :param base_id: :param encrypted_id: :param destination: :param message_id: Message identifier :param consent: If the principal gave her consent to this request :param extensions: Possible request extensions :param sign: Whether the request should be signed or not. :return: """ # One of them must be present assert name_id or base_id or encrypted_id if name_id: return self._message(NameIDMappingRequest, destination, message_id, consent, extensions, sign, name_id_policy=name_id_policy, name_id=name_id) elif base_id: return self._message(NameIDMappingRequest, destination, message_id, consent, extensions, sign, name_id_policy=name_id_policy, base_id=base_id) else: return self._message(NameIDMappingRequest, destination, message_id, consent, extensions, sign, name_id_policy=name_id_policy, encrypted_id=encrypted_id) # ======== response handling =========== def parse_authn_request_response(self, xmlstr, binding, outstanding=None, outstanding_certs=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. :return: An response.AuthnResponse or None """ try: _ = self.config.entityid except KeyError: raise SAMLError("Missing entity_id specification") resp = None if xmlstr: kwargs = { "outstanding_queries": outstanding, "outstanding_certs": outstanding_certs, "allow_unsolicited": self.allow_unsolicited, "want_assertions_signed": self.want_assertions_signed, "want_response_signed": self.want_response_signed, "return_addrs": self.service_urls(), "entity_id": self.config.entityid, "attribute_converters": self.config.attribute_converters, "allow_unknown_attributes": self.config.allow_unknown_attributes, } 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 exc: logger.error("%s" % exc) raise #logger.debug(">> %s", resp) if resp is None: return None elif isinstance(resp, AuthnResponse): self.users.add_information_about_person(resp.session_info()) logger.info("--- ADDED person info ----") pass else: logger.error("Response type not supported: %s" % ( saml2.class_name(resp),)) return resp # ------------------------------------------------------------------------ # SubjectQuery, AuthnQuery, RequestedAuthnContext, AttributeQuery, # AuthzDecisionQuery all get Response as response def parse_authz_decision_query_response(self, response, binding=BINDING_SOAP): """ Verify that the response is OK """ kwargs = {"entity_id": self.config.entityid, "attribute_converters": self.config.attribute_converters} return self._parse_response(response, AuthzResponse, "", binding, **kwargs) def parse_authn_query_response(self, response, binding=BINDING_SOAP): """ Verify that the response is OK """ kwargs = {"entity_id": self.config.entityid, "attribute_converters": self.config.attribute_converters} return self._parse_response(response, AuthnQueryResponse, "", binding, **kwargs) def parse_assertion_id_request_response(self, response, binding): """ Verify that the response is OK """ kwargs = {"entity_id": self.config.entityid, "attribute_converters": self.config.attribute_converters} res = self._parse_response(response, AssertionIDResponse, "", binding, **kwargs) return res # ------------------------------------------------------------------------ def parse_attribute_query_response(self, response, binding): kwargs = {"entity_id": self.config.entityid, "attribute_converters": self.config.attribute_converters} return self._parse_response(response, AttributeResponse, "attribute_consuming_service", binding, **kwargs) def parse_name_id_mapping_request_response(self, txt, binding=BINDING_SOAP): """ :param txt: SOAP enveloped SAML message :param binding: Just a placeholder, it's always BINDING_SOAP :return: parsed and verified <NameIDMappingResponse> instance """ return self._parse_response(txt, NameIDMappingResponse, "", binding) # ------------------- ECP ------------------------------------------------ def create_ecp_authn_request(self, entityid=None, relay_state="", sign=False, **kwargs): """ Makes an authentication request. :param entityid: The entity ID of the IdP to send the request to :param relay_state: A token that can be used by the SP to know where to continue the conversation with the client :param sign: Whether the request should be signed or not. :return: SOAP message with the AuthnRequest """ # ---------------------------------------- # <paos:Request> # ---------------------------------------- my_url = self.service_urls(BINDING_PAOS)[0] # must_understand and act according to the standard # paos_request = paos.Request(must_understand="1", actor=ACTOR, response_consumer_url=my_url, service=ECP_SERVICE) # ---------------------------------------- # <ecp:RelayState> # ---------------------------------------- relay_state = ecp.RelayState(actor=ACTOR, must_understand="1", text=relay_state) # ---------------------------------------- # <samlp:AuthnRequest> # ---------------------------------------- try: authn_req = kwargs["authn_req"] try: req_id = authn_req.id except AttributeError: req_id = 0 # Unknown but since it's SOAP it doesn't matter except KeyError: try: _binding = kwargs["binding"] except KeyError: _binding = BINDING_SOAP kwargs["binding"] = _binding logger.debug("entityid: %s, binding: %s" % (entityid, _binding)) # The IDP publishes support for ECP by using the SOAP binding on # SingleSignOnService _, location = self.pick_binding("single_sign_on_service", [_binding], entity_id=entityid) req_id, authn_req = self.create_authn_request( location, service_url_binding=BINDING_PAOS, **kwargs) # ---------------------------------------- # The SOAP envelope # ---------------------------------------- soap_envelope = make_soap_enveloped_saml_thingy(authn_req, [paos_request, relay_state]) return req_id, "%s" % soap_envelope def parse_ecp_authn_response(self, txt, outstanding=None): rdict = soap.class_instances_from_soap_enveloped_saml_thingies(txt, [paos, ecp, samlp]) _relay_state = None for item in rdict["header"]: if item.c_tag == "RelayState" and\ item.c_namespace == ecp.NAMESPACE: _relay_state = item response = self.parse_authn_request_response(rdict["body"], BINDING_PAOS, outstanding) return response, _relay_state @staticmethod def can_handle_ecp_response(response): try: accept = response.headers["accept"] except KeyError: try: accept = response.headers["Accept"] except KeyError: return False if MIME_PAOS in accept: return True else: return False # ---------------------------------------------------------------------- # IDP discovery # ---------------------------------------------------------------------- @staticmethod def create_discovery_service_request(url, entity_id, **kwargs): """ Created the HTTP redirect URL needed to send the user to the discovery service. :param url: The URL of the discovery service :param entity_id: The unique identifier of the service provider :param return: The discovery service MUST redirect the user agent to this location in response to this request :param policy: A parameter name used to indicate the desired behavior controlling the processing of the discovery service :param returnIDParam: A parameter name used to return the unique identifier of the selected identity provider to the original requester. :param isPassive: A boolean value True/False that controls whether the discovery service is allowed to visibly interact with the user agent. :return: A URL """ args = {"entityID": entity_id} for key in ["policy", "returnIDParam"]: try: args[key] = kwargs[key] except KeyError: pass try: args["return"] = kwargs["return_url"] except KeyError: try: args["return"] = kwargs["return"] except KeyError: pass if "isPassive" in kwargs: if kwargs["isPassive"]: args["isPassive"] = "true" else: args["isPassive"] = "false" params = urlencode(args) return "%s?%s" % (url, params) @staticmethod def parse_discovery_service_response(url="", query="", returnIDParam="entityID"): """ Deal with the response url from a Discovery Service :param url: the url the user was redirected back to or :param query: just the query part of the URL. :param returnIDParam: This is where the identifier of the IdP is place if it was specified in the query. Default is 'entityID' :return: The IdP identifier or "" if none was given """ if url: part = urlparse(url) qsd = parse_qs(part[4]) elif query: qsd = parse_qs(query) else: qsd = {} try: return qsd[returnIDParam][0] except KeyError: return ""
class Saml2Client(object): """ The basic pySAML2 service provider class """ def __init__(self, config=None, identity_cache=None, state_cache=None, virtual_organization=None, config_file="", logger=None): """ :param config: A saml2.config.Config instance :param identity_cache: Where the class should store identity information :param state_cache: Where the class should keep state information :param virtual_organization: Which if any virtual organization this SP belongs to """ self.users = Population(identity_cache) # for server state storage if state_cache is None: self.state = {} # in memory storage else: self.state = state_cache if config: self.config = config elif config_file: self.config = config_factory("sp", config_file) else: raise Exception("Missing configuration") self.metadata = self.config.metadata if logger is None: self.logger = self.config.setup_logger() else: self.logger = logger # we copy the config.debug variable in an internal # field for convenience and because we may need to # change it during the tests self.debug = self.config.debug self.sec = security_context(self.config, log=self.logger, debug=self.debug) if virtual_organization: self.vorg = VirtualOrg(self, virtual_organization) else: self.vorg = None if "allow_unsolicited" in self.config: self.allow_unsolicited = self.config.allow_unsolicited else: self.allow_unsolicited = False if getattr(self.config, 'authn_requests_signed', 'false') == 'true': self.authn_requests_signed_default = True else: self.authn_requests_signed_default = False if getattr(self.config, 'logout_requests_signed', 'false') == 'true': self.logout_requests_signed_default = True else: self.logout_requests_signed_default = False # # Private methods # def _relay_state(self, session_id): vals = [session_id, str(int(time.time()))] if self.config.secret is None: vals.append(signature("", vals)) else: vals.append(signature(self.config.secret, vals)) return "|".join(vals) def _issuer(self, entityid=None): """ Return an Issuer instance """ if entityid: if isinstance(entityid, saml.Issuer): return entityid else: return saml.Issuer(text=entityid, format=saml.NAMEID_FORMAT_ENTITY) else: return saml.Issuer(text=self.config.entityid, format=saml.NAMEID_FORMAT_ENTITY) def _sso_location(self, entityid=None, binding=BINDING_HTTP_REDIRECT): if entityid: # verify that it's in the metadata try: return self.config.single_sign_on_services(entityid, binding)[0] except IndexError: if self.logger: self.logger.info("_sso_location: %s, %s" % (entityid, binding)) return IdpUnspecified("No IdP to send to given the premises") # get the idp location from the configuration alternative the # metadata. If there is more than one IdP in the configuration # raise exception eids = self.config.idps() if len(eids) > 1: raise IdpUnspecified("Too many IdPs to choose from: %s" % eids) try: loc = self.config.single_sign_on_services(eids.keys()[0], binding)[0] return loc except IndexError: return IdpUnspecified("No IdP to send to given the premises") def _my_name(self): return self.config.name # # Public API # def service_url(self, binding=BINDING_HTTP_POST): _res = self.config.endpoint("assertion_consumer_service", binding) if _res: return _res[0] else: return None def response(self, post, outstanding, log=None, decode=True, asynchop=True): """ Deal with an AuthnResponse or LogoutResponse :param post: The reply as a dictionary :param outstanding: A dictionary with session IDs as keys and the original web request from the user before redirection as values. :param log: where loggin should go. :param decode: Whether the response is Base64 encoded or not :param asynchop: Whether the response was return over a asynchronous connection. SOAP for instance is synchronous :return: An response.AuthnResponse or response.LogoutResponse instance """ # If the request contains a samlResponse, try to validate it try: saml_response = post['SAMLResponse'] except KeyError: return None try: _ = self.config.entityid except KeyError: raise Exception("Missing entity_id specification") if log is None: log = self.logger reply_addr = self.service_url() resp = None if saml_response: try: resp = response_factory(saml_response, self.config, reply_addr, outstanding, log, debug=self.debug, decode=decode, asynchop=asynchop, allow_unsolicited=self.allow_unsolicited) except Exception, exc: if log: log.error("%s" % exc) return None if log: log.debug(">> %s", resp) resp = resp.verify() if resp is None: log.error("Response could not be verified") return if isinstance(resp, AuthnResponse): self.users.add_information_about_person(resp.session_info()) if log: log.info("--- ADDED person info ----") elif isinstance(resp, LogoutResponse): self.handle_logout_response(resp, log) elif log: log.error("Response type not supported: %s" % saml2.class_name(resp)) return resp
def __init__( self, config=None, debug=0, identity_cache=None, state_cache=None, virtual_organization=None, config_file="", logger=None, ): """ :param config: A saml2.config.Config instance :param debug: Whether debugging should be done even if the configuration says otherwise :param identity_cache: Where the class should store identity information :param state_cache: Where the class should keep state information :param virtual_organization: Which if any virtual organization this SP belongs to """ self.users = Population(identity_cache) # for server state storage if state_cache is None: self.state = {} # in memory storage else: self.state = state_cache self.sec = None if config: self.config = config elif config_file: self.config = config_factory("sp", config_file) else: raise Exception("Missing configuration") self.metadata = self.config.metadata if logger is None: self.logger = self.config.setup_logger() else: self.logger = logger if not debug and self.config: self.debug = self.config.debug else: self.debug = debug self.sec = security_context(self.config, log=self.logger, debug=self.debug) if virtual_organization: self.vorg = VirtualOrg(self, virtual_organization) else: self.vorg = None if "allow_unsolicited" in self.config: self.allow_unsolicited = self.config.allow_unsolicited else: self.allow_unsolicited = False if "verify_signatures" in self.config: self.verify_signatures = self.config.verify_signatures else: self.verify_signatures = True if getattr(self.config, "authn_requests_signed", "false") == "true": self.authn_requests_signed_default = True else: self.authn_requests_signed_default = False if getattr(self.config, "logout_requests_signed", "false") == "true": self.logout_requests_signed_default = True else: self.logout_requests_signed_default = False
class TestPopulationMemoryBased(): def setup_class(self): self.population = Population() def test_add_person(self): session_info = { "name_id": nid, "issuer": IDP_ONE, "not_on_or_after": in_a_while(minutes=15), "ava": { "givenName": "Anders", "surName": "Andersson", "mail": "*****@*****.**" } } self.population.add_information_about_person(session_info) issuers = self.population.issuers_of_info(nid) assert list(issuers) == [IDP_ONE] subjects = [code(c) for c in self.population.subjects()] assert subjects == [cnid] # Are any of the sources gone stale stales = self.population.stale_sources_for_person(nid) assert stales == [] # are any of the possible sources not used or gone stale possible = [IDP_ONE, IDP_OTHER] stales = self.population.stale_sources_for_person(nid, possible) assert stales == [IDP_OTHER] (identity, stale) = self.population.get_identity(nid) assert stale == [] assert identity == {'mail': '*****@*****.**', 'givenName': 'Anders', 'surName': 'Andersson'} info = self.population.get_info_from(nid, IDP_ONE) assert sorted(list(info.keys())) == sorted(["not_on_or_after", "name_id", "ava"]) assert info["name_id"] == nid assert info["ava"] == {'mail': '*****@*****.**', 'givenName': 'Anders', 'surName': 'Andersson'} def test_extend_person(self): session_info = { "name_id": nid, "issuer": IDP_OTHER, "not_on_or_after": in_a_while(minutes=15), "ava": { "eduPersonEntitlement": "Anka" } } self.population.add_information_about_person(session_info) issuers = self.population.issuers_of_info(nid) assert _eq(issuers, [IDP_ONE, IDP_OTHER]) subjects = [code(c) for c in self.population.subjects()] assert subjects == [cnid] # Are any of the sources gone stale stales = self.population.stale_sources_for_person(nid) assert stales == [] # are any of the possible sources not used or gone stale possible = [IDP_ONE, IDP_OTHER] stales = self.population.stale_sources_for_person(nid, possible) assert stales == [] (identity, stale) = self.population.get_identity(nid) assert stale == [] assert identity == {'mail': '*****@*****.**', 'givenName': 'Anders', 'surName': 'Andersson', "eduPersonEntitlement": "Anka"} info = self.population.get_info_from(nid, IDP_OTHER) assert sorted(list(info.keys())) == sorted(["not_on_or_after", "name_id", "ava"]) assert info["name_id"] == nid assert info["ava"] == {"eduPersonEntitlement": "Anka"} def test_add_another_person(self): session_info = { "name_id": nida, "issuer": IDP_ONE, "not_on_or_after": in_a_while(minutes=15), "ava": { "givenName": "Bertil", "surName": "Bertilsson", "mail": "*****@*****.**" } } self.population.add_information_about_person(session_info) issuers = self.population.issuers_of_info(nida) assert list(issuers) == [IDP_ONE] subjects = [code(c) for c in self.population.subjects()] assert _eq(subjects, [cnid, cnida]) stales = self.population.stale_sources_for_person(nida) assert stales == [] # are any of the possible sources not used or gone stale possible = [IDP_ONE, IDP_OTHER] stales = self.population.stale_sources_for_person(nida, possible) assert stales == [IDP_OTHER] (identity, stale) = self.population.get_identity(nida) assert stale == [] assert identity == {"givenName": "Bertil", "surName": "Bertilsson", "mail": "*****@*****.**" } info = self.population.get_info_from(nida, IDP_ONE) assert sorted(list(info.keys())) == sorted(["not_on_or_after", "name_id", "ava"]) assert info["name_id"] == nida assert info["ava"] == {"givenName": "Bertil", "surName": "Bertilsson", "mail": "*****@*****.**" } def test_modify_person(self): session_info = { "name_id": nid, "issuer": IDP_ONE, "not_on_or_after": in_a_while(minutes=15), "ava": { "givenName": "Arne", "surName": "Andersson", "mail": "*****@*****.**" } } self.population.add_information_about_person(session_info) issuers = self.population.issuers_of_info(nid) assert _eq(issuers, [IDP_ONE, IDP_OTHER]) subjects = [code(c) for c in self.population.subjects()] assert _eq(subjects, [cnid, cnida]) # Are any of the sources gone stale stales = self.population.stale_sources_for_person(nid) assert stales == [] # are any of the possible sources not used or gone stale possible = [IDP_ONE, IDP_OTHER] stales = self.population.stale_sources_for_person(nid, possible) assert stales == [] (identity, stale) = self.population.get_identity(nid) assert stale == [] assert identity == {'mail': '*****@*****.**', 'givenName': 'Arne', 'surName': 'Andersson', "eduPersonEntitlement": "Anka"} info = self.population.get_info_from(nid, IDP_OTHER) assert sorted(list(info.keys())) == sorted(["not_on_or_after", "name_id", "ava"]) assert info["name_id"] == nid assert info["ava"] == {"eduPersonEntitlement": "Anka"}
class Saml2Client(object): """ The basic pySAML2 service provider class """ def __init__(self, config=None, identity_cache=None, state_cache=None, virtual_organization=None, config_file=""): """ :param config: A saml2.config.Config instance :param identity_cache: Where the class should store identity information :param state_cache: Where the class should keep state information :param virtual_organization: Which if any virtual organization this SP belongs to """ self.users = Population(identity_cache) # for server state storage if state_cache is None: self.state = {} # in memory storage else: self.state = state_cache if config: self.config = config elif config_file: self.config = config_factory("sp", config_file) else: raise Exception("Missing configuration") self.metadata = self.config.metadata self.config.setup_logger() # we copy the config.debug variable in an internal # field for convenience and because we may need to # change it during the tests self.debug = self.config.debug self.sec = security_context(self.config) if virtual_organization: self.vorg = VirtualOrg(self, virtual_organization) else: self.vorg = None if "allow_unsolicited" in self.config: self.allow_unsolicited = self.config.allow_unsolicited else: self.allow_unsolicited = False if getattr(self.config, 'authn_requests_signed', 'false') == 'true': self.authn_requests_signed_default = True else: self.authn_requests_signed_default = False if getattr(self.config, 'logout_requests_signed', 'false') == 'true': self.logout_requests_signed_default = True else: self.logout_requests_signed_default = False # # Private methods # def _relay_state(self, session_id): vals = [session_id, str(int(time.time()))] if self.config.secret is None: vals.append(signature("", vals)) else: vals.append(signature(self.config.secret, vals)) return "|".join(vals) def _issuer(self, entityid=None): """ Return an Issuer instance """ if entityid: if isinstance(entityid, saml.Issuer): return entityid else: return saml.Issuer(text=entityid, format=saml.NAMEID_FORMAT_ENTITY) else: return saml.Issuer(text=self.config.entityid, format=saml.NAMEID_FORMAT_ENTITY) def _sso_location(self, entityid=None, binding=BINDING_HTTP_REDIRECT): if entityid: # verify that it's in the metadata try: return self.config.single_sign_on_services(entityid, binding)[0] except IndexError: logger.info("_sso_location: %s, %s" % (entityid, binding)) raise IdpUnspecified("No IdP to send to given the premises") # get the idp location from the configuration alternative the # metadata. If there is more than one IdP in the configuration # raise exception eids = self.config.idps() if len(eids) > 1: raise IdpUnspecified("Too many IdPs to choose from: %s" % eids) try: loc = self.config.single_sign_on_services(eids.keys()[0], binding)[0] return loc except IndexError: raise IdpUnspecified("No IdP to send to given the premises") def _my_name(self): return self.config.name # # Public API # def service_url(self, binding=BINDING_HTTP_POST): _res = self.config.endpoint("assertion_consumer_service", binding) if _res: return _res[0] else: return None def response(self, post, outstanding, decode=True, asynchop=True): """ Deal with an AuthnResponse or LogoutResponse :param post: The reply as a dictionary :param outstanding: A dictionary with session IDs as keys and the original web request from the user before redirection as values. :param decode: Whether the response is Base64 encoded or not :param asynchop: Whether the response was return over a asynchronous connection. SOAP for instance is synchronous :return: An response.AuthnResponse or response.LogoutResponse instance """ # If the request contains a samlResponse, try to validate it try: saml_response = post['SAMLResponse'] except KeyError: return None try: _ = self.config.entityid except KeyError: raise Exception("Missing entity_id specification") reply_addr = self.service_url() resp = None if saml_response: try: resp = response_factory( saml_response, self.config, reply_addr, outstanding, decode=decode, asynchop=asynchop, allow_unsolicited=self.allow_unsolicited) except Exception, exc: logger.error("%s" % exc) return None logger.debug(">> %s", resp) resp = resp.verify() if isinstance(resp, AuthnResponse): self.users.add_information_about_person(resp.session_info()) logger.info("--- ADDED person info ----") elif isinstance(resp, LogoutResponse): self.handle_logout_response(resp) else: logger.error("Response type not supported: %s" % (saml2.class_name(resp), )) return resp