From d1c3cfa5d995b57d8bd83a3a0ba9f9bd9edaeeb8 Mon Sep 17 00:00:00 2001 From: Ruslan Yemelianov Date: Wed, 17 Jun 2026 14:58:35 +0300 Subject: [PATCH] fix: add path-prefix matching for static npm registry credentials When multiple npm registries share the same hostname with different URL paths (e.g., two Artifactory virtual repos), the proxy incorrectly applies the first matching credential based on host+port only. This causes 403 errors when the wrong scoped token is sent. Add path-segment-aware matching to the static credential loop, consistent with OIDCRegistry.TryAuth behavior. Credentials configured for /team-a-npm/ will no longer be applied to /team-b-npm/ requests. Fixes #145 --- internal/handlers/npm_registry.go | 8 ++++++ internal/handlers/npm_registry_test.go | 36 +++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/internal/handlers/npm_registry.go b/internal/handlers/npm_registry.go index 02c7ad8..a5b2876 100644 --- a/internal/handlers/npm_registry.go +++ b/internal/handlers/npm_registry.go @@ -101,6 +101,14 @@ func (h *NPMRegistryHandler) HandleRequest(req *http.Request, ctx *goproxy.Proxy continue } + // Path-segment-aware matching prevents credentials configured for one + // path-scoped registry from being applied to sibling paths on the same + // host (e.g., /team-a-npm should not match /team-b-npm). + regPath := strings.TrimSuffix(regURL.Path, "/") + if regPath != "" && !strings.HasPrefix(req.URL.Path, regPath+"/") && req.URL.Path != regPath { + continue + } + if cred.token == "" && cred.password != "" { cred.token = cred.username + ":" + cred.password } diff --git a/internal/handlers/npm_registry_test.go b/internal/handlers/npm_registry_test.go index d89f1c7..f68d70c 100644 --- a/internal/handlers/npm_registry_test.go +++ b/internal/handlers/npm_registry_test.go @@ -59,9 +59,10 @@ func TestNPMRegistryHandler(t *testing.T) { req = handleRequestAndClose(handler, req, nil) assertHasTokenAuth(t, req, "Bearer", privateRegToken, "valid registry request with port and path") + // Sibling path on the same host should NOT receive credentials from /reg-path req = httptest.NewRequest("GET", "https://example.com/other-path/private-package", nil) req = handleRequestAndClose(handler, req, nil) - assertHasTokenAuth(t, req, "Bearer", privateRegToken, "different path") + assertUnauthenticated(t, req, "sibling path should not match") req = httptest.NewRequest("GET", "https://nexus.some-company.com/private-package", nil) req = handleRequestAndClose(handler, req, nil) @@ -87,3 +88,36 @@ func TestNPMRegistryHandler(t *testing.T) { req = handleRequestAndClose(handler, req, nil) assertHasBasicAuth(t, req, nexusUser, nexusPassword, "azure devops case insensitive registry request") } + +func TestNPMRegistryHandler_SameHostDifferentPaths(t *testing.T) { + teamAToken := "team-a-token" + teamBToken := "team-b-token" + credentials := config.Credentials{ + config.Credential{ + "type": "npm_registry", + "registry": "https://artifactory.example.com/api/npm/team-a-npm", + "token": teamAToken, + }, + config.Credential{ + "type": "npm_registry", + "registry": "https://artifactory.example.com/api/npm/team-b-npm", + "token": teamBToken, + }, + } + handler := NewNPMRegistryHandler(credentials) + + // Request to team-a path should use team-a token + req := httptest.NewRequest("GET", "https://artifactory.example.com/api/npm/team-a-npm/@scope/pkg", nil) + req = handleRequestAndClose(handler, req, nil) + assertHasTokenAuth(t, req, "Bearer", teamAToken, "team-a path should use team-a token") + + // Request to team-b path should use team-b token, not team-a + req = httptest.NewRequest("GET", "https://artifactory.example.com/api/npm/team-b-npm/@scope/pkg", nil) + req = handleRequestAndClose(handler, req, nil) + assertHasTokenAuth(t, req, "Bearer", teamBToken, "team-b path should use team-b token") + + // Request to unrelated path should not be authenticated + req = httptest.NewRequest("GET", "https://artifactory.example.com/api/npm/team-c-npm/@scope/pkg", nil) + req = handleRequestAndClose(handler, req, nil) + assertUnauthenticated(t, req, "unrelated path should not match any credential") +}