Skip to content

Commit 97b8148

Browse files
committed
Migrate npm handler to OIDCRegistry
Replace manual OIDC credential map, mutex, and direct hostname lookup with the shared OIDCRegistry type. npm previously stored OIDC credentials keyed by hostname only, which caused collisions when multiple npm registries shared a host. OIDCRegistry preserves the full registry URL, fixing this. The Cloudsmith-specific X-Api-Key handling is now provided by OIDCRegistry.TryAuth() automatically.
1 parent 5328230 commit 97b8148

File tree

2 files changed

+62
-43
lines changed

2 files changed

+62
-43
lines changed

internal/handlers/npm_registry.go

Lines changed: 7 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
package handlers
22

33
import (
4-
"fmt"
54
"net/http"
65
"strings"
7-
"sync"
86

97
"github.com/elazarl/goproxy"
108

@@ -17,9 +15,8 @@ import (
1715
// NPMRegistryHandler handles requests to NPM registries, adding auth to
1816
// requests to registries for which we have credentials.
1917
type NPMRegistryHandler struct {
20-
credentials []npmRegistryCredentials
21-
oidcCredentials map[string]*oidc.OIDCCredential
22-
mutex sync.RWMutex
18+
credentials []npmRegistryCredentials
19+
oidcRegistry *oidc.OIDCRegistry
2320
}
2421

2522
type npmRegistryCredentials struct {
@@ -33,8 +30,8 @@ type npmRegistryCredentials struct {
3330
// NewNPMRegistryHandler returns a new NPMRegistryHandler,
3431
func NewNPMRegistryHandler(creds config.Credentials) *NPMRegistryHandler {
3532
handler := NPMRegistryHandler{
36-
credentials: []npmRegistryCredentials{},
37-
oidcCredentials: make(map[string]*oidc.OIDCCredential),
33+
credentials: []npmRegistryCredentials{},
34+
oidcRegistry: oidc.NewOIDCRegistry(),
3835
}
3936

4037
for _, cred := range creds {
@@ -44,19 +41,7 @@ func NewNPMRegistryHandler(creds config.Credentials) *NPMRegistryHandler {
4441

4542
registry := cred.GetString("registry")
4643

47-
oidcCredential, _ := oidc.CreateOIDCCredential(cred)
48-
if oidcCredential != nil {
49-
host := cred.Host()
50-
if host == "" && registry != "" {
51-
regURL, err := helpers.ParseURLLax(registry)
52-
if err == nil {
53-
host = regURL.Hostname()
54-
}
55-
}
56-
if host != "" {
57-
handler.oidcCredentials[host] = oidcCredential
58-
logging.RequestLogf(nil, "registered %s OIDC credentials for npm registry: %s", oidcCredential.Provider(), host)
59-
}
44+
if _, _, ok := handler.oidcRegistry.Register(cred, []string{"registry", "url"}, "npm registry"); ok {
6045
continue
6146
}
6247

@@ -86,25 +71,8 @@ func (h *NPMRegistryHandler) HandleRequest(req *http.Request, ctx *goproxy.Proxy
8671
}
8772

8873
// Try OIDC credentials first
89-
h.mutex.RLock()
90-
oidcCred, hasOIDC := h.oidcCredentials[reqHost]
91-
h.mutex.RUnlock()
92-
93-
if hasOIDC {
94-
token, err := oidc.GetOrRefreshOIDCToken(oidcCred, req.Context())
95-
if err != nil {
96-
logging.RequestLogf(ctx, "* failed to get token via OIDC for %s: %v", reqHost, err)
97-
// Fall through to try static credentials
98-
} else {
99-
if oidcCred.Provider() == "cloudsmith" {
100-
logging.RequestLogf(ctx, "* authenticating npm registry request with OIDC API key (host: %s)", reqHost)
101-
req.Header.Set("X-Api-Key", token)
102-
} else {
103-
logging.RequestLogf(ctx, "* authenticating npm registry request with OIDC token (host: %s)", reqHost)
104-
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
105-
}
106-
return req, nil
107-
}
74+
if h.oidcRegistry.TryAuth(req, ctx) {
75+
return req, nil
10876
}
10977

11078
// Fall back to static credentials

internal/handlers/oidc_handling_test.go

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -721,7 +721,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) {
721721
},
722722
urlMocks: []mockHttpRequest{},
723723
expectedLogLines: []string{
724-
"registered aws OIDC credentials for npm registry: npm.example.com",
724+
"registered aws OIDC credentials for npm registry: https://npm.example.com",
725725
},
726726
urlsToAuthenticate: []string{
727727
"https://npm.example.com/some-package",
@@ -743,7 +743,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) {
743743
},
744744
urlMocks: []mockHttpRequest{},
745745
expectedLogLines: []string{
746-
"registered azure OIDC credentials for npm registry: npm.example.com",
746+
"registered azure OIDC credentials for npm registry: https://npm.example.com",
747747
},
748748
urlsToAuthenticate: []string{
749749
"https://npm.example.com/some-package",
@@ -764,7 +764,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) {
764764
},
765765
urlMocks: []mockHttpRequest{},
766766
expectedLogLines: []string{
767-
"registered jfrog OIDC credentials for npm registry: jfrog.example.com",
767+
"registered jfrog OIDC credentials for npm registry: https://jfrog.example.com",
768768
},
769769
urlsToAuthenticate: []string{
770770
"https://jfrog.example.com/some-package",
@@ -787,7 +787,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) {
787787
},
788788
urlMocks: []mockHttpRequest{},
789789
expectedLogLines: []string{
790-
"registered cloudsmith OIDC credentials for npm registry: cloudsmith.example.com",
790+
"registered cloudsmith OIDC credentials for npm registry: https://cloudsmith.example.com",
791791
},
792792
urlsToAuthenticate: []string{
793793
"https://cloudsmith.example.com/some-package",
@@ -1390,3 +1390,54 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) {
13901390
})
13911391
}
13921392
}
1393+
1394+
// TestNPMOIDCSameHostDifferentPaths verifies that two npm OIDC credentials on
1395+
// the same host with different URL paths do not collide — each request is
1396+
// authenticated with the credential whose path is the longest prefix match.
1397+
func TestNPMOIDCSameHostDifferentPaths(t *testing.T) {
1398+
httpmock.Activate()
1399+
defer httpmock.DeactivateAndReset()
1400+
1401+
tenantA := "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
1402+
tenantB := "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
1403+
clientId := "87654321-4321-4321-4321-210987654321"
1404+
1405+
tokenUrl := "https://token.actions.example.com" //nolint:gosec // test URL
1406+
httpmock.RegisterResponder("GET", tokenUrl,
1407+
httpmock.NewStringResponder(200, `{"count":1,"value":"sometoken"}`))
1408+
1409+
httpmock.RegisterResponder("POST", fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", tenantA),
1410+
httpmock.NewStringResponder(200, `{"access_token":"__token_A__","expires_in":3600,"token_type":"Bearer"}`))
1411+
httpmock.RegisterResponder("POST", fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", tenantB),
1412+
httpmock.NewStringResponder(200, `{"access_token":"__token_B__","expires_in":3600,"token_type":"Bearer"}`))
1413+
1414+
t.Setenv("ACTIONS_ID_TOKEN_REQUEST_URL", tokenUrl)
1415+
t.Setenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "sometoken")
1416+
1417+
creds := config.Credentials{
1418+
config.Credential{
1419+
"type": "npm_registry",
1420+
"url": "https://pkgs.example.com/org/feed-A",
1421+
"tenant-id": tenantA,
1422+
"client-id": clientId,
1423+
},
1424+
config.Credential{
1425+
"type": "npm_registry",
1426+
"url": "https://pkgs.example.com/org/feed-B",
1427+
"tenant-id": tenantB,
1428+
"client-id": clientId,
1429+
},
1430+
}
1431+
1432+
handler := NewNPMRegistryHandler(creds)
1433+
1434+
// Request to feed-A path should get token A
1435+
reqA := httptest.NewRequest("GET", "https://pkgs.example.com/org/feed-A/some-package", nil)
1436+
reqA = handleRequestAndClose(handler, reqA, nil)
1437+
assertHasTokenAuth(t, reqA, "Bearer", "__token_A__", "feed-A should use token A")
1438+
1439+
// Request to feed-B path should get token B
1440+
reqB := httptest.NewRequest("GET", "https://pkgs.example.com/org/feed-B/some-package", nil)
1441+
reqB = handleRequestAndClose(handler, reqB, nil)
1442+
assertHasTokenAuth(t, reqB, "Bearer", "__token_B__", "feed-B should use token B")
1443+
}

0 commit comments

Comments
 (0)