CVE-2022-39340: Missing Authorization (CWE-862)
value: OpenFGA is an authorization/permission engine. Prior to version 0.2.4, the `streamed-list-objects` endpoint was not validating the authorization header, resulting in disclosure of objects in the store. Users `openfga/openfga` versions 0.2.3 and prior who are exposing the OpenFGA service to the internet are vulnerable. Version 0.2.4 contains a patch for this issue.
CWE description: The product does not perform an authorization check when an actor attempts to access a resource or perform an action.
CVSS v3 base score: 5.3
Published: 2022-10-25T17:15Z
Repository: https://github.com/openfga/openfga
Language: Go
Files changed: pkg/cmd/service/service.go, pkg/cmd/service/service_test.go, server/middleware/logging.go, server/server.go
Commit date: 2022-10-21 09:19:48 -0700
Commit message: Merge pull request from GHSA-95x7-mh78-7w2r

* fix: add interceptors for streaming endpoints

* add tests

* remove unnecessary code
Diff: diff --git a/pkg/cmd/service/service.go b/pkg/cmd/service/service.go
index b99ad68..738573e 100644
--- a/pkg/cmd/service/service.go
+++ b/pkg/cmd/service/service.go
@@ -357,15 +357,20 @@ func BuildService(config *Config, logger logger.Logger) (*service, error) {
 	}
 	if err != nil {
 		return nil, errors.Errorf("failed to initialize authenticator: %v", err)
 	}
 
-	interceptors := []grpc.UnaryServerInterceptor{
+	unaryServerInterceptors := []grpc.UnaryServerInterceptor{
 		grpc_auth.UnaryServerInterceptor(middleware.AuthFunc(authenticator)),
 		middleware.NewErrorLoggingInterceptor(logger),
 	}
 
+	streamingServerInterceptors := []grpc.StreamServerInterceptor{
+		grpc_auth.StreamServerInterceptor(middleware.AuthFunc(authenticator)),
+		middleware.NewStreamingErrorLoggingInterceptor(logger),
+	}
+
 	grpcHostAddr, grpcHostPort, err := net.SplitHostPort(config.GRPC.Addr)
 	if err != nil {
 		return nil, errors.Errorf("`grpc.addr` config must be in the form [host]:port")
 	}
 
@@ -413,11 +418,12 @@ func BuildService(config *Config, logger logger.Logger) (*service, error) {
 		},
 		ResolveNodeLimit:       config.ResolveNodeLimit,
 		ChangelogHorizonOffset: config.ChangelogHorizonOffset,
 		ListObjectsDeadline:    config.ListObjectsDeadline,
 		ListObjectsMaxResults:  config.ListObjectsMaxResults,
-		UnaryInterceptors:      interceptors,
+		UnaryInterceptors:      unaryServerInterceptors,
+		StreamingInterceptors:  streamingServerInterceptors,
 		MuxOptions:             nil,
 	})
 	if err != nil {
 		return nil, errors.Errorf("failed to initialize openfga server: %v", err)
 	}
