Beispiel #1
0
 def create_backend(self, internal_attributes, backend_config):
     self.oidc_backend = OpenIDConnectBackend(Mock(), internal_attributes,
                                              backend_config, "base_url",
                                              "oidc")
Beispiel #2
0
 def create_backend(self, internal_attributes, backend_config):
     self.oidc_backend = OpenIDConnectBackend(Mock(), internal_attributes, backend_config, "base_url", "oidc")
Beispiel #3
0
class TestOpenIDConnectBackend(object):
    @pytest.fixture(autouse=True)
    def create_backend(self, internal_attributes, backend_config):
        self.oidc_backend = OpenIDConnectBackend(Mock(), internal_attributes,
                                                 backend_config, "base_url",
                                                 "oidc")

    @pytest.fixture
    def backend_config(self):
        return {
            "client": {
                "client_metadata": {
                    "client_id": CLIENT_ID,
                    "client_secret":
                    "ZJYCqe3GGRvdrudKyZS0XhGv_Z45DuKhCUk0gBR1vZk",
                    "application_type": "web",
                    "application_name": "SATOSA Test",
                    "contacts": ["*****@*****.**"],
                    "redirect_uris": ["https://client.test.com/authz_cb"],
                    "response_types": ["code"],
                    "subject_type": "pairwise"
                },
                "auth_req_params": {
                    "response_type": "code id_token token",
                    "scope": "openid foo"
                }
            },
            "provider_metadata": {
                "issuer": ISSUER,
                "authorization_endpoint": ISSUER + "/authorization",
                "token_endpoint": ISSUER + "/token",
                "userinfo_endpoint": ISSUER + "/userinfo",
                "registration_endpoint": ISSUER + "/registration",
                "jwks_uri": ISSUER + "/static/jwks"
            }
        }

    @pytest.fixture
    def internal_attributes(self):
        return {
            "attributes": {
                "givenname": {
                    "openid": ["given_name"]
                },
                "mail": {
                    "openid": ["email"]
                },
                "edupersontargetedid": {
                    "openid": ["sub"]
                },
                "surname": {
                    "openid": ["family_name"]
                }
            }
        }

    @pytest.fixture
    def userinfo(self):
        return {
            "given_name": "Test",
            "family_name": "Devsson",
            "email": "*****@*****.**",
            "sub": "username"
        }

    @pytest.fixture(scope="session")
    def signing_key(self):
        return RSAKey(key=RSA.generate(2048), alg="RS256")

    def assert_expected_attributes(self, actual_attributes):
        user_claims = self.userinfo()
        attr_map = self.internal_attributes()
        expected_attributes = {}
        for out_attr, in_mapping in attr_map["attributes"].items():
            expected_attributes[out_attr] = [
                user_claims[in_mapping["openid"][0]]
            ]

        assert actual_attributes == expected_attributes

    def setup_jwks_uri(self, jwks_uri, key):
        responses.add(responses.GET,
                      jwks_uri,
                      body=json.dumps({"keys": [key.serialize()]}),
                      status=200,
                      content_type="application/json")

    def setup_token_endpoint(self, token_endpoint_url, signing_key):
        id_token_claims = {
            "iss": ISSUER,
            "sub": self.userinfo()["sub"],
            "aud": CLIENT_ID,
            "nonce": NONCE,
            "exp": time.time() + 3600,
            "iat": time.time()
        }
        id_token = IdToken(**id_token_claims).to_jwt([signing_key],
                                                     signing_key.alg)
        token_response = {
            "access_token": "SlAV32hkKG",
            "token_type": "Bearer",
            "refresh_token": "8xLOxBtZp8",
            "expires_in": 3600,
            "id_token": id_token
        }
        responses.add(responses.POST,
                      token_endpoint_url,
                      body=json.dumps(token_response),
                      status=200,
                      content_type="application/json")

    def setup_userinfo_endpoint(self, userinfo_endpoint_url):
        responses.add(responses.POST,
                      userinfo_endpoint_url,
                      body=json.dumps(self.userinfo()),
                      status=200,
                      content_type="application/json")

    def get_redirect_uri_path(self, backend_config):
        return urlparse(backend_config["client"]["client_metadata"]
                        ["redirect_uris"][0]).path.lstrip("/")

    @pytest.fixture
    def incoming_authn_response(self, context, backend_config):
        oidc_state = "my state"
        context.path = self.get_redirect_uri_path(backend_config)
        context.request = {
            "code": "F+R4uWbN46U+Bq9moQPC4lEvRd2De4o=",
            "state": oidc_state
        }

        state_data = {STATE_KEY: oidc_state, NONCE_KEY: NONCE}
        context.state[self.oidc_backend.name] = state_data
        return context

    def test_register_endpoints(self, backend_config):
        redirect_uri_path = self.get_redirect_uri_path(backend_config)
        url_map = self.oidc_backend.register_endpoints()
        regex, callback = url_map[0]
        assert re.search(regex, redirect_uri_path)
        assert callback == self.oidc_backend.response_endpoint

    def test_translate_response_to_internal_response(self, userinfo):
        internal_response = self.oidc_backend._translate_response(
            userinfo, ISSUER)
        assert internal_response.user_id == userinfo["sub"]
        self.assert_expected_attributes(internal_response.attributes)

    @responses.activate
    def test_response_endpoint(self, backend_config, signing_key,
                               incoming_authn_response):
        self.setup_jwks_uri(backend_config["provider_metadata"]["jwks_uri"],
                            signing_key)
        self.setup_token_endpoint(
            backend_config["provider_metadata"]["token_endpoint"], signing_key)
        self.setup_userinfo_endpoint(
            backend_config["provider_metadata"]["userinfo_endpoint"])

        self.oidc_backend.response_endpoint(incoming_authn_response)
        assert self.oidc_backend.name not in incoming_authn_response.state

        args = self.oidc_backend.auth_callback_func.call_args[0]
        assert isinstance(args[0], Context)
        assert isinstance(args[1], InternalResponse)
        self.assert_expected_attributes(args[1].attributes)

    def test_start_auth_redirects_to_provider_authorization_endpoint(
            self, context, backend_config):
        auth_response = self.oidc_backend.start_auth(context, None)
        assert isinstance(auth_response, Response)

        login_url = auth_response.message
        parsed = urlparse(login_url)
        assert login_url.startswith(
            backend_config["provider_metadata"]["authorization_endpoint"])
        auth_params = dict(parse_qsl(parsed.query))
        assert auth_params["scope"] == backend_config["client"][
            "auth_req_params"]["scope"]
        assert auth_params["response_type"] == backend_config["client"][
            "auth_req_params"]["response_type"]
        assert auth_params["client_id"] == backend_config["client"][
            "client_metadata"]["client_id"]
        assert auth_params["redirect_uri"] == backend_config["client"][
            "client_metadata"]["redirect_uris"][0]
        assert "state" in auth_params
        assert "nonce" in auth_params

    @responses.activate
    def test_entire_flow(self, context, backend_config):
        self.setup_userinfo_endpoint(
            backend_config["provider_metadata"]["userinfo_endpoint"])
        auth_response = self.oidc_backend.start_auth(context, None)
        auth_params = dict(parse_qsl(urlparse(auth_response.message).query))

        access_token = 12345
        context.request = {
            "state": auth_params["state"],
            "access_token": access_token
        }
        self.oidc_backend.response_endpoint(context)
        assert self.oidc_backend.name not in context.state
        args = self.oidc_backend.auth_callback_func.call_args[0]
        self.assert_expected_attributes(args[1].attributes)
