Ejemplo n.º 1
0
    def test_authn_request_no_requested_attributes(self, context, idp_conf,
                                                   sp_conf, req_attributes):
        """
        If no attributes are requested or if they are not in the ,
        configuration don't add the extention
        """
        requested_attributes = [{
            "friendly_name": "cn",
            "required": True
        }, {
            "friendly_name": "sn",
            "required": False
        }]

        backend = SAMLBackend(
            None, INTERNAL_ATTRIBUTES, {
                SAMLBackend.KEY_DYNAMIC_REQUESTED_ATTRIBUTES:
                requested_attributes,
                "sp_config": sp_conf
            }, "base_url", "saml_backend")

        with patch.object(backend.sp,
                          "create_authn_request",
                          wraps=backend.sp.create_authn_request) as mock:
            backend.authn_request(context,
                                  idp_conf["entityid"],
                                  requested_attributes=req_attributes)

            kwargs = mock.call_args[1]
            assert "requested_attributes" not in kwargs
Ejemplo n.º 2
0
    def test_get_metadata_desc_with_logo_without_lang(self, sp_conf, idp_conf):
        # add logo without 'lang'
        idp_conf["service"]["idp"]["ui_info"]["logo"] = [{"text": "https://idp.example.com/static/logo.png",
                                                          "width": "120", "height": "60"}]

        sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)]
        # instantiate new backend, with a single backing IdP
        samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend")
        entity_descriptions = samlbackend.get_metadata_desc()

        assert len(entity_descriptions) == 1

        idp_desc = entity_descriptions[0].to_dict()

        assert idp_desc["entityid"] == urlsafe_b64encode(idp_conf["entityid"].encode("utf-8")).decode("utf-8")
        assert idp_desc["contact_person"] == idp_conf["contact_person"]

        assert idp_desc["organization"]["name"][0] == tuple(idp_conf["organization"]["name"][0])
        assert idp_desc["organization"]["display_name"][0] == tuple(idp_conf["organization"]["display_name"][0])
        assert idp_desc["organization"]["url"][0] == tuple(idp_conf["organization"]["url"][0])

        expected_ui_info = idp_conf["service"]["idp"]["ui_info"]
        ui_info = idp_desc["service"]["idp"]["ui_info"]
        assert ui_info["display_name"] == expected_ui_info["display_name"]
        assert ui_info["description"] == expected_ui_info["description"]
        assert ui_info["logo"] == expected_ui_info["logo"]
Ejemplo n.º 3
0
    def test_authn_request_requested_attributes_ignore_extra(
            self, context, idp_conf, sp_conf):
        """
        Extra internal attributes should be ignored
        """
        requested_attributes = [{
            "friendly_name": "cn",
            "required": True
        }, {
            "friendly_name": "sn",
            "required": False
        }]

        backend = SAMLBackend(
            None, INTERNAL_ATTRIBUTES, {
                SAMLBackend.KEY_DYNAMIC_REQUESTED_ATTRIBUTES:
                requested_attributes,
                "sp_config": sp_conf
            }, "base_url", "saml_backend")

        with patch.object(backend.sp,
                          "create_authn_request",
                          wraps=backend.sp.create_authn_request) as mock:
            backend.authn_request(
                context,
                idp_conf["entityid"],
                requested_attributes=["name", "surname", "email"])

            kwargs = mock.call_args[1]
            assert "requested_attributes" in kwargs
            assert all(
                any(r == req for r in requested_attributes)
                for req in kwargs["requested_attributes"])
            assert (len(
                kwargs["requested_attributes"]) == len(requested_attributes))
Ejemplo n.º 4
0
    def test_get_metadata_desc_with_logo_without_lang(self, sp_conf, idp_conf):
        # add logo without 'lang'
        idp_conf["service"]["idp"]["ui_info"]["logo"] = [{"text": "https://idp.example.com/static/logo.png",
                                                          "width": "120", "height": "60"}]

        sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)]
        # instantiate new backend, with a single backing IdP
        samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend")
        entity_descriptions = samlbackend.get_metadata_desc()

        assert len(entity_descriptions) == 1

        idp_desc = entity_descriptions[0].to_dict()

        assert idp_desc["entityid"] == urlsafe_b64encode(idp_conf["entityid"].encode("utf-8")).decode("utf-8")
        assert idp_desc["contact_person"] == idp_conf["contact_person"]

        assert idp_desc["organization"]["name"][0] == tuple(idp_conf["organization"]["name"][0])
        assert idp_desc["organization"]["display_name"][0] == tuple(idp_conf["organization"]["display_name"][0])
        assert idp_desc["organization"]["url"][0] == tuple(idp_conf["organization"]["url"][0])

        expected_ui_info = idp_conf["service"]["idp"]["ui_info"]
        ui_info = idp_desc["service"]["idp"]["ui_info"]
        assert ui_info["display_name"] == expected_ui_info["display_name"]
        assert ui_info["description"] == expected_ui_info["description"]
        assert ui_info["logo"] == expected_ui_info["logo"]
Ejemplo n.º 5
0
    def test_authn_response_with_encrypted_assertion(self, sp_conf, context):
        with open(os.path.join(TEST_RESOURCE_BASE_PATH,
                               "idp_metadata_for_encrypted_signed_auth_response.xml")) as idp_metadata_file:
            sp_conf["metadata"]["inline"] = [idp_metadata_file.read()]
        samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf,
                                                                "disco_srv": DISCOSRV_URL},
                                  "base_url", "samlbackend")
        response_binding = BINDING_HTTP_REDIRECT
        relay_state = "test relay state"

        with open(os.path.join(TEST_RESOURCE_BASE_PATH,
                               "auth_response_with_encrypted_signed_assertion.xml")) as auth_response_file:
            auth_response = auth_response_file.read()
        context.request = {"SAMLResponse": deflate_and_base64_encode(auth_response), "RelayState": relay_state}

        context.state[self.samlbackend.name] = {"relay_state": relay_state}
        with open(os.path.join(TEST_RESOURCE_BASE_PATH, "encryption_key.pem")) as encryption_key_file:
            samlbackend.encryption_keys = [encryption_key_file.read()]

        assertion_issued_at = 1479315212
        with patch('saml2.validate.time_util.shift_time') as mock_shift_time, \
                patch('saml2.validate.time_util.utc_now') as mock_utc_now:
            mock_utc_now.return_value = assertion_issued_at + 1
            mock_shift_time.side_effect = [datetime.utcfromtimestamp(assertion_issued_at + 1),
                                     datetime.utcfromtimestamp(assertion_issued_at - 1)]
            samlbackend.authn_response(context, response_binding)

        context, internal_resp = samlbackend.auth_callback_func.call_args[0]
        assert Counter(internal_resp.attributes.keys()) == Counter({"mail", "givenname", "displayname", "surname"})