diff --git a/pkg/cmd/service/service_test.go b/pkg/cmd/service/service_test.go
index 7a1d2f9..916df74 100644
--- a/pkg/cmd/service/service_test.go
+++ b/pkg/cmd/service/service_test.go
@@ -5,10 +5,11 @@ import (
 	"crypto/rand"
 	"crypto/rsa"
 	"crypto/tls"
 	"crypto/x509"
 	"crypto/x509/pkix"
+	"encoding/json"
 	"encoding/pem"
 	"fmt"
 	"io"
 	"math/big"
 	"net/http"
@@ -22,19 +23,21 @@ import (
 	"github.com/cenkalti/backoff/v4"
 	"github.com/go-errors/errors"
 	"github.com/hashicorp/go-retryablehttp"
 	"github.com/openfga/openfga/pkg/logger"
 	"github.com/openfga/openfga/server/authn/mocks"
+	serverErrors "github.com/openfga/openfga/server/errors"
 	"github.com/stretchr/testify/require"
 	"github.com/tidwall/gjson"
 	openfgapb "go.buf.build/openfga/go/openfga/api/openfga/v1"
 	"golang.org/x/sync/errgroup"
 	"google.golang.org/grpc"
 	grpcbackoff "google.golang.org/grpc/backoff"
 	"google.golang.org/grpc/credentials"
 	"google.golang.org/grpc/credentials/insecure"
 	healthv1pb "google.golang.org/grpc/health/grpc_health_v1"
+	"google.golang.org/protobuf/encoding/protojson"
 )
 
 func TestMain(m *testing.M) {
 	_, filename, _, _ := runtime.Caller(0)
 	dir := path.Join(path.Dir(filename), "../../..")
@@ -203,13 +206,14 @@ func createCertsAndKeys(t *testing.T) certHandle {
 		serverKeyFile:  serverKeyFile.Name(),
 	}
 }
 
 type authTest struct {
-	_name         string
-	authHeader    string
-	expectedError string
+	_name                 string
+	authHeader            string
+	expectedErrorResponse *serverErrors.ErrorResponse
+	expectedStatusCode    int
 }
 
 func TestBuildServiceWithNoAuth(t *testing.T) {
 	config, err := GetServiceConfig()
 	require.NoError(t, err)
@@ -249,60 +253,120 @@ func TestBuildServiceWithPresharedKeyAuthentication(t *testing.T) {
 	})
 
 	ensureServiceUp(t, service, nil, true)
 
 	tests := []authTest{{
-		_name:         "Header with incorrect key fails",
-		authHeader:    "Bearer incorrectkey",
-		expectedError: "unauthenticated",
+		_name:      "Header with incorrect key fails",
+		authHeader: "Bearer incorrectkey",
+		expectedErrorResponse: &serverErrors.ErrorResponse{
+			Code:    "unauthenticated",
+			Message: "unauthenticated",
+		},
+		expectedStatusCode: 401,
 	}, {
-		_name:         "Missing header fails",
-		authHeader:    "",
-		expectedError: "missing bearer token",
+		_name:      "Missing header fails",
+		authHeader: "",
+		expectedErrorResponse: &serverErrors.ErrorResponse{
+			Code:    "bearer_token_missing",
+			Message: "missing bearer token",
+		},
+		expectedStatusCode: 401,
 	}, {
-		_name:         "Correct key one succeeds",
-		authHeader:    "Bearer KEYONE",
-		expectedError: "",
+		_name:              "Correct key one succeeds",
+		authHeader:         fmt.Sprintf("Bearer %s", config.Authn.AuthnPresharedKeyConfig.Keys[0]),
+		expectedStatusCode: 200,
 	}, {
-		_name:         "Correct key two succeeds",
-		authHeader:    "Bearer KEYTWO",
-		expectedError: "",
+		_name:              "Correct key two succeeds",
+		authHeader:         fmt.Sprintf("Bearer %s", config.Authn.AuthnPresharedKeyConfig.Keys[1]),
+		expectedStatusCode: 200,
 	}}
 
 	retryClient := retryablehttp.NewClient()
 	for _, test := range tests {
 		t.Run(test._name, func(t *testing.T) {
-			payload := strings.NewReader(`{"name": "some-store-name"}`)
-			req, err := retryablehttp.NewRequest("POST", fmt.Sprintf("http://localhost:%d/stores", service.GetHTTPAddrPort().Port()), payload)
-			require.NoError(t, err, "Failed to construct request")
-			req.Header.Set("content-type", "application/json")
-			req.Header.Set("authorization", test.authHeader)
-
-			res, err := retryClient.Do(req)
-			require.NoError(t, err, "Failed to execute request")
-
-			defer res.Body.Close()
-			body, err := io.ReadAll(res.Body)
-			require.NoError(t, err, "Failed to read response")
-
-			stringBody := string(body)
-
-			if test.expectedError == "" && strings.Contains(stringBody, "code") {
-				t.Fatalf("Expected no error but got '%v'", stringBody)
-			}
+			tryGetStores(t, test, service, retryClient)
+		})
 
-			if !strings.Contains(stringBody, test.expectedError) && test.expectedError != "" {
-				t.Fatalf("Expected '%v' to contain '%v'", stringBody, test.expectedError)
-			}
+		t.Run(test._name+"/streaming", func(t *testing.T) {
+			tryStreamingListObjects(t, test, service, retryClient, config.Authn.AuthnPresharedKeyConfig.Keys[0])
 		})
 	}
 
 	cancel()
 	require.NoError(t, service.Close(ctx))
 	require.NoError(t, g.Wait())
 }
 