Beispiel #4
0
class TestOpenIDConnectBackend(object):
    @pytest.fixture(autouse=True)
    def create_backend(self, internal_attributes, backend_config):
        self.oidc_backend = OpenIDConnectBackend(Mock(), internal_attributes, backend_config, "base_url", "oidc")

    @pytest.fixture
    def backend_config(self):
        return {
            "client": {
                "client_metadata": {
                    "client_id": CLIENT_ID,
                    "client_secret": "ZJYCqe3GGRvdrudKyZS0XhGv_Z45DuKhCUk0gBR1vZk",
                    "application_type": "web",
                    "application_name": "SATOSA Test",
                    "contacts": ["*****@*****.**"],
                    "redirect_uris": ["https://client.test.com/authz_cb"],
                    "response_types": ["code"],
                    "subject_type": "pairwise"
                },
                "auth_req_params": {
                    "response_type": "code id_token token",
                    "scope": "openid foo"
                }
            },
            "provider_metadata": {
                "issuer": ISSUER,
                "authorization_endpoint": ISSUER + "/authorization",
                "token_endpoint": ISSUER + "/token",
                "userinfo_endpoint": ISSUER + "/userinfo",
                "registration_endpoint": ISSUER + "/registration",
                "jwks_uri": ISSUER + "/static/jwks"
            }
        }

    @pytest.fixture
    def internal_attributes(self):
        return {
            "attributes": {
                "givenname": {"openid": ["given_name"]},
                "mail": {"openid": ["email"]},
                "edupersontargetedid": {"openid": ["sub"]},
                "surname": {"openid": ["family_name"]}
            }
        }

    @pytest.fixture
    def userinfo(self):
        return {
            "given_name": "Test",
            "family_name": "Devsson",
            "email": "*****@*****.**",
            "sub": "username"
        }

    @pytest.fixture(scope="session")
    def signing_key(self):
        return RSAKey(key=RSA.generate(2048), alg="RS256")

    def assert_expected_attributes(self, actual_attributes):
        user_claims = self.userinfo()
        attr_map = self.internal_attributes()
        expected_attributes = {}
        for out_attr, in_mapping in attr_map["attributes"].items():
            expected_attributes[out_attr] = [user_claims[in_mapping["openid"][0]]]

        assert actual_attributes == expected_attributes

    def setup_jwks_uri(self, jwks_uri, key):
        responses.add(
            responses.GET,
            jwks_uri,
            body=json.dumps({"keys": [key.serialize()]}),
            status=200,
            content_type="application/json")

    def setup_token_endpoint(self, token_endpoint_url, signing_key):
        id_token_claims = {
            "iss": ISSUER,
            "sub": self.userinfo()["sub"],
            "aud": CLIENT_ID,
            "nonce": NONCE,
            "exp": time.time() + 3600,
            "iat": time.time()
        }
        id_token = IdToken(**id_token_claims).to_jwt([signing_key], signing_key.alg)
        token_response = {
            "access_token": "SlAV32hkKG",
            "token_type": "Bearer",
            "refresh_token": "8xLOxBtZp8",
            "expires_in": 3600,
            "id_token": id_token
        }
        responses.add(responses.POST,
                      token_endpoint_url,
                      body=json.dumps(token_response),
                      status=200,
                      content_type="application/json")

    def setup_userinfo_endpoint(self, userinfo_endpoint_url):
        responses.add(responses.POST,
                      userinfo_endpoint_url,
                      body=json.dumps(self.userinfo()),
                      status=200,
                      content_type="application/json")

    def get_redirect_uri_path(self, backend_config):
        return urlparse(backend_config["client"]["client_metadata"]["redirect_uris"][0]).path.lstrip("/")

    @pytest.fixture
    def incoming_authn_response(self, context, backend_config):
        oidc_state = "my state"
        context.path = self.get_redirect_uri_path(backend_config)
        context.request = {
            "code": "F+R4uWbN46U+Bq9moQPC4lEvRd2De4o=",
            "state": oidc_state
        }

        state_data = {
            STATE_KEY: oidc_state,
            NONCE_KEY: NONCE
        }
        context.state[self.oidc_backend.name] = state_data
        return context

    def test_register_endpoints(self, backend_config):
        redirect_uri_path = self.get_redirect_uri_path(backend_config)
        url_map = self.oidc_backend.register_endpoints()
        regex, callback = url_map[0]
        assert re.search(regex, redirect_uri_path)
        assert callback == self.oidc_backend.response_endpoint

    def test_translate_response_to_internal_response(self, userinfo):
        internal_response = self.oidc_backend._translate_response(userinfo, ISSUER)
        assert internal_response.user_id == userinfo["sub"]
        self.assert_expected_attributes(internal_response.attributes)

    @responses.activate
    def test_response_endpoint(self, backend_config, signing_key, incoming_authn_response):
        self.setup_jwks_uri(backend_config["provider_metadata"]["jwks_uri"], signing_key)
        self.setup_token_endpoint(backend_config["provider_metadata"]["token_endpoint"], signing_key)
        self.setup_userinfo_endpoint(backend_config["provider_metadata"]["userinfo_endpoint"])

        self.oidc_backend.response_endpoint(incoming_authn_response)
        assert self.oidc_backend.name not in incoming_authn_response.state

        args = self.oidc_backend.auth_callback_func.call_args[0]
        assert isinstance(args[0], Context)
        assert isinstance(args[1], InternalResponse)
        self.assert_expected_attributes(args[1].attributes)

    def test_start_auth_redirects_to_provider_authorization_endpoint(self, context, backend_config):
        auth_response = self.oidc_backend.start_auth(context, None)
        assert isinstance(auth_response, Response)

        login_url = auth_response.message
        parsed = urlparse(login_url)
        assert login_url.startswith(backend_config["provider_metadata"]["authorization_endpoint"])
        auth_params = dict(parse_qsl(parsed.query))
        assert auth_params["scope"] == backend_config["client"]["auth_req_params"]["scope"]
        assert auth_params["response_type"] == backend_config["client"]["auth_req_params"]["response_type"]
        assert auth_params["client_id"] == backend_config["client"]["client_metadata"]["client_id"]
        assert auth_params["redirect_uri"] == backend_config["client"]["client_metadata"]["redirect_uris"][0]
        assert "state" in auth_params
        assert "nonce" in auth_params

    @responses.activate
    def test_entire_flow(self, context, backend_config):
        self.setup_userinfo_endpoint(backend_config["provider_metadata"]["userinfo_endpoint"])
        auth_response = self.oidc_backend.start_auth(context, None)
        auth_params = dict(parse_qsl(urlparse(auth_response.message).query))

        access_token = 12345
        context.request = {"state": auth_params["state"], "access_token": access_token}
        self.oidc_backend.response_endpoint(context)
        assert self.oidc_backend.name not in context.state
        args = self.oidc_backend.auth_callback_func.call_args[0]
        self.assert_expected_attributes(args[1].attributes)