Ejemplo n.º 6
0
    def test_get_metadata_desc(self, sp_conf, idp_conf):
        sp_conf["metadata"]["inline"] = [
            create_metadata_from_config_dict(idp_conf)
        ]
        # instantiate new backend, with a single backing IdP
        samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES,
                                  {"sp_config": sp_conf}, "base_url",
                                  "saml_backend")
        entity_descriptions = samlbackend.get_metadata_desc()

        assert len(entity_descriptions) == 1

        idp_desc = entity_descriptions[0].to_dict()

        assert idp_desc["entityid"] == urlsafe_b64encode(
            idp_conf["entityid"].encode("utf-8")).decode("utf-8")
        assert idp_desc["contact_person"] == idp_conf["contact_person"]

        assert idp_desc["organization"]["name"][0] == tuple(
            idp_conf["organization"]["name"][0])
        assert idp_desc["organization"]["display_name"][0] == tuple(
            idp_conf["organization"]["display_name"][0])
        assert idp_desc["organization"]["url"][0] == tuple(
            idp_conf["organization"]["url"][0])

        expected_ui_info = idp_conf["service"]["idp"]["ui_info"]
        ui_info = idp_desc["service"]["idp"]["ui_info"]
        assert ui_info["display_name"] == expected_ui_info["display_name"]
        assert ui_info["description"] == expected_ui_info["description"]
        assert ui_info["logo"] == expected_ui_info["logo"]
Ejemplo n.º 7
0
    def test_redirect_to_idp_if_only_one_idp_in_metadata(self, context, sp_conf, idp_conf):
        sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)]
        # instantiate new backend, without any discovery service configured
        samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend")

        resp = samlbackend.start_auth(context, InternalData())
        assert_redirect_to_idp(resp, idp_conf)
Ejemplo n.º 8
0
    def test_authn_response_with_encrypted_assertion(self, sp_conf, context):
        with open(os.path.join(TEST_RESOURCE_BASE_PATH,
                               "idp_metadata_for_encrypted_signed_auth_response.xml")) as idp_metadata_file:
            sp_conf["metadata"]["inline"] = [idp_metadata_file.read()]
        samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf,
                                                                "disco_srv": DISCOSRV_URL},
                                  "base_url", "samlbackend")
        response_binding = BINDING_HTTP_REDIRECT
        relay_state = "test relay state"

        with open(os.path.join(TEST_RESOURCE_BASE_PATH,
                               "auth_response_with_encrypted_signed_assertion.xml")) as auth_response_file:
            auth_response = auth_response_file.read()
        context.request = {"SAMLResponse": deflate_and_base64_encode(auth_response), "RelayState": relay_state}

        context.state[self.samlbackend.name] = {"relay_state": relay_state}
        with open(os.path.join(TEST_RESOURCE_BASE_PATH, "encryption_key.pem")) as encryption_key_file:
            samlbackend.encryption_keys = [encryption_key_file.read()]

        assertion_issued_at = 1479315212
        with patch('saml2.validate.time_util.shift_time') as mock_shift_time, \
                patch('saml2.validate.time_util.utc_now') as mock_utc_now:
            mock_utc_now.return_value = assertion_issued_at + 1
            mock_shift_time.side_effect = [datetime.utcfromtimestamp(assertion_issued_at + 1),
                                     datetime.utcfromtimestamp(assertion_issued_at - 1)]
            samlbackend.authn_response(context, response_binding)

        context, internal_resp = samlbackend.auth_callback_func.call_args[0]
        assert Counter(internal_resp.attributes.keys()) == Counter({"mail", "givenname", "displayname", "surname"})
Ejemplo n.º 9
0
    def test_redirect_to_idp_if_only_one_idp_in_metadata(self, context, sp_conf, idp_conf):
        sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)]
        # instantiate new backend, without any discovery service configured
        samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend")

        resp = samlbackend.start_auth(context, InternalData())
        self.assert_redirect_to_idp(resp, idp_conf)
Ejemplo n.º 10
0
 def test_always_redirect_to_discovery_service_if_using_mdq(self, context, sp_conf, idp_conf):
     # one IdP in the metadata, but MDQ also configured so should always redirect to the discovery service
     sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)]
     sp_conf["metadata"]["mdq"] = ["https://mdq.example.com"]
     samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf, "disco_srv": DISCOSRV_URL,},
                               "base_url", "saml_backend")
     resp = samlbackend.start_auth(context, InternalData())
     self.assert_redirect_to_discovery_server(resp, sp_conf)
Ejemplo n.º 11
0
 def test_default_redirect_to_discovery_service_if_using_mdq(
     self, context, sp_conf, idp_conf
 ):
     # one IdP in the metadata, but MDQ also configured so should always redirect to the discovery service
     sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)]
     sp_conf["metadata"]["mdq"] = ["https://mdq.example.com"]
     samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf, "disco_srv": DISCOSRV_URL,},
                               "base_url", "saml_backend")
     resp = samlbackend.start_auth(context, InternalData())
     assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL)
Ejemplo n.º 12
0
 def test_backend_reads_encryption_key_from_encryption_keypair(self, sp_conf):
     del sp_conf["key_file"]
     sp_conf["encryption_keypairs"] = [{"key_file": os.path.join(TEST_RESOURCE_BASE_PATH, "encryption_key.pem")}]
     samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf,
                                                             "disco_srv": DISCOSRV_URL},
                               "base_url", "samlbackend")
     assert samlbackend.encryption_keys
Ejemplo n.º 13
0
    def test_authn_request_no_requested_attributes_configured(
            self, context, idp_conf, sp_conf, req_attributes):
        """
        If requested attributes is not configured, don't add the extention
        """
        backend = SAMLBackend(
            None, INTERNAL_ATTRIBUTES, {
                SAMLBackend.KEY_DYNAMIC_REQUESTED_ATTRIBUTES: req_attributes,
                "sp_config": sp_conf
            }, "base_url", "saml_backend")

        with patch.object(backend.sp,
                          "create_authn_request",
                          wraps=backend.sp.create_authn_request) as mock:
            backend.authn_request(context,
                                  idp_conf["entityid"],
                                  requested_attributes=["email"])

            kwargs = mock.call_args[1]
            assert "requested_attributes" not in kwargs
Ejemplo n.º 14
0
    def test_use_of_disco_or_redirect_to_idp_when_using_mdq_and_forceauthn_is_set_1(
        self, context, sp_conf, idp_conf
    ):
        sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)]
        sp_conf["metadata"]["mdq"] = ["https://mdq.example.com"]

        context.decorate(Context.KEY_FORCE_AUTHN, "1")
        context.state[Context.KEY_MEMORIZED_IDP] = idp_conf["entityid"]

        backend_conf = {
            SAMLBackend.KEY_SP_CONFIG: sp_conf,
            SAMLBackend.KEY_DISCO_SRV: DISCOSRV_URL,
            SAMLBackend.KEY_MEMORIZE_IDP: True,
            SAMLBackend.KEY_MIRROR_FORCE_AUTHN: True,
        }
        samlbackend = SAMLBackend(
            None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend"
        )
        resp = samlbackend.start_auth(context, InternalData())
        assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL)

        backend_conf[SAMLBackend.KEY_USE_MEMORIZED_IDP_WHEN_FORCE_AUTHN] = True
        samlbackend = SAMLBackend(
            None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend"
        )
        resp = samlbackend.start_auth(context, InternalData())
        assert_redirect_to_idp(resp, idp_conf)
