Example #1
0
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
Example #2
0
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
Example #4
0
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
Example #5
0
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
Example #6
0
    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
Example #7
0
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
Example #8
0
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)
Example #9
0
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
Example #10
0
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
Example #11
0
 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
Example #12
0
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
Example #13
0
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
Example #14
0
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
Example #15
0
    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)
Example #16
0
 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
Example #17
0
    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())
Example #18
0
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
Example #19
0
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
Example #20
0
    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]
Example #21
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
Example #22
0
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
Example #23
0
def test_unset_force_authn():
    cnf = SPConfig().load(sp1)
    assert bool(cnf.getattr('force_authn', 'sp')) == False
Example #24
0
def test_set_force_authn():
    cnf = SPConfig().load(sp2)
    assert bool(cnf.getattr('force_authn', 'sp')) == True
Example #25
0
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
Example #26
0
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
Example #27
0
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)
Example #29
0
def test_sp():
    cnf = SPConfig()
    cnf.load_file(dotname("sp_1_conf"))
    assert cnf.endpoint("assertion_consumer_service") == \
           ["http://lingon.catalogix.se:8087/"]
Example #30
0
    _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 = ""
Example #31
0
def test_sp():
    cnf = SPConfig()
    cnf.load_file(dotname("sp_1_conf"))
    assert cnf.endpoint("assertion_consumer_service") == \
                                            ["http://lingon.catalogix.se:8087/"]
Example #32
0
def test_config_loader(request):
    config = SPConfig()
    config.load({'entityid': 'testentity'})
    return config
Example #33
0
        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
Example #34
0
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
Example #35
0
def test_config_loader(request):
    config = SPConfig()
    config.load({'entityid': 'testentity'})
    return config