diff --git a/sources/dev/authentication-go/internal/middleware/middleware.go b/sources/dev/authentication-go/internal/middleware/middleware.go index ff2b859..f3d9da1 100644 --- a/sources/dev/authentication-go/internal/middleware/middleware.go +++ b/sources/dev/authentication-go/internal/middleware/middleware.go @@ -367,8 +367,18 @@ func (l *RateLimiter) Middleware() gin.HandlerFunc { // --- CORS --- -// CORS mirrors the tower-http CorsLayer: echo allowed origins (or "*"), allow -// any method/header, and short-circuit preflight requests. +// corsAllowedHeaders is the explicit allow-list echoed on preflight responses. +// It must name every non-safelisted request header clients send. Authorization +// is listed explicitly on purpose: per the Fetch spec the "*" wildcard does NOT +// cover Authorization, so a wildcard would silently break cross-origin Bearer +// (and Basic, for /oauth) requests. X-Client-Id is the app identity header. +const corsAllowedHeaders = "Authorization, Content-Type, X-Client-Id" + +// corsAllowedMethods is the explicit method allow-list for preflight responses. +const corsAllowedMethods = "GET, POST, PATCH, DELETE, OPTIONS" + +// CORS mirrors the tower-http CorsLayer: echo allowed origins (or "*") and the +// explicit method/header allow-lists, then short-circuit preflight requests. func CORS(allowedOrigins string) gin.HandlerFunc { wildcard := strings.TrimSpace(allowedOrigins) == "*" set := map[string]bool{} @@ -388,8 +398,8 @@ func CORS(allowedOrigins string) gin.HandlerFunc { c.Header("Access-Control-Allow-Origin", origin) c.Header("Vary", "Origin") } - c.Header("Access-Control-Allow-Methods", "*") - c.Header("Access-Control-Allow-Headers", "*") + c.Header("Access-Control-Allow-Methods", corsAllowedMethods) + c.Header("Access-Control-Allow-Headers", corsAllowedHeaders) if c.Request.Method == http.MethodOptions { c.AbortWithStatus(http.StatusNoContent) return diff --git a/sources/dev/authentication-go/internal/server/integration_test.go b/sources/dev/authentication-go/internal/server/integration_test.go index a2cdba2..58fed33 100644 --- a/sources/dev/authentication-go/internal/server/integration_test.go +++ b/sources/dev/authentication-go/internal/server/integration_test.go @@ -241,9 +241,11 @@ func TestRegisterLoginRefreshLogout(t *testing.T) { }, ta.clientHeaders()) mustStatus(t, reuse, http.StatusUnauthorized) + // Logout authenticates via X-Client-Id (like refresh) and revokes by the + // refresh_token in the body, so it does not require a Bearer access token. logout := ta.do(http.MethodPost, "/api/auth/logout", map[string]any{ "refresh_token": refreshResp.RefreshToken, - }, ta.bearer(refreshResp.AccessToken)) + }, ta.clientHeaders()) mustStatus(t, logout, http.StatusOK) } diff --git a/sources/dev/authentication-go/internal/server/server.go b/sources/dev/authentication-go/internal/server/server.go index 7ca99c7..8fc8ea4 100644 --- a/sources/dev/authentication-go/internal/server/server.go +++ b/sources/dev/authentication-go/internal/server/server.go @@ -49,7 +49,9 @@ func NewRouter(repo repository.Repository, jwt *auth.JWTManager, cfg *config.Con oauth.POST("/introspect", h.Introspect) } - // Auth endpoints (X-Client-Id, except logout which is Bearer). + // Auth endpoints (X-Client-Id). Logout revokes by refresh_token in the body, + // so it uses ClientApp() like refresh instead of requiring a Bearer token; + // this keeps logout idempotent even when the access token has expired. authGroup := r.Group("/api/auth") authGroup.Use(authLimiter.Middleware()) { @@ -57,7 +59,7 @@ func NewRouter(repo repository.Repository, jwt *auth.JWTManager, cfg *config.Con authGroup.POST("/login", am.ClientApp(), h.Login) authGroup.POST("/provider/:provider_id/login", am.ClientApp(), h.ProviderLogin) authGroup.POST("/refresh", am.ClientApp(), h.Refresh) - authGroup.POST("/logout", am.AuthenticatedUser(), h.Logout) + authGroup.POST("/logout", am.ClientApp(), h.Logout) } // User endpoints (Bearer).