Ejemplo n.º 15
0
    def test_get_metadata_desc(self, sp_conf, idp_conf):
        sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)]
        # instantiate new backend, with a single backing IdP
        samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend")
        entity_descriptions = samlbackend.get_metadata_desc()

        assert len(entity_descriptions) == 1

        idp_desc = entity_descriptions[0].to_dict()

        assert idp_desc["entityid"] == urlsafe_b64encode(idp_conf["entityid"].encode("utf-8")).decode("utf-8")
        assert idp_desc["contact_person"] == idp_conf["contact_person"]

        assert idp_desc["organization"]["name"][0] == tuple(idp_conf["organization"]["name"][0])
        assert idp_desc["organization"]["display_name"][0] == tuple(idp_conf["organization"]["display_name"][0])
        assert idp_desc["organization"]["url"][0] == tuple(idp_conf["organization"]["url"][0])

        expected_ui_info = idp_conf["service"]["idp"]["ui_info"]
        ui_info = idp_desc["service"]["idp"]["ui_info"]
        assert ui_info["display_name"] == expected_ui_info["display_name"]
        assert ui_info["description"] == expected_ui_info["description"]
        assert ui_info["logo"] == expected_ui_info["logo"]
Ejemplo n.º 16
0
    def test_authn_request_requested_attributes_not_present(
            self, context, idp_conf, sp_conf):
        """
        If some requested attributes are not in the requested don't add them to
        the request
        """
        requested_attributes = [{
            "friendly_name": "cn",
            "required": True
        }, {
            "friendly_name": "sn",
            "required": False
        }]

        backend = SAMLBackend(
            None, INTERNAL_ATTRIBUTES, {
                SAMLBackend.KEY_DYNAMIC_REQUESTED_ATTRIBUTES:
                requested_attributes,
                "sp_config": sp_conf
            }, "base_url", "saml_backend")

        with patch.object(backend.sp,
                          "create_authn_request",
                          wraps=backend.sp.create_authn_request) as mock:
            backend.authn_request(context,
                                  idp_conf["entityid"],
                                  requested_attributes=["name"])

            kwargs = mock.call_args[1]
            assert "requested_attributes" in kwargs
            assert kwargs["requested_attributes"] == [
                {
                    "friendly_name": "cn",
                    "required": True
                },
            ]
Ejemplo n.º 17
0
 def create_backend(self, sp_conf, idp_conf):
     self.setup_test_config(sp_conf, idp_conf)
     self.samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf,
                                                                  "disco_srv": DISCOSRV_URL},
                                    "base_url",
                                    "samlbackend")
