From 19563de449448d5caf8b89cc574b2ca1d1552491 Mon Sep 17 00:00:00 2001 From: Tomas Virgl <739690+tvi@users.noreply.github.com> Date: Mon, 18 May 2026 21:01:08 -0700 Subject: [PATCH 1/2] fix(auth): rename X-Team-Id header to X-Team-ID --- packages/auth/pkg/auth/consts.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/auth/pkg/auth/consts.go b/packages/auth/pkg/auth/consts.go index 852aa3da71..075d609484 100644 --- a/packages/auth/pkg/auth/consts.go +++ b/packages/auth/pkg/auth/consts.go @@ -6,7 +6,7 @@ const ( HeaderAuthorization = "Authorization" HeaderSupabaseToken = "X-Supabase-Token" HeaderSupabaseTeam = "X-Supabase-Team" - HeaderTeamID = "X-Team-Id" + HeaderTeamID = "X-Team-ID" HeaderAdminToken = "X-Admin-Token" // Token prefixes. From 4b201958796376020a2dca31a383083acfde57d5 Mon Sep 17 00:00:00 2001 From: Tomas Virgl <739690+tvi@users.noreply.github.com> Date: Mon, 18 May 2026 21:10:25 -0700 Subject: [PATCH 2/2] test(api): verify openapi spec security scheme headers match auth constants --- packages/api/internal/api/spec_test.go | 147 +++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 packages/api/internal/api/spec_test.go diff --git a/packages/api/internal/api/spec_test.go b/packages/api/internal/api/spec_test.go new file mode 100644 index 0000000000..bb2c200a8b --- /dev/null +++ b/packages/api/internal/api/spec_test.go @@ -0,0 +1,147 @@ +package api + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/gin-gonic/gin" + middleware "github.com/oapi-codegen/gin-middleware" + "github.com/stretchr/testify/require" + + "github.com/e2b-dev/infra/packages/auth/pkg/auth" +) + +// TestSpecSecuritySchemeHeaderNames asserts that the OpenAPI spec's security +// scheme header names stay in sync with the constants defined in the shared +// auth package. A drift between the two leads to silent authentication +// failures. +func TestSpecSecuritySchemeHeaderNames(t *testing.T) { + t.Parallel() + + swagger, err := GetSpec() + require.NoError(t, err) + require.NotNil(t, swagger.Components) + + cases := []struct { + schemeName string + expectedHeader string + }{ + {"ApiKeyAuth", auth.HeaderAPIKey}, + {"Supabase1TokenAuth", auth.HeaderSupabaseToken}, + {"Supabase2TeamAuth", auth.HeaderSupabaseTeam}, + {"AuthProviderTeamAuth", auth.HeaderTeamID}, + {"AdminTokenAuth", auth.HeaderAdminToken}, + } + + for _, tc := range cases { + t.Run(tc.schemeName, func(t *testing.T) { + t.Parallel() + + ref, ok := swagger.Components.SecuritySchemes[tc.schemeName] + require.True(t, ok, "security scheme %q not found in spec", tc.schemeName) + require.NotNil(t, ref.Value, "security scheme %q has nil value", tc.schemeName) + require.Equal(t, tc.expectedHeader, ref.Value.Name, + "security scheme %q header name in spec does not match auth constant", tc.schemeName) + }) + } +} + +// TestAuthProviderTeamAuthHeaderRoutes verifies that a request carrying the +// X-Team-ID header reaches the AuthenticationFunc via the openapi3filter, and +// that the header value is the one extracted by the auth middleware. +func TestAuthProviderTeamAuthHeaderRoutes(t *testing.T) { + t.Parallel() + + swagger, err := GetSpec() + require.NoError(t, err) + // Clear servers to avoid spurious validation warnings/errors with httptest hosts. + swagger.Servers = nil + + const wantToken = "team-token-value" + + var ( + gotSchemeName string + gotToken string + ) + + // Map each security scheme to the header that satisfies it. We require + // the header for every scheme to model the production setup, where + // missing headers cause auth failure. + schemeHeaders := map[string]string{ + "ApiKeyAuth": auth.HeaderAPIKey, + "AccessTokenAuth": auth.HeaderAuthorization, + "Supabase1TokenAuth": auth.HeaderSupabaseToken, + "Supabase2TeamAuth": auth.HeaderSupabaseTeam, + "AuthProviderBearerAuth": auth.HeaderAuthorization, + "AuthProviderTeamAuth": auth.HeaderTeamID, + "AdminTokenAuth": auth.HeaderAdminToken, + } + + authFn := func(_ context.Context, input *openapi3filter.AuthenticationInput) error { + header, ok := schemeHeaders[input.SecuritySchemeName] + if !ok { + return http.ErrNoCookie + } + + value := input.RequestValidationInput.Request.Header.Get(header) + if value == "" { + return http.ErrNoCookie + } + + if input.SecuritySchemeName == "AuthProviderTeamAuth" { + gotSchemeName = input.SecuritySchemeName + gotToken = value + } + + return nil + } + + r := gin.New() + r.Use(middleware.OapiRequestValidatorWithOptions(swagger, &middleware.Options{ + Options: openapi3filter.Options{ + AuthenticationFunc: authFn, + MultiError: true, + }, + SilenceServersWarning: true, + })) + + // Register a catch-all handler so any path with the right method succeeds + // once auth passes. The OAPI middleware will reject unknown routes before + // the handler runs, but valid spec routes will get here. + r.NoRoute(func(c *gin.Context) { + c.Status(http.StatusOK) + }) + r.Any("/*any", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + // Pick a route protected by AuthProviderTeamAuth. /api-keys requires it. + makeReq := func(headers map[string]string) *httptest.ResponseRecorder { + req := httptest.NewRequest(http.MethodGet, "/api-keys", nil) + for k, v := range headers { + req.Header.Set(k, v) + } + rr := httptest.NewRecorder() + r.ServeHTTP(rr, req) + + return rr + } + + // Without any auth header, no scheme passes -> middleware rejects. + rr := makeReq(nil) + require.NotEqual(t, http.StatusOK, rr.Code, "request without auth header should be rejected") + + // With both the AuthProviderBearerAuth Authorization header and the + // AuthProviderTeamAuth X-Team-ID header, the second security requirement + // in the spec is satisfied. + rr = makeReq(map[string]string{ + auth.HeaderAuthorization: "Bearer some-bearer-token", + auth.HeaderTeamID: wantToken, + }) + require.Equal(t, http.StatusOK, rr.Code, "request with %s header should pass auth (body: %s)", auth.HeaderTeamID, rr.Body.String()) + require.Equal(t, "AuthProviderTeamAuth", gotSchemeName) + require.Equal(t, wantToken, gotToken) +}