+func tryStreamingListObjects(t *testing.T, test authTest, service *service, retryClient *retryablehttp.Client, validToken string) {
+	// create a store
+	createStorePayload := strings.NewReader(`{"name": "some-store-name"}`)
+	req, err := retryablehttp.NewRequest("POST", fmt.Sprintf("http://localhost:%d/stores", service.GetHTTPAddrPort().Port()), createStorePayload)
+	require.NoError(t, err, "Failed to construct create store request")
+	req.Header.Set("content-type", "application/json")
+	req.Header.Set("authorization", fmt.Sprintf("Bearer %s", validToken))
+	res, err := retryClient.Do(req)
+	require.NoError(t, err, "Failed to execute create store request")
+	defer res.Body.Close()
+	body, err := io.ReadAll(res.Body)
+	require.NoError(t, err, "Failed to read create store response")
+	var createStoreResponse openfgapb.CreateStoreResponse
+	err = protojson.Unmarshal(body, &createStoreResponse)
+	require.NoError(t, err, "Failed to unmarshal create store response")
+
+	// create an authorization model
+	authModelPayload := strings.NewReader(`{"type_definitions":[{"type":"document","relations":{"owner":{"this":{}}}}]}`)
+	req, err = retryablehttp.NewRequest("POST", fmt.Sprintf("http://localhost:%d/stores/%s/authorization-models", service.GetHTTPAddrPort().Port(), createStoreResponse.Id), authModelPayload)
+	require.NoError(t, err, "Failed to construct create authorization model request")
+	req.Header.Set("content-type", "application/json")
+	req.Header.Set("authorization", fmt.Sprintf("Bearer %s", validToken))
+	_, err = retryClient.Do(req)
+	require.NoError(t, err, "Failed to execute create authorization model request")
+
+	// call one streaming endpoint
+	listObjectsPayload := strings.NewReader(`{"type": "document", "user": "anne", "relation": "owner"}`)
+	req, err = retryablehttp.NewRequest("POST", fmt.Sprintf("http://localhost:%d/stores/%s/streamed-list-objects", service.GetHTTPAddrPort().Port(), createStoreResponse.Id), listObjectsPayload)
+	require.NoError(t, err, "Failed to construct request")
+	req.Header.Set("content-type", "application/json")
+	req.Header.Set("authorization", test.authHeader)
+
+	res, err = retryClient.Do(req)
+	require.Equal(t, test.expectedStatusCode, res.StatusCode)
+	require.NoError(t, err, "Failed to execute streaming request")
+
+	defer res.Body.Close()
+	body, err = io.ReadAll(res.Body)
+	require.NoError(t, err, "Failed to read response")
+
+	if test.expectedErrorResponse != nil {
+		require.Contains(t, string(body), fmt.Sprintf(",\"message\":\"%s\"", test.expectedErrorResponse.Message))
+	}
+}
+
+func tryGetStores(t *testing.T, test authTest, service *service, retryClient *retryablehttp.Client) {
+	req, err := retryablehttp.NewRequest("GET", fmt.Sprintf("http://localhost:%d/stores", service.GetHTTPAddrPort().Port()), nil)
+	require.NoError(t, err, "Failed to construct request")
+	req.Header.Set("content-type", "application/json")
+	req.Header.Set("authorization", test.authHeader)
+
+	res, err := retryClient.Do(req)
+	require.NoError(t, err, "Failed to execute request")
+	require.Equal(t, test.expectedStatusCode, res.StatusCode)
+
+	defer res.Body.Close()
+	body, err := io.ReadAll(res.Body)
+	require.NoError(t, err, "Failed to read response")
+
+	if test.expectedErrorResponse != nil {
+		var actualErrorResponse serverErrors.ErrorResponse
+		err = json.Unmarshal(body, &actualErrorResponse)
+
+		require.NoError(t, err, "Failed to unmarshal response")
+
+		require.Equal(t, test.expectedErrorResponse, &actualErrorResponse)
+	}
+}
+
 func TestHTTPServerWithCORS(t *testing.T) {
 	config, err := DefaultConfigWithRandomPorts()
 	require.NoError(t, err)
 
 	config.Authn.Method = "preshared"
@@ -434,47 +498,39 @@ func TestBuildServerWithOIDCAuthentication(t *testing.T) {
 
 	trustedToken, err := trustedIssuerServer.GetToken("openfga.dev", "some-user")
 	require.NoError(t, err)
 
 	tests := []authTest{{
-		_name:         "Header with invalid token fails",
-		authHeader:    "Bearer incorrecttoken",
-		expectedError: "invalid bearer token",
+		_name:      "Header with invalid token fails",
+		authHeader: "Bearer incorrecttoken",
+		expectedErrorResponse: &serverErrors.ErrorResponse{
+			Code:    "auth_failed_invalid_bearer_token",
+			Message: "invalid bearer token",
+		},
+		expectedStatusCode: 401,
 	}, {
-		_name:         "Missing header fails",
-		authHeader:    "",
-		expectedError: "missing bearer token",
+		_name:      "Missing header fails",
+		authHeader: "",
+		expectedErrorResponse: &serverErrors.ErrorResponse{
+			Code:    "bearer_token_missing",
+			Message: "missing bearer token",
+		},
+		expectedStatusCode: 401,
 	}, {
-		_name:         "Correct token succeeds",
-		authHeader:    "Bearer " + trustedToken,
-		expectedError: "",
+		_name:              "Correct token succeeds",
+		authHeader:         "Bearer " + trustedToken,
+		expectedStatusCode: 200,
 	}}
 
 	retryClient := retryablehttp.NewClient()
 	for _, test := range tests {
 		t.Run(test._name, func(t *testing.T) {
-			payload := strings.NewReader(`{"name": "some-store-name"}`)
-			req, err := retryablehttp.NewRequest("POST", fmt.Sprintf("http://localhost:%d/stores", service.GetHTTPAddrPort().Port()), payload)
-			require.NoError(t, err, "Failed to construct request")
-			req.Header.Set("content-type", "application/json")
-			req.Header.Set("authorization", test.authHeader)
-
-			res, err := retryClient.Do(req)
-			require.NoError(t, err, "Failed to execute request")
-
-			defer res.Body.Close()
-			body, err := io.ReadAll(res.Body)
-			require.NoError(t, err, "Failed to read response")
-
-			stringBody := string(body)
-			if test.expectedError == "" && strings.Contains(stringBody, "code") {
-				t.Fatalf("Expected no error but got %v", stringBody)
-			}
+			tryGetStores(t, test, service, retryClient)
+		})
 
-			if !strings.Contains(stringBody, test.expectedError) && test.expectedError != "" {
-				t.Fatalf("Expected %v to contain %v", stringBody, test.expectedError)
-			}
+		t.Run(test._name+"/streaming", func(t *testing.T) {
+			tryStreamingListObjects(t, test, service, retryClient, trustedToken)
 		})
 	}
 
 	cancel()
 	require.NoError(t, service.Close(ctx))
diff --git a/server/middleware/logging.go b/server/middleware/logging.go
index 760f935..9d0769d 100644
--- a/server/middleware/logging.go
+++ b/server/middleware/logging.go
@@ -23,5 +23,20 @@ func NewErrorLoggingInterceptor(logger logger.Logger) grpc.UnaryServerIntercepto
 		}
 
 		return resp, nil
 	}
 }