Ejemplo n.º 18
0
class TestSAMLBackend:
    @pytest.fixture(autouse=True)
    def create_backend(self, sp_conf, idp_conf):
        setup_test_config(sp_conf, idp_conf)
        self.samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf,
                                                                     "disco_srv": DISCOSRV_URL},
                                       "base_url",
                                       "samlbackend")

    def test_register_endpoints(self, sp_conf):
        """
        Tests the method register_endpoints
        """

        def get_path_from_url(url):
            return urlparse(url).path.lstrip("/")

        url_map = self.samlbackend.register_endpoints()
        all_sp_endpoints = [get_path_from_url(v[0][0]) for v in sp_conf["service"]["sp"]["endpoints"].values()]
        compiled_regex = [re.compile(regex) for regex, _ in url_map]
        for endp in all_sp_endpoints:
            assert any(p.match(endp) for p in compiled_regex)

    def test_start_auth_defaults_to_redirecting_to_discovery_server(self, context, sp_conf):
        resp = self.samlbackend.start_auth(context, InternalData())
        assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL)

    def test_discovery_server_set_in_context(self, context, sp_conf):
        discosrv_url = 'https://my.org/saml_discovery_service'
        context.decorate(
            SAMLBackend.KEY_SAML_DISCOVERY_SERVICE_URL, discosrv_url
        )
        resp = self.samlbackend.start_auth(context, InternalData())
        assert_redirect_to_discovery_server(resp, sp_conf, discosrv_url)

    def test_full_flow(self, context, idp_conf, sp_conf):
        test_state_key = "test_state_key_456afgrh"
        response_binding = BINDING_HTTP_REDIRECT
        fakeidp = FakeIdP(USERS, config=IdPConfig().load(idp_conf, metadata_construction=False))

        context.state[test_state_key] = "my_state"

        # start auth flow (redirecting to discovery server)
        resp = self.samlbackend.start_auth(context, InternalData())
        assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL)

        # fake response from discovery server
        disco_resp = parse_qs(urlparse(resp.message).query)
        info = parse_qs(urlparse(disco_resp["return"][0]).query)
        info["entityID"] = idp_conf["entityid"]
        request_context = Context()
        request_context.request = info
        request_context.state = context.state

        # pass discovery response to backend and check that it redirects to the selected IdP
        resp = self.samlbackend.disco_response(request_context)
        assert_redirect_to_idp(resp, idp_conf)

        # fake auth response to the auth request
        req_params = dict(parse_qsl(urlparse(resp.message).query))
        url, fake_idp_resp = fakeidp.handle_auth_req(
            req_params["SAMLRequest"],
            req_params["RelayState"],
            BINDING_HTTP_REDIRECT,
            "testuser1",
            response_binding=response_binding)
        response_context = Context()
        response_context.request = fake_idp_resp
        response_context.state = request_context.state

        # pass auth response to backend and verify behavior
        self.samlbackend.authn_response(response_context, response_binding)
        context, internal_resp = self.samlbackend.auth_callback_func.call_args[0]
        assert self.samlbackend.name not in context.state
        assert context.state[test_state_key] == "my_state"
        assert_authn_response(internal_resp)

    def test_start_auth_redirects_directly_to_mirrored_idp(
            self, context, idp_conf):
        entityid = idp_conf["entityid"]
        context.decorate(Context.KEY_TARGET_ENTITYID, entityid)

        resp = self.samlbackend.start_auth(context, InternalData())
        assert_redirect_to_idp(resp, idp_conf)

    def test_redirect_to_idp_if_only_one_idp_in_metadata(self, context, sp_conf, idp_conf):
        sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)]
        # instantiate new backend, without any discovery service configured
        samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend")

        resp = samlbackend.start_auth(context, InternalData())
        assert_redirect_to_idp(resp, idp_conf)

    def test_authn_request(self, context, idp_conf):
        resp = self.samlbackend.authn_request(context, idp_conf["entityid"])
        assert_redirect_to_idp(resp, idp_conf)
        req_params = dict(parse_qsl(urlparse(resp.message).query))
        assert context.state[self.samlbackend.name]["relay_state"] == req_params["RelayState"]

    def test_authn_response(self, context, idp_conf, sp_conf):
        response_binding = BINDING_HTTP_REDIRECT
        fakesp = FakeSP(SPConfig().load(sp_conf, metadata_construction=False))
        fakeidp = FakeIdP(USERS, config=IdPConfig().load(idp_conf, metadata_construction=False))
        destination, request_params = fakesp.make_auth_req(idp_conf["entityid"])
        url, auth_resp = fakeidp.handle_auth_req(request_params["SAMLRequest"], request_params["RelayState"],
                                                 BINDING_HTTP_REDIRECT,
                                                 "testuser1", response_binding=response_binding)

        context.request = auth_resp
        context.state[self.samlbackend.name] = {"relay_state": request_params["RelayState"]}
        self.samlbackend.authn_response(context, response_binding)

        context, internal_resp = self.samlbackend.auth_callback_func.call_args[0]
        assert_authn_response(internal_resp)
        assert self.samlbackend.name not in context.state

    @pytest.mark.skipif(
            saml2.__version__ < '4.6.1',
            reason="Optional NameID needs pysaml2 v4.6.1 or higher")
    def test_authn_response_no_name_id(self, context, idp_conf, sp_conf):
        response_binding = BINDING_HTTP_REDIRECT

        fakesp_conf = SPConfig().load(sp_conf, metadata_construction=False)
        fakesp = FakeSP(fakesp_conf)

        fakeidp_conf = IdPConfig().load(idp_conf, metadata_construction=False)
        fakeidp = FakeIdP(USERS, config=fakeidp_conf)

        destination, request_params = fakesp.make_auth_req(
            idp_conf["entityid"])

        # Use the fake IdP to mock up an authentication request that has no
        # <NameID> element.
        url, auth_resp = fakeidp.handle_auth_req_no_name_id(
            request_params["SAMLRequest"],
            request_params["RelayState"],
            BINDING_HTTP_REDIRECT,
            "testuser1",
            response_binding=response_binding)

        backend = self.samlbackend

        context.request = auth_resp
        context.state[backend.name] = {
            "relay_state": request_params["RelayState"],
        }
        backend.authn_response(context, response_binding)

        context, internal_resp = backend.auth_callback_func.call_args[0]
        assert_authn_response(internal_resp)
        assert backend.name not in context.state

    def test_authn_response_with_encrypted_assertion(self, sp_conf, context):
        with open(os.path.join(TEST_RESOURCE_BASE_PATH,
                               "idp_metadata_for_encrypted_signed_auth_response.xml")) as idp_metadata_file:
            sp_conf["metadata"]["inline"] = [idp_metadata_file.read()]
        samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf,
                                                                "disco_srv": DISCOSRV_URL},
                                  "base_url", "samlbackend")
        response_binding = BINDING_HTTP_REDIRECT
        relay_state = "test relay state"

        with open(os.path.join(TEST_RESOURCE_BASE_PATH,
                               "auth_response_with_encrypted_signed_assertion.xml")) as auth_response_file:
            auth_response = auth_response_file.read()
        context.request = {"SAMLResponse": deflate_and_base64_encode(auth_response), "RelayState": relay_state}

        context.state[self.samlbackend.name] = {"relay_state": relay_state}
        with open(os.path.join(TEST_RESOURCE_BASE_PATH, "encryption_key.pem")) as encryption_key_file:
            samlbackend.encryption_keys = [encryption_key_file.read()]

        assertion_issued_at = 1479315212
        with patch('saml2.validate.time_util.shift_time') as mock_shift_time, \
                patch('saml2.validate.time_util.utc_now') as mock_utc_now:
            mock_utc_now.return_value = assertion_issued_at + 1
            mock_shift_time.side_effect = [datetime.utcfromtimestamp(assertion_issued_at + 1),
                                     datetime.utcfromtimestamp(assertion_issued_at - 1)]
            samlbackend.authn_response(context, response_binding)

        context, internal_resp = samlbackend.auth_callback_func.call_args[0]
        assert Counter(internal_resp.attributes.keys()) == Counter({"mail", "givenname", "displayname", "surname"})

    def test_backend_reads_encryption_key_from_key_file(self, sp_conf):
        sp_conf["key_file"] = os.path.join(TEST_RESOURCE_BASE_PATH, "encryption_key.pem")
        samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf,
                                                                "disco_srv": DISCOSRV_URL},
                                  "base_url", "samlbackend")
        assert samlbackend.encryption_keys

    def test_backend_reads_encryption_key_from_encryption_keypair(self, sp_conf):
        del sp_conf["key_file"]
        sp_conf["encryption_keypairs"] = [{"key_file": os.path.join(TEST_RESOURCE_BASE_PATH, "encryption_key.pem")}]
        samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf,
                                                                "disco_srv": DISCOSRV_URL},
                                  "base_url", "samlbackend")
        assert samlbackend.encryption_keys

    def test_metadata_endpoint(self, context, sp_conf):
        resp = self.samlbackend._metadata_endpoint(context)
        headers = dict(resp.headers)
        assert headers["Content-Type"] == "text/xml"
        assert sp_conf["entityid"] in resp.message

    def test_get_metadata_desc(self, sp_conf, idp_conf):
        sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)]
        # instantiate new backend, with a single backing IdP
        samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend")
        entity_descriptions = samlbackend.get_metadata_desc()

        assert len(entity_descriptions) == 1

        idp_desc = entity_descriptions[0].to_dict()

        assert idp_desc["entityid"] == urlsafe_b64encode(idp_conf["entityid"].encode("utf-8")).decode("utf-8")
        assert idp_desc["contact_person"] == idp_conf["contact_person"]

        assert idp_desc["organization"]["name"][0] == tuple(idp_conf["organization"]["name"][0])
        assert idp_desc["organization"]["display_name"][0] == tuple(idp_conf["organization"]["display_name"][0])
        assert idp_desc["organization"]["url"][0] == tuple(idp_conf["organization"]["url"][0])

        expected_ui_info = idp_conf["service"]["idp"]["ui_info"]
        ui_info = idp_desc["service"]["idp"]["ui_info"]
        assert ui_info["display_name"] == expected_ui_info["display_name"]
        assert ui_info["description"] == expected_ui_info["description"]
        assert ui_info["logo"] == expected_ui_info["logo"]

    def test_get_metadata_desc_with_logo_without_lang(self, sp_conf, idp_conf):
        # add logo without 'lang'
        idp_conf["service"]["idp"]["ui_info"]["logo"] = [{"text": "https://idp.example.com/static/logo.png",
                                                          "width": "120", "height": "60"}]

        sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)]
        # instantiate new backend, with a single backing IdP
        samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend")
        entity_descriptions = samlbackend.get_metadata_desc()

        assert len(entity_descriptions) == 1

        idp_desc = entity_descriptions[0].to_dict()

        assert idp_desc["entityid"] == urlsafe_b64encode(idp_conf["entityid"].encode("utf-8")).decode("utf-8")
        assert idp_desc["contact_person"] == idp_conf["contact_person"]

        assert idp_desc["organization"]["name"][0] == tuple(idp_conf["organization"]["name"][0])
        assert idp_desc["organization"]["display_name"][0] == tuple(idp_conf["organization"]["display_name"][0])
        assert idp_desc["organization"]["url"][0] == tuple(idp_conf["organization"]["url"][0])

        expected_ui_info = idp_conf["service"]["idp"]["ui_info"]
        ui_info = idp_desc["service"]["idp"]["ui_info"]
        assert ui_info["display_name"] == expected_ui_info["display_name"]
        assert ui_info["description"] == expected_ui_info["description"]
        assert ui_info["logo"] == expected_ui_info["logo"]
