def test_wayf(): c = SPConfig().load_file("server_conf") c.context = "sp" idps = c.metadata.with_descriptor("idpsso") ent = idps.values()[0] assert name(ent) == 'Example Co.' assert name(ent, "se") == 'Exempel AB' c.setup_logger() assert root_logger.level != logging.NOTSET assert root_logger.level == logging.INFO assert len(root_logger.handlers) == 1 assert isinstance(root_logger.handlers[0], logging.handlers.RotatingFileHandler) handler = root_logger.handlers[0] assert handler.backupCount == 5 try: assert handler.maxBytes == 100000 except AssertionError: assert handler.maxBytes == 500000 assert handler.mode == "a" assert root_logger.name == "saml2" assert root_logger.level == 20
def get_saml2_config(module_path): module = imp.load_source('saml2_settings', module_path) conf = SPConfig() conf.load(module.SAML_CONFIG) return conf
def test(): with closing(Server(config_file=dotname("idp_all_conf"))) as idp: conf = SPConfig() conf.load_file(dotname("servera_conf")) sp = Saml2Client(conf) srvs = sp.metadata.single_sign_on_service(idp.config.entityid, BINDING_HTTP_REDIRECT) destination = srvs[0]["location"] req_id, req = sp.create_authn_request(destination, id="id1") try: key = sp.sec.key except AttributeError: key = import_rsa_key_from_file(sp.sec.key_file) info = http_redirect_message(req, destination, relay_state="RS", typ="SAMLRequest", sigalg=SIG_RSA_SHA1, key=key) verified_ok = False for param, val in info["headers"]: if param == "Location": _dict = parse_qs(val.split("?")[1]) _certs = idp.metadata.certs(sp.config.entityid, "any", "signing") for cert in _certs: if verify_redirect_signature(_dict, cert): verified_ok = True assert verified_ok
def test_conf_syslog(): c = SPConfig().load_file("server_conf_syslog") c.context = "sp" # otherwise the logger setting is not changed root_logger.level = logging.NOTSET root_logger.handlers = [] print c.logger c.setup_logger() assert root_logger.level != logging.NOTSET assert root_logger.level == logging.INFO assert len(root_logger.handlers) == 1 assert isinstance(root_logger.handlers[0], logging.handlers.SysLogHandler) handler = root_logger.handlers[0] print handler.__dict__ assert handler.facility == "local3" assert handler.address == ('localhost', 514) if sys.version >= (2, 7): assert handler.socktype == 2 else: pass assert root_logger.name == "saml2" assert root_logger.level == 20
def test_conf_syslog(): c = SPConfig().load_file("server_conf_syslog") c.context = "sp" # otherwise the logger setting is not changed root_logger.level = logging.NOTSET while root_logger.handlers: handler = root_logger.handlers[-1] root_logger.removeHandler(handler) handler.flush() handler.close() print(c.logger) c.setup_logger() assert root_logger.level != logging.NOTSET assert root_logger.level == logging.INFO assert len(root_logger.handlers) == 1 assert isinstance(root_logger.handlers[0], logging.handlers.SysLogHandler) handler = root_logger.handlers[0] print(handler.__dict__) assert handler.facility == "local3" assert handler.address == ('localhost', 514) if ((sys.version_info.major == 2 and sys.version_info.minor >= 7) or sys.version_info.major > 2): assert handler.socktype == 2 else: pass assert root_logger.name == "saml2" assert root_logger.level == 20
def setup_class(self): self.server = FakeIDP("idp_all_conf") conf = SPConfig() conf.load_file("servera_conf") self.client = Saml2Client(conf) self.client.send = self.server.receive
def config_settings_loader(request=None): """Utility function to load the pysaml2 configuration. This is also the default config loader. """ conf = SPConfig() conf.load(copy.deepcopy(settings.SAML_CONFIG)) return conf
def make_entity(sp, **kw_args): try: conf = SPConfig().load(kw_args["spconf"][sp]) except KeyError: logging.warning("known SP configs: {}".format(kw_args["spconf"].keys())) raise conf.metadata = kw_args['metadata'] return Saml2Client(config=conf)
def create_logout_request(subject_id, destination, issuer_entity_id, req_entity_id, sign=True): config = SPConfig() config.load(sp_config) sp_client = Saml2Client(config=config) # construct a request logout_request = samlp.LogoutRequest( id='a123456', version=VERSION, destination=destination, issuer=saml.Issuer(text=req_entity_id, format=saml.NAMEID_FORMAT_ENTITY), name_id=saml.NameID(text=subject_id)) return logout_request
def test_2(): c = SPConfig().load(sp2) c.context = "sp" print c assert c.endpoints assert c.idp assert c.optional_attributes assert c.name assert c.required_attributes assert len(c.idp) == 1 assert c.idp.keys() == [""] assert c.idp.values() == ["https://example.com/saml2/idp/SSOService.php"] assert c.only_use_keys_in_metadata is None
def _saml2_config(self): if self._v_config is None: sp_config = self._saml2_config_template() sp_config['metadata']['local'] = [self.saml2_idp_configfile] sp_config['entityid'] = self.saml2_sp_entityid sp_config['service']['sp']['name'] = self.saml2_sp_entityid sp_config['service']['sp']['url'] = self.saml2_sp_url sp_config['service']['sp']['endpoints']['assertion_consumer_service'] = [self.saml2_sp_url,] sp_config['service']['sp']['endpoints']['single_logout_service'] = ['%s/logout' % self.saml2_sp_url, BINDING_HTTP_REDIRECT] sp_config['service']['sp']['url'] = self.saml2_sp_url sp_config['xmlsec_binary'] = self.saml2_xmlsec config = SPConfig() conf=sp_config.copy() config.load(conf) self._v_config = config return self._v_config
def test_2(): c = SPConfig().load(sp2) c.context = "sp" print(c) assert c._sp_endpoints assert c.getattr("endpoints", "sp") assert c._sp_idp assert c._sp_optional_attributes assert c.name assert c._sp_required_attributes assert len(c._sp_idp) == 1 assert list(c._sp_idp.keys()) == [""] assert list(c._sp_idp.values()) == ["https://example.com/saml2/idp/SSOService.php"] assert c.only_use_keys_in_metadata is True
def test_minimum(): minimum = { "entityid": "urn:mace:example.com:saml:roland:sp", "service": { "sp": { "endpoints": {"assertion_consumer_service": ["http://sp.example.org/"]}, "name": "test", "idp": {"": "https://example.com/idp/SSOService.php"}, } }, # "xmlsec_binary" : "/usr/local/bin/xmlsec1", } c = SPConfig().load(minimum) c.context = "sp" assert c is not None
def test_1(): c = SPConfig().load(sp1) c.context = "sp" print c assert c._sp_endpoints assert c._sp_name assert c._sp_idp md = c.metadata assert isinstance(md, MetaData) assert len(c._sp_idp) == 1 assert c._sp_idp.keys() == ["urn:mace:example.com:saml:roland:idp"] assert c._sp_idp.values() == [{'single_sign_on_service': {'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect': 'http://localhost:8088/sso/'}}] assert c.only_use_keys_in_metadata
def get_auth_response(self, samlfrontend, context, internal_response, sp_conf, idp_metadata_str): sp_config = SPConfig().load(sp_conf, metadata_construction=False) resp_args = { "name_id_policy": NameIDPolicy(format=NAMEID_FORMAT_TRANSIENT), "in_response_to": None, "destination": sp_config.endpoint("assertion_consumer_service", binding=BINDING_HTTP_REDIRECT)[0], "sp_entity_id": sp_conf["entityid"], "binding": BINDING_HTTP_REDIRECT } request_state = samlfrontend._create_state_data(context, resp_args, "") context.state[samlfrontend.name] = request_state resp = samlfrontend.handle_authn_response(context, internal_response) sp_conf["metadata"]["inline"].append(idp_metadata_str) fakesp = FakeSP(sp_config) resp_dict = parse_qs(urlparse(resp.message).query) return fakesp.parse_authn_request_response(resp_dict["SAMLResponse"][0], BINDING_HTTP_REDIRECT)
def _saml2_config(self): if self._v_config is None: sp_config = self._saml2_config_template() sp_config["metadata"]["local"] = [self.saml2_idp_configfile] sp_config["entityid"] = self.saml2_sp_entityid sp_config["service"]["sp"]["name"] = self.saml2_sp_entityid sp_config["service"]["sp"]["url"] = self.saml2_sp_url sp_config["service"]["sp"]["endpoints"]["assertion_consumer_service"] = [self.saml2_sp_url] sp_config["service"]["sp"]["endpoints"]["single_logout_service"] = [ "%s/logout" % self.saml2_sp_url, BINDING_HTTP_REDIRECT, ] sp_config["service"]["sp"]["url"] = self.saml2_sp_url sp_config["xmlsec_binary"] = self.saml2_xmlsec config = SPConfig() conf = sp_config.copy() config.load(conf) self._v_config = config return self._v_config
def __init__(self, outgoing, internal_attributes, config, base_url, name): """ :type outgoing: (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response :type internal_attributes: dict[str, dict[str, list[str] | str]] :type config: dict[str, Any] :type base_url: str :type name: str :param outgoing: Callback should be called by the module after the authorization in the backend is done. :param internal_attributes: Internal attribute map :param config: The module config :param base_url: base url of the service :param name: name of the plugin """ super().__init__(outgoing, internal_attributes, base_url, name) self.config = self.init_config(config) sp_config = SPConfig().load(copy.deepcopy(config[self.KEY_SP_CONFIG]), False) self.sp = Base(sp_config) self.discosrv = config.get(self.KEY_DISCO_SRV) self.encryption_keys = [] self.outstanding_queries = {} self.idp_blacklist_file = config.get('idp_blacklist_file', None) sp_keypairs = sp_config.getattr('encryption_keypairs', '') sp_key_file = sp_config.getattr('key_file', '') if sp_keypairs: key_file_paths = [pair['key_file'] for pair in sp_keypairs] elif sp_key_file: key_file_paths = [sp_key_file] else: key_file_paths = [] for p in key_file_paths: with open(p) as key_file: self.encryption_keys.append(key_file.read())
def test_sp(): cnf = SPConfig() cnf.load_file("sp_1_conf") assert cnf.single_logout_services("urn:mace:example.com:saml:roland:idp", BINDING_HTTP_POST) == ["http://localhost:8088/slo"] assert cnf.endpoint("assertion_consumer_service") == \ ["http://lingon.catalogix.se:8087/"] assert len(cnf.idps()) == 1
def test_ecp(): cnf = SPConfig() cnf.load(ECP_SP) assert cnf.endpoint("assertion_consumer_service") == ["http://lingon.catalogix.se:8087/"] eid = cnf.ecp_endpoint("130.239.16.3") assert eid == "http://example.com/idp" eid = cnf.ecp_endpoint("130.238.20.20") assert eid is None
def __init__(self, config): """Initialize SAML Service Provider. Args: config (dict): Service Provider config info in dict form """ if config.get('metadata') is not None: config['metadata'] = _parse_metadata_dict_to_inline( config['metadata']) self._config = SPConfig().load(config) self._config.setattr('', 'allow_unknown_attributes', True) # Set discovery end point, if configured for. if config['service']['sp'].get('ds'): self.discovery_service_end_point = \ config['service']['sp'].get('ds')[0]
def __init__(self, config, attribute_map=None): """Initialize SAML Service Provider. Args: config (dict): Service Provider config info in dict form attribute_map (dict): Mapping of attribute keys to user data """ self._config = SPConfig() self._config.load(config) if config['metadata'].get('config'): # Hacked in a way to get the IdP metadata from a python dict # rather than having to resort to loading XML from file or http. idp_config = IdPConfig() idp_config.load(config['metadata']['config'][0]) idp_entityid = config['metadata']['config'][0]['entityid'] idp_metadata_str = str(entity_descriptor(idp_config, 24)) LOGGER.debug('IdP XML Metadata for %s: %s' % ( idp_entityid, idp_metadata_str)) self._config.metadata.import_metadata( idp_metadata_str, idp_entityid) self.attribute_map = {} if attribute_map is not None: self.attribute_map = attribute_map
def test_wayf(): c = SPConfig().load_file("server_conf") c.context = "sp" idps = c.idps() assert idps == {'urn:mace:example.com:saml:roland:idp': 'Example Co.'} idps = c.idps(["se","en"]) assert idps == {'urn:mace:example.com:saml:roland:idp': 'Exempel AB'} c.setup_logger() assert root_logger.level != logging.NOTSET assert root_logger.level == logging.INFO assert len(root_logger.handlers) == 1 assert isinstance(root_logger.handlers[0], logging.handlers.RotatingFileHandler) handler = root_logger.handlers[0] assert handler.backupCount == 5 assert handler.maxBytes == 100000 assert handler.mode == "a" assert root_logger.name == "saml2" assert root_logger.level == 20
def test_unset_force_authn(): cnf = SPConfig().load(sp1) assert bool(cnf.getattr('force_authn', 'sp')) == False
def test_set_force_authn(): cnf = SPConfig().load(sp2) assert bool(cnf.getattr('force_authn', 'sp')) == True
class Saml(object): """ SAML Wrapper around pysaml2. Implements SAML2 Service Provider functionality for Flask. """ def __init__(self, config, attribute_map=None): """Initialize SAML Service Provider. Args: config (dict): Service Provider config info in dict form attribute_map (dict): Mapping of attribute keys to user data """ self._config = SPConfig() self._config.load(config) if config['metadata'].get('config'): # Hacked in a way to get the IdP metadata from a python dict # rather than having to resort to loading XML from file or http. idp_config = IdPConfig() idp_config.load(config['metadata']['config'][0]) idp_entityid = config['metadata']['config'][0]['entityid'] idp_metadata_str = str(entity_descriptor(idp_config, 24)) LOGGER.debug('IdP XML Metadata for %s: %s' % ( idp_entityid, idp_metadata_str)) self._config.metadata.import_metadata( idp_metadata_str, idp_entityid) self.attribute_map = {} if attribute_map is not None: self.attribute_map = attribute_map def authenticate(self, next_url='/', binding=BINDING_HTTP_REDIRECT): """Start SAML Authentication login process. Args: next_url (string): HTTP URL to return user to when authentication is complete. binding (binding): Saml2 binding method to use for request, default BINDING_HTTP_REDIRECT (don't change til HTTP_POST support is complete in pysaml2. Returns: Flask Response object to return to user containing either HTTP_REDIRECT or HTTP_POST SAML message. Raises: AuthException: when unable to locate valid IdP. BadRequest: when invalid result returned from SAML client. """ # find configured for IdP for requested binding method idp_entityid = '' idps = self._config.idps().keys() for idp in idps: if self._config.single_sign_on_services(idp, binding) != []: idp_entityid = idp break if idp_entityid == '': raise AuthException('Unable to locate valid IdP for this request') # fail if signing requested but no private key configured if self._config.authn_requests_signed == 'true': if not self._config.key_file \ or not os.path.exists(self._config.key_file): raise AuthException( 'Signature requested for this Saml authentication request,' ' but no private key file configured') LOGGER.debug('Connecting to Identity Provider %s' % idp_entityid) # retrieve cache outstanding_queries_cache = \ AuthDictCache(session, '_saml_outstanding_queries') LOGGER.debug('Outstanding queries cache %s' % ( outstanding_queries_cache)) # make pysaml2 call to authenticate client = Saml2Client(self._config, logger=LOGGER) (session_id, result) = client.authenticate( entityid=idp_entityid, relay_state=next_url, binding=binding) # The psaml2 source for this method indicates that BINDING_HTTP_POST # should not be used right now to authenticate. Regardless, we'll # check for it and act accordingly. if binding == BINDING_HTTP_REDIRECT: LOGGER.debug('Redirect to Identity Provider %s ( %s )' % ( idp_entityid, result)) response = make_response('', 302, dict([result])) elif binding == BINDING_HTTP_POST: LOGGER.warn('POST binding used to authenticate is not currently' ' supported by pysaml2 release version. Fix in place in repo.') LOGGER.debug('Post to Identity Provider %s ( %s )' % ( idp_entityid, result)) response = make_response('\n'.join(result), 200) else: raise BadRequest('Invalid result returned from SAML client') LOGGER.debug( 'Saving session_id ( %s ) in outstanding queries' % session_id) # cache the outstanding query outstanding_queries_cache.update({session_id: next_url}) outstanding_queries_cache.sync() LOGGER.debug('Outstanding queries cache %s' % ( session['_saml_outstanding_queries'])) return response def handle_assertion(self, request): """Handle SAML Authentication login assertion (POST). Args: request (Request): Flask request object for this HTTP transaction. Returns: User Id (string), User attributes (dict), Redirect Flask response object to return user to now that authentication is complete. Raises: BadRequest: when error with SAML response from Identity Provider. AuthException: when unable to locate uid attribute in response. """ if not request.form.get('SAMLResponse'): raise BadRequest('SAMLResponse missing from POST') # retrieve cache outstanding_queries_cache = \ AuthDictCache(session, '_saml_outstanding_queries') identity_cache = IdentityCache(session, '_saml_identity') LOGGER.debug('Outstanding queries cache %s' % ( outstanding_queries_cache)) LOGGER.debug('Identity cache %s' % identity_cache) # use pysaml2 to process the SAML authentication response client = Saml2Client(self._config, identity_cache=identity_cache, logger=LOGGER) saml_response = client.response( dict(SAMLResponse=request.form['SAMLResponse']), outstanding_queries_cache) if saml_response is None: raise BadRequest('SAML response is invalid') # make sure outstanding query cache is cleared for this session_id session_id = saml_response.session_id() if session_id in outstanding_queries_cache.keys(): del outstanding_queries_cache[session_id] outstanding_queries_cache.sync() # retrieve session_info saml_session_info = saml_response.session_info() LOGGER.debug('SAML Session Info ( %s )' % saml_session_info) # retrieve user data via API try: if self.attribute_map.get('uid', 'name_id') == 'name_id': user_id = saml_session_info.get('name_id') else: user_id = saml_session_info['ava'] \ .get(self.attribute_map.get('uid'))[0] except: raise AuthException('Unable to find "%s" attribute in response' % ( self.attribute_map.get('uid', 'name_id'))) # Future: map attributes to user info user_attributes = dict() # set subject Id in cache to retrieved name_id session['_saml_subject_id'] = saml_session_info.get('name_id') LOGGER.debug('Outstanding queries cache %s' % ( session['_saml_outstanding_queries'])) LOGGER.debug('Identity cache %s' % session['_saml_identity']) LOGGER.debug('Subject Id %s' % session['_saml_subject_id']) relay_state = request.form.get('RelayState', '/') LOGGER.debug('Returning redirect to %s' % relay_state) return user_id, user_attributes, redirect(relay_state) def logout(self, next_url='/'): """Start SAML Authentication logout process. Args: next_url (string): HTTP URL to return user to when logout is complete. Returns: Flask Response object to return to user containing either HTTP_REDIRECT or HTTP_POST SAML message. Raises: AuthException: when unable to resolve Identity Provider single logout end-point. """ # retrieve cache state_cache = AuthDictCache(session, '_saml_state') identity_cache = IdentityCache(session, '_saml_identity') subject_id = session.get('_saml_subject_id') # don't logout if not logged in if subject_id is None: raise AuthException('Unable to retrieve subject id for logout') # fail if signing requested but no private key configured if self._config.logout_requests_signed == 'true': if not self._config.key_file \ or not os.path.exists(self._config.key_file): raise AuthException( 'Signature requested for this Saml logout request,' ' but no private key file configured') LOGGER.debug('State cache %s' % state_cache) LOGGER.debug('Identity cache %s' % identity_cache) LOGGER.debug('Subject Id %s' % subject_id) # use pysaml2 to initiate the SAML logout request client = Saml2Client(self._config, state_cache=state_cache, identity_cache=identity_cache, logger=LOGGER) saml_response = client.global_logout(subject_id, return_to=next_url) # sync the state to cache state_cache.sync() LOGGER.debug('State cache %s' % session['_saml_state']) LOGGER.debug('Identity cache %s' % session['_saml_identity']) if saml_response[1] == "": # used SOAP BINDING successfully return redirect(next_url) LOGGER.debug('Returning Response from SAML for continuation of the' ' logout process') return make_response('\n'.join(saml_response[3]), saml_response[1], saml_response[2]) # body, status, headers def _handle_logout_request(self, client, request, subject_id, binding): """Handle SAML Authentication logout request (GET). Args: client (Saml2Client): instance of SAML client class. request (Request): Flask request object for this HTTP transaction. subject_id (string): Id of the subject we are processing the logout for. binding (string): the SAML binding method being used for this request. Returns: Flask Response object to return to user containing HTTP_REDIRECT SAML message. Raises: BadRequest: when SAML request data is missing. AuthException: when SAML request indicates logout failed. """ LOGGER.debug('Received a logout request from Identity Provider') # pysaml2 logout_request currently only returns for # BINDING_HTTP_REDIRECT. We will have it fail for anything # other than the header 'Location' try: headers, _success = client.logout_request( request.values, subject_id, binding=binding) except TypeError: raise BadRequest('SAML request is invalid') try: assert headers is not None assert headers[0][0] == 'Location' return redirect(headers[0][1]) except: raise AuthException('An error occurred during logout') def _handle_logout_response(self, client, request, binding, next_url): """Handle SAML Authentication logout response (GET or POST). Args: client (Saml2Client): instance of SAML client class. request (Request): Flask request object for this HTTP transaction. binding (string): the SAML binding method being used for this request. next_url (string): URL to get redirected to if all is successful. Returns: Flask Response object to return to user containing HTTP_REDIRECT SAML message. Raises: BadRequest: when SAML response data is missing. AuthException: when SAML response indicates logout failed. """ LOGGER.debug('Received a logout response from Identity Provider') try: saml_response = client.logout_response( request.values['SAMLResponse'], binding=binding) except TypeError: raise BadRequest('SAML response is invalid') LOGGER.debug(saml_response) if saml_response: if saml_response[1] == '': # used SOAP BINDING successfully response = redirect(next_url) else: # body, status, headers response = make_response('\n'.join(saml_response[3]), saml_response[1], saml_response[2]) # pysaml2 returns an empty 200 in some cases, # we'll redirect instead if response.status_code == 200 and not response.data: response = redirect(next_url) else: raise AuthException('An error occurred during logout') return response def handle_logout(self, request, next_url='/'): """Handle SAML Authentication logout request/response. Args: request (Request): Flask request object for this HTTP transaction. next_url (string): URL to get redirected to if all is successful. Returns: (boolean) Success, Flask Response object to return to user containing HTTP_REDIRECT SAML message. Raises: BadRequest: when SAML request/response data is missing. """ # retrieve cache state_cache = AuthDictCache(session, '_saml_state') identity_cache = IdentityCache(session, '_saml_identity') subject_id = session.get('_saml_subject_id') LOGGER.debug('State cache %s' % state_cache) LOGGER.debug('Identity cache %s' % identity_cache) LOGGER.debug('Subject Id %s' % subject_id) # use pysaml2 to complete the SAML logout request client = Saml2Client(self._config, state_cache=state_cache, identity_cache=identity_cache, logger=LOGGER) # let's try to figure out what binding is being used and what type of # logout call we are handling if request.args: binding = BINDING_HTTP_REDIRECT elif request.form: binding = BINDING_HTTP_POST else: # The SOAP binding is only valid on logout requests which currently # pysaml2 doesn't support. raise BadRequest('Unable to find supported binding') if 'SAMLRequest' in request.values: response = self._handle_logout_request( client, request, subject_id, binding) elif 'SAMLResponse' in request.values: response = self._handle_logout_response( client, request, binding, next_url) else: raise BadRequest('Unable to find SAMLRequest or SAMLResponse') # cache the state and remove subject if logout was successful success = identity_cache.get_identity(subject_id) == ({}, []) if success: session.pop('_saml_subject_id') state_cache.sync() LOGGER.debug('State cache %s' % session['_saml_state']) LOGGER.debug('Identity cache %s' % session['_saml_identity']) LOGGER.debug( 'Returning redirect to complete/continue the logout process') return success, response def get_metadata(self): """Returns SAML Service Provider Metadata""" edesc = entity_descriptor(self._config, 24) if self._config.key_file: edesc = sign_entity_descriptor(edesc, 24, None, security_context(self._config)) response = make_response(str(edesc)) response.headers['Content-type'] = 'text/xml; charset=utf-8' return response
def test_config_loader_with_real_conf(request): config = SPConfig() config.load(conf.create_conf(sp_host='sp.example.com', idp_hosts=['idp.example.com'])) return config
def test_config_loader_with_real_conf(request): config = SPConfig() config.load(conf.create_conf(sp_host='sp.example.com', idp_hosts=['idp.example.com'], metadata_file='remote_metadata_one_idp.xml')) return config
from saml2.sigver import verify_redirect_signature from saml2.sigver import RSA_SHA1 from saml2.server import Server from saml2 import BINDING_HTTP_REDIRECT from saml2.client import Saml2Client from saml2.config import SPConfig from saml2.sigver import rsa_load from urlparse import parse_qs from pathutils import dotname __author__ = 'rolandh' idp = Server(config_file=dotname("idp_all_conf")) conf = SPConfig() conf.load_file(dotname("servera_conf")) sp = Saml2Client(conf) def test(): srvs = sp.metadata.single_sign_on_service(idp.config.entityid, BINDING_HTTP_REDIRECT) destination = srvs[0]["location"] req = sp.create_authn_request(destination, id="id1") try: key = sp.sec.key except AttributeError: key = rsa_load(sp.sec.key_file)
def test_sp(): cnf = SPConfig() cnf.load_file(dotname("sp_1_conf")) assert cnf.endpoint("assertion_consumer_service") == \ ["http://lingon.catalogix.se:8087/"]
_args = _parser.parse_args() if _args.entity_id: ARGS["entity_id"] = _args.entity_id if _args.discosrv: ARGS["discosrv"] = _args.discosrv if _args.wayf: ARGS["wayf"] = _args.wayf CACHE = Cache() CNFBASE = _args.config if _args.seed: SEED = _args.seed else: SEED = "SnabbtInspel" sp_base_conf = SPConfig().load_file("%s" % CNFBASE, metadata_construction=False) SP[""] = Saml2Client(config=sp_base_conf) for variant in EC_SEQUENCE[1:]: sp_conf = SPConfig().load_file(config_file="%s_%s" % (CNFBASE, variant), metadata_construction=True) sp_conf.metadata = sp_base_conf.metadata SP[variant] = Saml2Client(config=sp_conf) POLICY = server_conf.POLICY for entcat in SP: sp = SP[entcat] attr_list = POLICY.get_entity_categories(sp.config.entityid, sp.metadata) attr_html_list = ""
def test_sp(): cnf = SPConfig() cnf.load_file(dotname("sp_1_conf")) assert cnf.endpoint("assertion_consumer_service") == \ ["http://lingon.catalogix.se:8087/"]
def test_config_loader(request): config = SPConfig() config.load({'entityid': 'testentity'}) return config
ARGS["discosrv"] = _args.discosrv if _args.wayf: ARGS["wayf"] = _args.wayf CACHE = Cache() CNFBASE = _args.config if _args.seed: SEED = _args.seed else: SEED = "SnabbtInspel" sp_base_conf = SPConfig().load_file("%s" % CNFBASE, metadata_construction=False) SP[""] = Saml2Client(config=sp_base_conf) for variant in EC_SEQUENCE[1:]: sp_conf = SPConfig().load_file(config_file="%s_%s" % (CNFBASE, variant), metadata_construction=True) sp_conf.metadata = sp_base_conf.metadata SP[variant] = Saml2Client(config=sp_conf) POLICY = server_conf.POLICY for entcat in SP: sp = SP[entcat] attr_list = POLICY.get_entity_categories(sp.config.entityid, sp.metadata) attr_html_list = "" if attr_list is not None and len(attr_list) > 0: attr_html_list += "<ul>" for attr in attr_list: attr_html_list += "<li>%s</li>" % attr attr_html_list += "</ul>" EC_INFORMATION[entcat]["Description"] += RETURN_CATEGORY + attr_html_list pass
class Saml(object): """ SAML Wrapper around pysaml2. Implements SAML2 Service Provider functionality for Flask. """ def __init__(self, config): """Initialize SAML Service Provider. Args: config (dict): Service Provider config info in dict form """ if config.get('metadata') is not None: config['metadata'] = _parse_metadata_dict_to_inline( config['metadata']) self._config = SPConfig().load(config) self._config.setattr('', 'allow_unknown_attributes', True) # Set discovery end point, if configured for. if config['service']['sp'].get('ds'): self.discovery_service_end_point = \ config['service']['sp'].get('ds')[0] def authenticate(self, next_url="/", binding=BINDING_HTTP_REDIRECT, selected_idp=None): """Start SAML Authentication login process. Args: next_url (string): HTTP URL to return user to when authentication is complete. binding (binding): Saml2 binding method to use for request. Defaults to BINDING_HTTP_REDIRECT (don't change til HTTP_POST support is complete in pysaml2). selected_idp (string): A specfic IdP that should be used to authenticate. Defaults to `None`. Returns: Flask Response object to return to user containing either HTTP_REDIRECT or HTTP_POST SAML message. Raises: AuthException: when unable to locate valid IdP. BadRequest: when invalid result returned from SAML client. """ # Fail if signing requested but no private key configured. if self._config.getattr('authn_requests_signed') == True: if not self._config.key_file \ or not os.path.exists(self._config.key_file): raise AuthException( "Signature requested for this Saml authentication" + \ " request, but no private key file configured") # Find configured for IdPs for requested binding method. bindable_idps = [] all_idps = self._config.metadata.identity_providers() # Filter IdPs to allowed IdPs, if we have some. if self._config.getattr('idp') is not None: all_idps = list(set(all_idps) & set(self._config.getattr('idp'))) # Filter IdPs to selected IdP, if we have one. if selected_idp is not None: all_idps = list(set(all_idps) & set([selected_idp])) # From all IdPs allowed/selected, get the ones we can bind to. for idp in all_idps: if self._config.metadata.single_sign_on_service(idp, binding) != []: bindable_idps.append(idp) if not len(bindable_idps): raise AuthException("Unable to locate valid IdP for this request") # Retrieve cache. outstanding_queries_cache = \ AuthDictCache(session, '_saml_outstanding_queries') LOGGER.debug("Outstanding queries cache %s", outstanding_queries_cache) if len(bindable_idps) > 1: # Redirect to discovery service (session_id, response) = self._handle_discovery_request() else: idp_entityid = bindable_idps[0] LOGGER.debug("Connecting to Identity Provider %s", idp_entityid) # Make pysaml2 call to authenticate. client = Saml2Client(self._config) (session_id, result) = client.prepare_for_authenticate( entityid=idp_entityid, relay_state=next_url, sign=self._config.getattr('authn_requests_signed'), binding=binding) # The psaml2 source for this method indicates that # BINDING_HTTP_POST should not be used right now to authenticate. # Regardless, we'll check for it and act accordingly. if binding == BINDING_HTTP_REDIRECT: LOGGER.debug("Redirect to Identity Provider %s ( %s )", idp_entityid, result) response = make_response("", 302, dict(result['headers'])) elif binding == BINDING_HTTP_POST: LOGGER.debug("Post to Identity Provider %s ( %s )", idp_entityid, result) response = result, 200 else: raise BadRequest("Invalid result returned from SAML client") LOGGER.debug( "Saving session_id ( %s ) in outstanding queries", session_id) # cache the outstanding query outstanding_queries_cache.update({session_id: next_url}) outstanding_queries_cache.sync() LOGGER.debug("Outstanding queries cache %s", session['_saml_outstanding_queries']) return response def _handle_discovery_request(self): """Handle SAML Discovery Service request. This method is called internally by the `authenticate` method when multiple acceptable IdPs are detected. Returns: Tuple containing session Id and Flask Response object to return to user containing either HTTP_REDIRECT to configured Discovery Service end point. Raises: AuthException: when unable to find discovery response end point. """ session_id = sid() try: return_url = self._config.getattr( 'endpoints', 'sp')['discovery_response'][0][0] except KeyError: raise AuthException( "Multiple IdPs configured with no configured Discovery" + \ " response end point.") return_url += "?session_id=%s" % session_id disco_url = Saml2Client.create_discovery_service_request( self.discovery_service_end_point, self._config.entityid, **{'return': return_url}) LOGGER.debug("Redirect to Discovery Service %s", disco_url) return (session_id, make_response('', 302, {'Location': disco_url})) def handle_discovery_response(self, request): """Handle SAML Discovery Service response. This method is basically a wrapper around `authenticate` with a little extra logic for getting the `entityID` out of the request and the next_url and binding that was previously submitted to `authenticate` from the user's session. Args: request (Request): Flask request object for this HTTP transaction. Returns: Flask Response object to return to user containing either HTTP_REDIRECT or HTTP_POST SAML message. Raises: AuthException: when unable to locate valid IdP. BadRequest: when invalid result returned from SAML client. """ session_id = request.args.get('session_id') next_url = "/" # Retrieve cache. Get `next_url` from cache. outstanding_queries_cache = \ AuthDictCache(session, '_saml_outstanding_queries') if session_id in outstanding_queries_cache.keys(): next_url = outstanding_queries_cache[session_id] del outstanding_queries_cache[session_id] outstanding_queries_cache.sync() # Get the selected IdP from the Discovery Service response. selected_idp = Saml2Client.parse_discovery_service_response( query=request.query_string) return self.authenticate(next_url=next_url, selected_idp=selected_idp) def handle_assertion(self, request): """Handle SAML Authentication login assertion (POST). Args: request (Request): Flask request object for this HTTP transaction. Returns: (tuple) SAML assertion response information (dict) containing the IdP entity id, the subject's name id, and any additional attributes which may have been returned in the assertion, and Redirect Flask response object to return user to now that authentication is complete. Raises: BadRequest: when error with SAML response from Identity Provider. AuthException: when unable to locate uid attribute in response. """ if not request.form.get('SAMLResponse'): raise BadRequest("SAMLResponse missing from POST") # retrieve cache outstanding_queries_cache = \ AuthDictCache(session, '_saml_outstanding_queries') identity_cache = IdentityCache(session, '_saml_identity') LOGGER.debug("Outstanding queries cache %s", outstanding_queries_cache) LOGGER.debug("Identity cache %s", identity_cache) # use pysaml2 to process the SAML authentication response client = Saml2Client(self._config, identity_cache=identity_cache) saml_response = client.parse_authn_request_response( request.form['SAMLResponse'], BINDING_HTTP_POST, outstanding=outstanding_queries_cache) if saml_response is None: raise BadRequest("SAML response is invalid") # make sure outstanding query cache is cleared for this session_id session_id = saml_response.session_id() if session_id in outstanding_queries_cache.keys(): del outstanding_queries_cache[session_id] outstanding_queries_cache.sync() saml_subject_id = saml_response.name_id # Assemble SAML assertion info for returning to the method caller. saml_assertion_info = saml_response.get_identity() # Note: SAML assertion attributes can have multiple values so the # values returned for these attributes are lists even if there is only # one entry. For consistency the `name_id` returned with the SAML # assertion information has been included as a single item list. saml_assertion_info['name_id'] = [saml_response.get_subject().text] # The IdP entity id is obviously not an attribute, so no list required. saml_assertion_info['idp_entity_id'] = saml_response.issuer() LOGGER.debug("SAML Session Info ( %s )", saml_assertion_info) # set subject Id in cache to retrieved name_id session['_saml_subject_id'] = saml_subject_id LOGGER.debug("Outstanding queries cache %s", session['_saml_outstanding_queries']) LOGGER.debug("Identity cache %s", session['_saml_identity']) LOGGER.debug("Subject Id %s", session['_saml_subject_id']) relay_state = request.form.get('RelayState', "/") LOGGER.debug("Returning redirect to %s", relay_state) return (saml_assertion_info, redirect(relay_state)) def logout(self, next_url='/', expire=None): """Start SAML Authentication logout process. Args: next_url (string): HTTP URL to return user to when logout is complete. expire (struct_time): The latest the log out should happen. Returns: Flask Response object to return to user containing either HTTP_REDIRECT or HTTP_POST SAML message. Raises: AuthException: Can not resolve IdP single logout end-point. """ # retrieve cache state_cache = AuthDictCache(session, '_saml_state') identity_cache = IdentityCache(session, '_saml_identity') subject_id = session.get('_saml_subject_id') # don't logout if not logged in if subject_id is None: raise AuthException("Unable to retrieve subject id for logout") # fail if signing requested but no private key configured if self._config.getattr('logout_requests_signed') == True: LOGGER.debug("key_file %s", self._config.key_file) if not self._config.key_file \ or not os.path.exists(self._config.key_file): raise AuthException( "Signature requested for this Saml logout request," + \ " but no private key file configured") LOGGER.debug("State cache %s", state_cache) LOGGER.debug("Identity cache %s", identity_cache) LOGGER.debug("Subject Id %s", subject_id) # use pysaml2 to initiate the SAML logout request client = Saml2Client( self._config, state_cache=state_cache, identity_cache=identity_cache) saml_response = client.global_logout(subject_id, expire=expire) # sync the state to cache state_cache.sync() LOGGER.debug("State cache %s", session['_saml_state']) LOGGER.debug("Identity cache %s", session['_saml_identity']) if saml_response.get('1', None) == "": # used SOAP BINDING successfully return redirect(next_url) LOGGER.debug("Returning Response from SAML for continuation of the" + \ " logout process") for _, item in saml_response.items(): if isinstance(item, tuple): http_type, htargs = item break if http_type == BINDING_HTTP_POST: return htargs, 200 else: return make_response("", 302, htargs['headers']) def handle_logout(self, request, next_url="/"): """Handle SAML Authentication logout request/response. Args: request (Request): Flask request object for this HTTP transaction. next_url (string): URL to get redirected to if all is successful. Returns: (boolean) Success, Flask Response object to return to user containing HTTP_REDIRECT SAML message. Raises: BadRequest: when SAML request/response data is missing. """ # retrieve cache state_cache = AuthDictCache(session, '_saml_state') identity_cache = IdentityCache(session, '_saml_identity') subject_id = session.get('_saml_subject_id') LOGGER.debug("State cache %s", state_cache) LOGGER.debug("Identity cache %s", identity_cache) LOGGER.debug("Subject Id %s", subject_id) # use pysaml2 to complete the SAML logout request client = Saml2Client( self._config, state_cache=state_cache, identity_cache=identity_cache) # let's try to figure out what binding is being used and what type of # logout call we are handling if request.args: binding = BINDING_HTTP_REDIRECT elif request.form: binding = BINDING_HTTP_POST else: # The SOAP binding is only valid on logout requests which currently # pysaml2 doesn't support. raise BadRequest("Unable to find supported binding") if "SAMLRequest" in request.values: response = _handle_logout_request( client, request, subject_id, binding) elif "SAMLResponse" in request.values: response = _handle_logout_response( client, request, binding, next_url) else: raise BadRequest("Unable to find SAMLRequest or SAMLResponse") # cache the state and remove subject if logout was successful or # this subject was already logged out. success = not subject_id or \ identity_cache.get_identity(subject_id) == ({}, []) if success: session.pop('_saml_subject_id', None) state_cache.sync() LOGGER.debug("State cache %s", session['_saml_state']) LOGGER.debug("Identity cache %s", session['_saml_identity']) LOGGER.debug( "Returning redirect to complete/continue the logout process") return success, response def get_metadata(self): """Returns SAML Service Provider Metadata""" edesc = entity_descriptor(self._config) if self._config.key_file: _, edesc = sign_entity_descriptor( edesc, None, security_context(self._config)) response = make_response(str(edesc)) response.headers['Content-type'] = "text/xml; charset=utf-8" return response
def test_config_loader(request): config = SPConfig() config.load({'entityid': 'testentity'}) return config