+
+func NewStreamingErrorLoggingInterceptor(logger logger.Logger) grpc.StreamServerInterceptor {
+	return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
+		err := handler(srv, stream)
+		if err != nil {
+			var e error
+			if internalError, ok := err.(serverErrors.InternalError); ok {
+				e = internalError.Internal()
+			}
+			logger.Error("grpc_error", zap.Error(e), zap.String("public_error", err.Error()))
+		}
+
+		return err
+	}
+}
diff --git a/server/server.go b/server/server.go
index 6b77d92..f73bb10 100644
--- a/server/server.go
+++ b/server/server.go
@@ -79,10 +79,11 @@ type Config struct {
 	ResolveNodeLimit       uint32
 	ChangelogHorizonOffset int
 	ListObjectsDeadline    time.Duration
 	ListObjectsMaxResults  uint32
 	UnaryInterceptors      []grpc.UnaryServerInterceptor
+	StreamingInterceptors  []grpc.StreamServerInterceptor
 	MuxOptions             []runtime.ServeMuxOption
 }
 
 type GRPCServerConfig struct {
 	Addr      netip.AddrPort
@@ -484,17 +485,23 @@ func (s *Server) IsReady(ctx context.Context) (bool, error) {
 
 // Run starts server execution, and blocks until complete, returning any server errors. To close the
 // server cancel the provided ctx.
 func (s *Server) Run(ctx context.Context) error {
 
-	interceptors := []grpc.UnaryServerInterceptor{
+	unaryServerInterceptors := []grpc.UnaryServerInterceptor{
 		grpc_validator.UnaryServerInterceptor(),
 	}
-	interceptors = append(interceptors, s.config.UnaryInterceptors...)
+	unaryServerInterceptors = append(unaryServerInterceptors, s.config.UnaryInterceptors...)
+
+	streamingInterceptors := []grpc.StreamServerInterceptor{
+		grpc_validator.StreamServerInterceptor(),
+	}
+	streamingInterceptors = append(streamingInterceptors, s.config.StreamingInterceptors...)
 
 	opts := []grpc.ServerOption{
-		grpc.ChainUnaryInterceptor(interceptors...),
+		grpc.ChainUnaryInterceptor(unaryServerInterceptors...),
+		grpc.ChainStreamInterceptor(streamingInterceptors...),
 	}
 
 	if s.config.GRPCServer.TLSConfig != nil {
 		creds, err := credentials.NewServerTLSFromFile(s.config.GRPCServer.TLSConfig.CertPath, s.config.GRPCServer.TLSConfig.KeyPath)
 		if err != nil {

Vulnerable code: 	interceptors := []grpc.UnaryServerInterceptor{
		UnaryInterceptors:      interceptors,
	_name         string
	authHeader    string
	expectedError string
		_name:         "Header with incorrect key fails",
		authHeader:    "Bearer incorrectkey",
		expectedError: "unauthenticated",
		_name:         "Missing header fails",
		authHeader:    "",
		expectedError: "missing bearer token",
		_name:         "Correct key one succeeds",
		authHeader:    "Bearer KEYONE",
		expectedError: "",
		_name:         "Correct key two succeeds",
		authHeader:    "Bearer KEYTWO",
		expectedError: "",
			payload := strings.NewReader(`{"name": "some-store-name"}`)
			req, err := retryablehttp.NewRequest("POST", fmt.Sprintf("http://localhost:%d/stores", service.GetHTTPAddrPort().Port()), payload)
			require.NoError(t, err, "Failed to construct request")
			req.Header.Set("content-type", "application/json")
			req.Header.Set("authorization", test.authHeader)

			res, err := retryClient.Do(req)
			require.NoError(t, err, "Failed to execute request")

			defer res.Body.Close()
			body, err := io.ReadAll(res.Body)
			require.NoError(t, err, "Failed to read response")

			stringBody := string(body)

			if test.expectedError == "" && strings.Contains(stringBody, "code") {
				t.Fatalf("Expected no error but got '%v'", stringBody)
			}
			if !strings.Contains(stringBody, test.expectedError) && test.expectedError != "" {
				t.Fatalf("Expected '%v' to contain '%v'", stringBody, test.expectedError)
			}
		_name:         "Header with invalid token fails",
		authHeader:    "Bearer incorrecttoken",
		expectedError: "invalid bearer token",
		_name:         "Missing header fails",
		authHeader:    "",
		expectedError: "missing bearer token",
		_name:         "Correct token succeeds",
		authHeader:    "Bearer " + trustedToken,
		expectedError: "",
			payload := strings.NewReader(`{"name": "some-store-name"}`)
			req, err := retryablehttp.NewRequest("POST", fmt.Sprintf("http://localhost:%d/stores", service.GetHTTPAddrPort().Port()), payload)
			require.NoError(t, err, "Failed to construct request")
			req.Header.Set("content-type", "application/json")
			req.Header.Set("authorization", test.authHeader)

			res, err := retryClient.Do(req)
			require.NoError(t, err, "Failed to execute request")

			defer res.Body.Close()
			body, err := io.ReadAll(res.Body)
			require.NoError(t, err, "Failed to read response")

			stringBody := string(body)
			if test.expectedError == "" && strings.Contains(stringBody, "code") {
				t.Fatalf("Expected no error but got %v", stringBody)
			}
			if !strings.Contains(stringBody, test.expectedError) && test.expectedError != "" {
				t.Fatalf("Expected %v to contain %v", stringBody, test.expectedError)
			}
	interceptors := []grpc.UnaryServerInterceptor{
	interceptors = append(interceptors, s.config.UnaryInterceptors...)
		grpc.ChainUnaryInterceptor(interceptors...),
Fixed code: 	unaryServerInterceptors := []grpc.UnaryServerInterceptor{
	streamingServerInterceptors := []grpc.StreamServerInterceptor{
		grpc_auth.StreamServerInterceptor(middleware.AuthFunc(authenticator)),
		middleware.NewStreamingErrorLoggingInterceptor(logger),
	}

		UnaryInterceptors:      unaryServerInterceptors,
		StreamingInterceptors:  streamingServerInterceptors,
	"encoding/json"
	serverErrors "github.com/openfga/openfga/server/errors"
	"google.golang.org/protobuf/encoding/protojson"
	_name                 string
	authHeader            string
	expectedErrorResponse *serverErrors.ErrorResponse
	expectedStatusCode    int
		_name:      "Header with incorrect key fails",
		authHeader: "Bearer incorrectkey",
		expectedErrorResponse: &serverErrors.ErrorResponse{
			Code:    "unauthenticated",
			Message: "unauthenticated",
		},
		expectedStatusCode: 401,
		_name:      "Missing header fails",
		authHeader: "",
		expectedErrorResponse: &serverErrors.ErrorResponse{
			Code:    "bearer_token_missing",
			Message: "missing bearer token",
		},
		expectedStatusCode: 401,
		_name:              "Correct key one succeeds",
		authHeader:         fmt.Sprintf("Bearer %s", config.Authn.AuthnPresharedKeyConfig.Keys[0]),
		expectedStatusCode: 200,
		_name:              "Correct key two succeeds",
		authHeader:         fmt.Sprintf("Bearer %s", config.Authn.AuthnPresharedKeyConfig.Keys[1]),
		expectedStatusCode: 200,
			tryGetStores(t, test, service, retryClient)
		})
		t.Run(test._name+"/streaming", func(t *testing.T) {
			tryStreamingListObjects(t, test, service, retryClient, config.Authn.AuthnPresharedKeyConfig.Keys[0])
func tryStreamingListObjects(t *testing.T, test authTest, service *service, retryClient *retryablehttp.Client, validToken string) {
	// create a store
	createStorePayload := strings.NewReader(`{"name": "some-store-name"}`)
	req, err := retryablehttp.NewRequest("POST", fmt.Sprintf("http://localhost:%d/stores", service.GetHTTPAddrPort().Port()), createStorePayload)
	require.NoError(t, err, "Failed to construct create store request")
	req.Header.Set("content-type", "application/json")
	req.Header.Set("authorization", fmt.Sprintf("Bearer %s", validToken))
	res, err := retryClient.Do(req)
	require.NoError(t, err, "Failed to execute create store request")
	defer res.Body.Close()
	body, err := io.ReadAll(res.Body)
	require.NoError(t, err, "Failed to read create store response")
	var createStoreResponse openfgapb.CreateStoreResponse
	err = protojson.Unmarshal(body, &createStoreResponse)
	require.NoError(t, err, "Failed to unmarshal create store response")

	// create an authorization model
	authModelPayload := strings.NewReader(`{"type_definitions":[{"type":"document","relations":{"owner":{"this":{}}}}]}`)
	req, err = retryablehttp.NewRequest("POST", fmt.Sprintf("http://localhost:%d/stores/%s/authorization-models", service.GetHTTPAddrPort().Port(), createStoreResponse.Id), authModelPayload)
	require.NoError(t, err, "Failed to construct create authorization model request")
	req.Header.Set("content-type", "application/json")
	req.Header.Set("authorization", fmt.Sprintf("Bearer %s", validToken))
	_, err = retryClient.Do(req)
	require.NoError(t, err, "Failed to execute create authorization model request")

	// call one streaming endpoint
	listObjectsPayload := strings.NewReader(`{"type": "document", "user": "anne", "relation": "owner"}`)
	req, err = retryablehttp.NewRequest("POST", fmt.Sprintf("http://localhost:%d/stores/%s/streamed-list-objects", service.GetHTTPAddrPort().Port(), createStoreResponse.Id), listObjectsPayload)
	require.NoError(t, err, "Failed to construct request")
	req.Header.Set("content-type", "application/json")
	req.Header.Set("authorization", test.authHeader)

	res, err = retryClient.Do(req)
	require.Equal(t, test.expectedStatusCode, res.StatusCode)
	require.NoError(t, err, "Failed to execute streaming request")

	defer res.Body.Close()
	body, err = io.ReadAll(res.Body)
	require.NoError(t, err, "Failed to read response")

	if test.expectedErrorResponse != nil {
		require.Contains(t, string(body), fmt.Sprintf(",\"message\":\"%s\"", test.expectedErrorResponse.Message))
	}
}

func tryGetStores(t *testing.T, test authTest, service *service, retryClient *retryablehttp.Client) {
	req, err := retryablehttp.NewRequest("GET", fmt.Sprintf("http://localhost:%d/stores", service.GetHTTPAddrPort().Port()), nil)
	require.NoError(t, err, "Failed to construct request")
	req.Header.Set("content-type", "application/json")
	req.Header.Set("authorization", test.authHeader)

	res, err := retryClient.Do(req)
	require.NoError(t, err, "Failed to execute request")
	require.Equal(t, test.expectedStatusCode, res.StatusCode)

	defer res.Body.Close()
	body, err := io.ReadAll(res.Body)
	require.NoError(t, err, "Failed to read response")

	if test.expectedErrorResponse != nil {
		var actualErrorResponse serverErrors.ErrorResponse
		err = json.Unmarshal(body, &actualErrorResponse)

		require.NoError(t, err, "Failed to unmarshal response")

		require.Equal(t, test.expectedErrorResponse, &actualErrorResponse)
	}
}

		_name:      "Header with invalid token fails",
		authHeader: "Bearer incorrecttoken",
		expectedErrorResponse: &serverErrors.ErrorResponse{
			Code:    "auth_failed_invalid_bearer_token",
			Message: "invalid bearer token",
		},
		expectedStatusCode: 401,
		_name:      "Missing header fails",
		authHeader: "",
		expectedErrorResponse: &serverErrors.ErrorResponse{
			Code:    "bearer_token_missing",
			Message: "missing bearer token",
		},
		expectedStatusCode: 401,
		_name:              "Correct token succeeds",
		authHeader:         "Bearer " + trustedToken,
		expectedStatusCode: 200,
			tryGetStores(t, test, service, retryClient)
		})
		t.Run(test._name+"/streaming", func(t *testing.T) {
			tryStreamingListObjects(t, test, service, retryClient, trustedToken)

func NewStreamingErrorLoggingInterceptor(logger logger.Logger) grpc.StreamServerInterceptor {
	return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
		err := handler(srv, stream)
		if err != nil {
			var e error
			if internalError, ok := err.(serverErrors.InternalError); ok {
				e = internalError.Internal()
			}
			logger.Error("grpc_error", zap.Error(e), zap.String("public_error", err.Error()))
		}

		return err
	}
}
	StreamingInterceptors  []grpc.StreamServerInterceptor
	unaryServerInterceptors := []grpc.UnaryServerInterceptor{
	unaryServerInterceptors = append(unaryServerInterceptors, s.config.UnaryInterceptors...)

	streamingInterceptors := []grpc.StreamServerInterceptor{
		grpc_validator.StreamServerInterceptor(),
	}
	streamingInterceptors = append(streamingInterceptors, s.config.StreamingInterceptors...)
		grpc.ChainUnaryInterceptor(unaryServerInterceptors...),
		grpc.ChainStreamInterceptor(streamingInterceptors...),