Ejemplo n.º 19
0
class TestSAMLBackend:
    def assert_redirect_to_idp(self, redirect_response, idp_conf):
        assert redirect_response.status == "303 See Other"
        parsed = urlparse(redirect_response.message)
        redirect_location = "{parsed.scheme}://{parsed.netloc}{parsed.path}".format(
            parsed=parsed)
        assert redirect_location == idp_conf["service"]["idp"]["endpoints"][
            "single_sign_on_service"][0][0]
        assert "SAMLRequest" in parse_qs(parsed.query)

    def assert_redirect_to_discovery_server(self, redirect_response, sp_conf):
        assert redirect_response.status == "303 See Other"
        parsed = urlparse(redirect_response.message)
        redirect_location = "{parsed.scheme}://{parsed.netloc}{parsed.path}".format(
            parsed=parsed)
        assert redirect_location == DISCOSRV_URL

        request_params = dict(parse_qsl(parsed.query))
        assert request_params["return"] == sp_conf["service"]["sp"][
            "endpoints"]["discovery_response"][0][0]
        assert request_params["entityID"] == sp_conf["entityid"]

    def assert_authn_response(self, internal_resp):
        assert internal_resp.auth_info.auth_class_ref == PASSWORD
        expected_data = {
            'surname': ['Testsson 1'],
            'mail': ['*****@*****.**'],
            'displayname': ['Test Testsson'],
            'givenname': ['Test 1'],
            'edupersontargetedid': ['one!for!all']
        }
        assert expected_data == internal_resp.attributes

    def setup_test_config(self, sp_conf, idp_conf):
        idp_metadata_str = create_metadata_from_config_dict(idp_conf)
        sp_conf["metadata"]["inline"].append(idp_metadata_str)
        idp2_config = idp_conf.copy()
        idp2_config["entityid"] = "just_an_extra_idp"
        idp_metadata_str2 = create_metadata_from_config_dict(idp2_config)
        sp_conf["metadata"]["inline"].append(idp_metadata_str2)

        sp_metadata_str = create_metadata_from_config_dict(sp_conf)
        idp_conf["metadata"]["inline"] = [sp_metadata_str]

    @pytest.fixture(autouse=True)
    def create_backend(self, sp_conf, idp_conf):
        self.setup_test_config(sp_conf, idp_conf)
        self.samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {
            "sp_config": sp_conf,
            "disco_srv": DISCOSRV_URL
        }, "base_url", "samlbackend")

    def test_register_endpoints(self, sp_conf):
        """
        Tests the method register_endpoints
        """
        def get_path_from_url(url):
            return urlparse(url).path.lstrip("/")

        url_map = self.samlbackend.register_endpoints()
        all_sp_endpoints = [
            get_path_from_url(v[0][0])
            for v in sp_conf["service"]["sp"]["endpoints"].values()
        ]
        compiled_regex = [re.compile(regex) for regex, _ in url_map]
        for endp in all_sp_endpoints:
            assert any(p.match(endp) for p in compiled_regex)

    def test_start_auth_defaults_to_redirecting_to_discovery_server(
            self, context, sp_conf):
        resp = self.samlbackend.start_auth(context,
                                           InternalRequest(None, None))
        self.assert_redirect_to_discovery_server(resp, sp_conf)

    def test_full_flow(self, context, idp_conf, sp_conf):
        test_state_key = "test_state_key_456afgrh"
        response_binding = BINDING_HTTP_REDIRECT
        fakeidp = FakeIdP(USERS,
                          config=IdPConfig().load(idp_conf,
                                                  metadata_construction=False))

        context.state[test_state_key] = "my_state"

        # start auth flow (redirecting to discovery server)
        resp = self.samlbackend.start_auth(context,
                                           InternalRequest(None, None))
        self.assert_redirect_to_discovery_server(resp, sp_conf)

        # fake response from discovery server
        disco_resp = parse_qs(urlparse(resp.message).query)
        info = parse_qs(urlparse(disco_resp["return"][0]).query)
        info["entityID"] = idp_conf["entityid"]
        request_context = Context()
        request_context.request = info
        request_context.state = context.state

        # pass discovery response to backend and check that it redirects to the selected IdP
        resp = self.samlbackend.disco_response(request_context)
        self.assert_redirect_to_idp(resp, idp_conf)

        # fake auth response to the auth request
        req_params = dict(parse_qsl(urlparse(resp.message).query))
        url, fake_idp_resp = fakeidp.handle_auth_req(
            req_params["SAMLRequest"],
            req_params["RelayState"],
            BINDING_HTTP_REDIRECT,
            "testuser1",
            response_binding=response_binding)
        response_context = Context()
        response_context.request = fake_idp_resp
        response_context.state = request_context.state

        # pass auth response to backend and verify behavior
        self.samlbackend.authn_response(response_context, response_binding)
        context, internal_resp = self.samlbackend.auth_callback_func.call_args[
            0]
        assert self.samlbackend.name not in context.state
        assert context.state[test_state_key] == "my_state"
        self.assert_authn_response(internal_resp)

    def test_start_auth_redirects_directly_to_mirrored_idp(
            self, context, idp_conf):
        entityid = idp_conf["entityid"]
        entityid_bytes = entityid.encode("utf-8")
        entityid_b64_str = urlsafe_b64encode(entityid_bytes).decode("utf-8")
        context.decorate(Context.KEY_MIRROR_TARGET_ENTITYID, entityid_b64_str)

        resp = self.samlbackend.start_auth(context,
                                           InternalRequest(None, None))
        self.assert_redirect_to_idp(resp, idp_conf)

    def test_redirect_to_idp_if_only_one_idp_in_metadata(
            self, context, sp_conf, idp_conf):
        sp_conf["metadata"]["inline"] = [
            create_metadata_from_config_dict(idp_conf)
        ]
        # instantiate new backend, without any discovery service configured
        samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES,
                                  {"sp_config": sp_conf}, "base_url",
                                  "saml_backend")

        resp = samlbackend.start_auth(context, InternalRequest(None, None))
        self.assert_redirect_to_idp(resp, idp_conf)

    def test_always_redirect_to_discovery_service_if_using_mdq(
            self, context, sp_conf, idp_conf):
        # one IdP in the metadata, but MDQ also configured so should always redirect to the discovery service
        sp_conf["metadata"]["inline"] = [
            create_metadata_from_config_dict(idp_conf)
        ]
        sp_conf["metadata"]["mdq"] = ["https://mdq.example.com"]
        samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {
            "sp_config": sp_conf,
            "disco_srv": DISCOSRV_URL,
        }, "base_url", "saml_backend")
        resp = samlbackend.start_auth(context, InternalRequest(None, None))
        self.assert_redirect_to_discovery_server(resp, sp_conf)

    def test_authn_request(self, context, idp_conf):
        resp = self.samlbackend.authn_request(context, idp_conf["entityid"])
        self.assert_redirect_to_idp(resp, idp_conf)
        req_params = dict(parse_qsl(urlparse(resp.message).query))
        assert context.state[
            self.samlbackend.name]["relay_state"] == req_params["RelayState"]

    def test_authn_response(self, context, idp_conf, sp_conf):
        response_binding = BINDING_HTTP_REDIRECT
        fakesp = FakeSP(SPConfig().load(sp_conf, metadata_construction=False))
        fakeidp = FakeIdP(USERS,
                          config=IdPConfig().load(idp_conf,
                                                  metadata_construction=False))
        destination, request_params = fakesp.make_auth_req(
            idp_conf["entityid"])
        url, auth_resp = fakeidp.handle_auth_req(
            request_params["SAMLRequest"],
            request_params["RelayState"],
            BINDING_HTTP_REDIRECT,
            "testuser1",
            response_binding=response_binding)

        context.request = auth_resp
        context.state[self.samlbackend.name] = {
            "relay_state": request_params["RelayState"]
        }
        self.samlbackend.authn_response(context, response_binding)

        context, internal_resp = self.samlbackend.auth_callback_func.call_args[
            0]
        self.assert_authn_response(internal_resp)
        assert self.samlbackend.name not in context.state

    def test_authn_response_with_encrypted_assertion(self, sp_conf, context):
        with open(
                os.path.join(
                    TEST_RESOURCE_BASE_PATH,
                    "idp_metadata_for_encrypted_signed_auth_response.xml")
        ) as idp_metadata_file:
            sp_conf["metadata"]["inline"] = [idp_metadata_file.read()]
        samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {
            "sp_config": sp_conf,
            "disco_srv": DISCOSRV_URL
        }, "base_url", "samlbackend")
        response_binding = BINDING_HTTP_REDIRECT
        relay_state = "test relay state"

        with open(
                os.path.join(
                    TEST_RESOURCE_BASE_PATH,
                    "auth_response_with_encrypted_signed_assertion.xml")
        ) as auth_response_file:
            auth_response = auth_response_file.read()
        context.request = {
            "SAMLResponse": deflate_and_base64_encode(auth_response),
            "RelayState": relay_state
        }

        context.state[self.samlbackend.name] = {"relay_state": relay_state}
        with open(os.path.join(TEST_RESOURCE_BASE_PATH,
                               "encryption_key.pem")) as encryption_key_file:
            samlbackend.encryption_keys = [encryption_key_file.read()]

        assertion_issued_at = 1479315212
        with patch('saml2.validate.time_util.shift_time') as mock_shift_time, \
                patch('saml2.validate.time_util.utc_now') as mock_utc_now:
            mock_utc_now.return_value = assertion_issued_at + 1
            mock_shift_time.side_effect = [
                datetime.utcfromtimestamp(assertion_issued_at + 1),
                datetime.utcfromtimestamp(assertion_issued_at - 1)
            ]
            samlbackend.authn_response(context, response_binding)

        context, internal_resp = samlbackend.auth_callback_func.call_args[0]
        assert Counter(internal_resp.attributes.keys()) == Counter(
            {"mail", "givenname", "displayname", "surname"})

    def test_backend_reads_encryption_key_from_key_file(self, sp_conf):
        sp_conf["key_file"] = os.path.join(TEST_RESOURCE_BASE_PATH,
                                           "encryption_key.pem")
        samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {
            "sp_config": sp_conf,
            "disco_srv": DISCOSRV_URL
        }, "base_url", "samlbackend")
        assert samlbackend.encryption_keys

    def test_backend_reads_encryption_key_from_encryption_keypair(
            self, sp_conf):
        del sp_conf["key_file"]
        sp_conf["encryption_keypairs"] = [{
            "key_file":
            os.path.join(TEST_RESOURCE_BASE_PATH, "encryption_key.pem")
        }]
        samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {
            "sp_config": sp_conf,
            "disco_srv": DISCOSRV_URL
        }, "base_url", "samlbackend")
        assert samlbackend.encryption_keys

    def test_metadata_endpoint(self, context, sp_conf):
        resp = self.samlbackend._metadata_endpoint(context)
        headers = dict(resp.headers)
        assert headers["Content-Type"] == "text/xml"
        assert sp_conf["entityid"] in resp.message

    def test_get_metadata_desc(self, sp_conf, idp_conf):
        sp_conf["metadata"]["inline"] = [
            create_metadata_from_config_dict(idp_conf)
        ]
        # instantiate new backend, with a single backing IdP
        samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES,
                                  {"sp_config": sp_conf}, "base_url",
                                  "saml_backend")
        entity_descriptions = samlbackend.get_metadata_desc()

        assert len(entity_descriptions) == 1

        idp_desc = entity_descriptions[0].to_dict()

        assert idp_desc["entityid"] == urlsafe_b64encode(
            idp_conf["entityid"].encode("utf-8")).decode("utf-8")
        assert idp_desc["contact_person"] == idp_conf["contact_person"]

        assert idp_desc["organization"]["name"][0] == tuple(
            idp_conf["organization"]["name"][0])
        assert idp_desc["organization"]["display_name"][0] == tuple(
            idp_conf["organization"]["display_name"][0])
        assert idp_desc["organization"]["url"][0] == tuple(
            idp_conf["organization"]["url"][0])

        expected_ui_info = idp_conf["service"]["idp"]["ui_info"]
        ui_info = idp_desc["service"]["idp"]["ui_info"]
        assert ui_info["display_name"] == expected_ui_info["display_name"]
        assert ui_info["description"] == expected_ui_info["description"]
        assert ui_info["logo"] == expected_ui_info["logo"]

    def test_get_metadata_desc_with_logo_without_lang(self, sp_conf, idp_conf):
        # add logo without 'lang'
        idp_conf["service"]["idp"]["ui_info"]["logo"] = [{
            "text": "https://idp.example.com/static/logo.png",
            "width": "120",
            "height": "60"
        }]

        sp_conf["metadata"]["inline"] = [
            create_metadata_from_config_dict(idp_conf)
        ]
        # instantiate new backend, with a single backing IdP
        samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES,
                                  {"sp_config": sp_conf}, "base_url",
                                  "saml_backend")
        entity_descriptions = samlbackend.get_metadata_desc()

        assert len(entity_descriptions) == 1

        idp_desc = entity_descriptions[0].to_dict()

        assert idp_desc["entityid"] == urlsafe_b64encode(
            idp_conf["entityid"].encode("utf-8")).decode("utf-8")
        assert idp_desc["contact_person"] == idp_conf["contact_person"]

        assert idp_desc["organization"]["name"][0] == tuple(
            idp_conf["organization"]["name"][0])
        assert idp_desc["organization"]["display_name"][0] == tuple(
            idp_conf["organization"]["display_name"][0])
        assert idp_desc["organization"]["url"][0] == tuple(
            idp_conf["organization"]["url"][0])

        expected_ui_info = idp_conf["service"]["idp"]["ui_info"]
        ui_info = idp_desc["service"]["idp"]["ui_info"]
        assert ui_info["display_name"] == expected_ui_info["display_name"]
        assert ui_info["description"] == expected_ui_info["description"]
        assert ui_info["logo"] == expected_ui_info["logo"]
