def create_backend(self, internal_attributes, backend_config): self.oidc_backend = OpenIDConnectBackend(Mock(), internal_attributes, backend_config, "base_url", "oidc")
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)
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)