Ejemplo n.º 20
0
 def create_backend(self, sp_conf, idp_conf):
     setup_test_config(sp_conf, idp_conf)
     self.samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf,
                                                                  "disco_srv": DISCOSRV_URL},
                                    "base_url",
                                    "samlbackend")
Ejemplo n.º 21
0
class TestSAMLBackend:
    def assert_redirect_to_idp(self, redirect_response, idp_conf):
        assert redirect_response.status == "303 See Other"
        parsed = urlparse(redirect_response.message)
        redirect_location = "{parsed.scheme}://{parsed.netloc}{parsed.path}".format(parsed=parsed)
        assert redirect_location == idp_conf["service"]["idp"]["endpoints"]["single_sign_on_service"][0][0]
        assert "SAMLRequest" in parse_qs(parsed.query)

    def assert_redirect_to_discovery_server(self, redirect_response, sp_conf):
        assert redirect_response.status == "303 See Other"
        parsed = urlparse(redirect_response.message)
        redirect_location = "{parsed.scheme}://{parsed.netloc}{parsed.path}".format(parsed=parsed)
        assert redirect_location == DISCOSRV_URL

        request_params = dict(parse_qsl(parsed.query))
        assert request_params["return"] == sp_conf["service"]["sp"]["endpoints"]["discovery_response"][0][0]
        assert request_params["entityID"] == sp_conf["entityid"]

    def assert_authn_response(self, internal_resp):
        assert internal_resp.auth_info.auth_class_ref == PASSWORD
        expected_data = {'surname': ['Testsson 1'], 'mail': ['*****@*****.**'],
                         'displayname': ['Test Testsson'], 'givenname': ['Test 1'],
                         'edupersontargetedid': ['one!for!all']}
        assert expected_data == internal_resp.attributes

    def setup_test_config(self, sp_conf, idp_conf):
        idp_metadata_str = create_metadata_from_config_dict(idp_conf)
        sp_conf["metadata"]["inline"].append(idp_metadata_str)
        idp2_config = idp_conf.copy()
        idp2_config["entityid"] = "just_an_extra_idp"
        idp_metadata_str2 = create_metadata_from_config_dict(idp2_config)
        sp_conf["metadata"]["inline"].append(idp_metadata_str2)

        sp_metadata_str = create_metadata_from_config_dict(sp_conf)
        idp_conf["metadata"]["inline"] = [sp_metadata_str]

    @pytest.fixture(autouse=True)
    def create_backend(self, sp_conf, idp_conf):
        self.setup_test_config(sp_conf, idp_conf)
        self.samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf,
                                                                     "disco_srv": DISCOSRV_URL},
                                       "base_url",
                                       "samlbackend")

    def test_register_endpoints(self, sp_conf):
        """
        Tests the method register_endpoints
        """

        def get_path_from_url(url):
            return urlparse(url).path.lstrip("/")

        url_map = self.samlbackend.register_endpoints()
        all_sp_endpoints = [get_path_from_url(v[0][0]) for v in sp_conf["service"]["sp"]["endpoints"].values()]
        compiled_regex = [re.compile(regex) for regex, _ in url_map]
        for endp in all_sp_endpoints:
            assert any(p.match(endp) for p in compiled_regex)

    def test_start_auth_defaults_to_redirecting_to_discovery_server(self, context, sp_conf):
        resp = self.samlbackend.start_auth(context, InternalData())
        self.assert_redirect_to_discovery_server(resp, sp_conf)

    def test_full_flow(self, context, idp_conf, sp_conf):
        test_state_key = "test_state_key_456afgrh"
        response_binding = BINDING_HTTP_REDIRECT
        fakeidp = FakeIdP(USERS, config=IdPConfig().load(idp_conf, metadata_construction=False))

        context.state[test_state_key] = "my_state"

        # start auth flow (redirecting to discovery server)
        resp = self.samlbackend.start_auth(context, InternalData())
        self.assert_redirect_to_discovery_server(resp, sp_conf)

        # fake response from discovery server
        disco_resp = parse_qs(urlparse(resp.message).query)
        info = parse_qs(urlparse(disco_resp["return"][0]).query)
        info["entityID"] = idp_conf["entityid"]
        request_context = Context()
        request_context.request = info
        request_context.state = context.state

        # pass discovery response to backend and check that it redirects to the selected IdP
        resp = self.samlbackend.disco_response(request_context)
        self.assert_redirect_to_idp(resp, idp_conf)

        # fake auth response to the auth request
        req_params = dict(parse_qsl(urlparse(resp.message).query))
        url, fake_idp_resp = fakeidp.handle_auth_req(
            req_params["SAMLRequest"],
            req_params["RelayState"],
            BINDING_HTTP_REDIRECT,
            "testuser1",
            response_binding=response_binding)
        response_context = Context()
        response_context.request = fake_idp_resp
        response_context.state = request_context.state

        # pass auth response to backend and verify behavior
        self.samlbackend.authn_response(response_context, response_binding)
        context, internal_resp = self.samlbackend.auth_callback_func.call_args[0]
        assert self.samlbackend.name not in context.state
        assert context.state[test_state_key] == "my_state"
        self.assert_authn_response(internal_resp)

    def test_start_auth_redirects_directly_to_mirrored_idp(
            self, context, idp_conf):
        entityid = idp_conf["entityid"]
        context.decorate(Context.KEY_TARGET_ENTITYID, entityid)

        resp = self.samlbackend.start_auth(context, InternalData())
        self.assert_redirect_to_idp(resp, idp_conf)

    def test_redirect_to_idp_if_only_one_idp_in_metadata(self, context, sp_conf, idp_conf):
        sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)]
        # instantiate new backend, without any discovery service configured
        samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend")

        resp = samlbackend.start_auth(context, InternalData())
        self.assert_redirect_to_idp(resp, idp_conf)

    def test_always_redirect_to_discovery_service_if_using_mdq(self, context, sp_conf, idp_conf):
        # one IdP in the metadata, but MDQ also configured so should always redirect to the discovery service
        sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)]
        sp_conf["metadata"]["mdq"] = ["https://mdq.example.com"]
        samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf, "disco_srv": DISCOSRV_URL,},
                                  "base_url", "saml_backend")
        resp = samlbackend.start_auth(context, InternalData())
        self.assert_redirect_to_discovery_server(resp, sp_conf)

    def test_authn_request(self, context, idp_conf):
        resp = self.samlbackend.authn_request(context, idp_conf["entityid"])
        self.assert_redirect_to_idp(resp, idp_conf)
        req_params = dict(parse_qsl(urlparse(resp.message).query))
        assert context.state[self.samlbackend.name]["relay_state"] == req_params["RelayState"]

    def test_authn_response(self, context, idp_conf, sp_conf):
        response_binding = BINDING_HTTP_REDIRECT
        fakesp = FakeSP(SPConfig().load(sp_conf, metadata_construction=False))
        fakeidp = FakeIdP(USERS, config=IdPConfig().load(idp_conf, metadata_construction=False))
        destination, request_params = fakesp.make_auth_req(idp_conf["entityid"])
        url, auth_resp = fakeidp.handle_auth_req(request_params["SAMLRequest"], request_params["RelayState"],
                                                 BINDING_HTTP_REDIRECT,
                                                 "testuser1", response_binding=response_binding)

        context.request = auth_resp
        context.state[self.samlbackend.name] = {"relay_state": request_params["RelayState"]}
        self.samlbackend.authn_response(context, response_binding)

        context, internal_resp = self.samlbackend.auth_callback_func.call_args[0]
        self.assert_authn_response(internal_resp)
        assert self.samlbackend.name not in context.state

    @pytest.mark.skipif(
            saml2.__version__ < '4.6.1',
            reason="Optional NameID needs pysaml2 v4.6.1 or higher")
    def test_authn_response_no_name_id(self, context, idp_conf, sp_conf):
        response_binding = BINDING_HTTP_REDIRECT

        fakesp_conf = SPConfig().load(sp_conf, metadata_construction=False)
        fakesp = FakeSP(fakesp_conf)

        fakeidp_conf = IdPConfig().load(idp_conf, metadata_construction=False)
        fakeidp = FakeIdP(USERS, config=fakeidp_conf)

        destination, request_params = fakesp.make_auth_req(
            idp_conf["entityid"])

        # Use the fake IdP to mock up an authentication request that has no
        # <NameID> element.
        url, auth_resp = fakeidp.handle_auth_req_no_name_id(
            request_params["SAMLRequest"],
            request_params["RelayState"],
            BINDING_HTTP_REDIRECT,
            "testuser1",
            response_binding=response_binding)

        backend = self.samlbackend

        context.request = auth_resp
        context.state[backend.name] = {
            "relay_state": request_params["RelayState"],
        }
        backend.authn_response(context, response_binding)

        context, internal_resp = backend.auth_callback_func.call_args[0]
        self.assert_authn_response(internal_resp)
        assert backend.name not in context.state

    def test_authn_response_with_encrypted_assertion(self, sp_conf, context):
        with open(os.path.join(TEST_RESOURCE_BASE_PATH,
                               "idp_metadata_for_encrypted_signed_auth_response.xml")) as idp_metadata_file:
            sp_conf["metadata"]["inline"] = [idp_metadata_file.read()]
        samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf,
                                                                "disco_srv": DISCOSRV_URL},
                                  "base_url", "samlbackend")
        response_binding = BINDING_HTTP_REDIRECT
        relay_state = "test relay state"

        with open(os.path.join(TEST_RESOURCE_BASE_PATH,
                               "auth_response_with_encrypted_signed_assertion.xml")) as auth_response_file:
            auth_response = auth_response_file.read()
        context.request = {"SAMLResponse": deflate_and_base64_encode(auth_response), "RelayState": relay_state}

        context.state[self.samlbackend.name] = {"relay_state": relay_state}
        with open(os.path.join(TEST_RESOURCE_BASE_PATH, "encryption_key.pem")) as encryption_key_file:
            samlbackend.encryption_keys = [encryption_key_file.read()]

        assertion_issued_at = 1479315212
        with patch('saml2.validate.time_util.shift_time') as mock_shift_time, \
                patch('saml2.validate.time_util.utc_now') as mock_utc_now:
            mock_utc_now.return_value = assertion_issued_at + 1
            mock_shift_time.side_effect = [datetime.utcfromtimestamp(assertion_issued_at + 1),
                                     datetime.utcfromtimestamp(assertion_issued_at - 1)]
            samlbackend.authn_response(context, response_binding)

        context, internal_resp = samlbackend.auth_callback_func.call_args[0]
        assert Counter(internal_resp.attributes.keys()) == Counter({"mail", "givenname", "displayname", "surname"})

    def test_backend_reads_encryption_key_from_key_file(self, sp_conf):
        sp_conf["key_file"] = os.path.join(TEST_RESOURCE_BASE_PATH, "encryption_key.pem")
        samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf,
                                                                "disco_srv": DISCOSRV_URL},
                                  "base_url", "samlbackend")
        assert samlbackend.encryption_keys

    def test_backend_reads_encryption_key_from_encryption_keypair(self, sp_conf):
        del sp_conf["key_file"]
        sp_conf["encryption_keypairs"] = [{"key_file": os.path.join(TEST_RESOURCE_BASE_PATH, "encryption_key.pem")}]
        samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf,
                                                                "disco_srv": DISCOSRV_URL},
                                  "base_url", "samlbackend")
        assert samlbackend.encryption_keys

    def test_metadata_endpoint(self, context, sp_conf):
        resp = self.samlbackend._metadata_endpoint(context)
        headers = dict(resp.headers)
        assert headers["Content-Type"] == "text/xml"
        assert sp_conf["entityid"] in resp.message

    def test_get_metadata_desc(self, sp_conf, idp_conf):
        sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)]
        # instantiate new backend, with a single backing IdP
        samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend")
        entity_descriptions = samlbackend.get_metadata_desc()

        assert len(entity_descriptions) == 1

        idp_desc = entity_descriptions[0].to_dict()

        assert idp_desc["entityid"] == urlsafe_b64encode(idp_conf["entityid"].encode("utf-8")).decode("utf-8")
        assert idp_desc["contact_person"] == idp_conf["contact_person"]

        assert idp_desc["organization"]["name"][0] == tuple(idp_conf["organization"]["name"][0])
        assert idp_desc["organization"]["display_name"][0] == tuple(idp_conf["organization"]["display_name"][0])
        assert idp_desc["organization"]["url"][0] == tuple(idp_conf["organization"]["url"][0])

        expected_ui_info = idp_conf["service"]["idp"]["ui_info"]
        ui_info = idp_desc["service"]["idp"]["ui_info"]
        assert ui_info["display_name"] == expected_ui_info["display_name"]
        assert ui_info["description"] == expected_ui_info["description"]
        assert ui_info["logo"] == expected_ui_info["logo"]

    def test_get_metadata_desc_with_logo_without_lang(self, sp_conf, idp_conf):
        # add logo without 'lang'
        idp_conf["service"]["idp"]["ui_info"]["logo"] = [{"text": "https://idp.example.com/static/logo.png",
                                                          "width": "120", "height": "60"}]

        sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)]
        # instantiate new backend, with a single backing IdP
        samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend")
        entity_descriptions = samlbackend.get_metadata_desc()

        assert len(entity_descriptions) == 1

        idp_desc = entity_descriptions[0].to_dict()

        assert idp_desc["entityid"] == urlsafe_b64encode(idp_conf["entityid"].encode("utf-8")).decode("utf-8")
        assert idp_desc["contact_person"] == idp_conf["contact_person"]

        assert idp_desc["organization"]["name"][0] == tuple(idp_conf["organization"]["name"][0])
        assert idp_desc["organization"]["display_name"][0] == tuple(idp_conf["organization"]["display_name"][0])
        assert idp_desc["organization"]["url"][0] == tuple(idp_conf["organization"]["url"][0])

        expected_ui_info = idp_conf["service"]["idp"]["ui_info"]
        ui_info = idp_desc["service"]["idp"]["ui_info"]
        assert ui_info["display_name"] == expected_ui_info["display_name"]
        assert ui_info["description"] == expected_ui_info["description"]
        assert ui_info["logo"] == expected_ui_info["logo"]