diff --git a/internal/handlers/npm_registry.go b/internal/handlers/npm_registry.go index 816b11c..b9c8bc9 100644 --- a/internal/handlers/npm_registry.go +++ b/internal/handlers/npm_registry.go @@ -1,7 +1,6 @@ package handlers import ( - "fmt" "net/http" "strings" "sync" @@ -46,18 +45,20 @@ func NewNPMRegistryHandler(creds config.Credentials) *NPMRegistryHandler { oidcCredential, _ := oidc.CreateOIDCCredential(cred) if oidcCredential != nil { - host := cred.Host() - if host == "" && registry != "" { - regURL, err := helpers.ParseURLLax(registry) - if err == nil { - host = regURL.Hostname() + maybeUrl := cred.GetString("host") + if maybeUrl == "" { + maybeUrl = cred.GetString("url") + if maybeUrl == "" { + maybeUrl = registry } } - if host != "" { - handler.oidcCredentials[host] = oidcCredential - logging.RequestLogf(nil, "registered %s OIDC credentials for npm registry: %s", oidcCredential.Provider(), host) + parsedUrl, err := helpers.ParseURLLax(maybeUrl) + if err == nil { + handler.oidcCredentials[parsedUrl.String()] = oidcCredential + logging.RequestLogf(nil, "registered %s OIDC credentials for npm registry: %s", oidcCredential.Provider(), parsedUrl.String()) + continue } - continue + logging.RequestLogf(nil, "failed to register OIDC credential for npm registry: %s", registry) } npmCred := npmRegistryCredentials{ @@ -86,20 +87,10 @@ func (h *NPMRegistryHandler) HandleRequest(req *http.Request, ctx *goproxy.Proxy } // Try OIDC credentials first - h.mutex.RLock() - oidcCred, hasOIDC := h.oidcCredentials[reqHost] - h.mutex.RUnlock() + authed := oidc.TryAuthOIDCRequestWithPrefix(&h.mutex, h.oidcCredentials, req, ctx) - if hasOIDC { - token, err := oidc.GetOrRefreshOIDCToken(oidcCred, req.Context()) - if err != nil { - logging.RequestLogf(ctx, "* failed to get token via OIDC for %s: %v", reqHost, err) - // Fall through to try static credentials - } else { - logging.RequestLogf(ctx, "* authenticating npm registry request with OIDC token (host: %s)", reqHost) - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - return req, nil - } + if authed { + return req, nil } // Fall back to static credentials diff --git a/internal/handlers/npm_registry_test.go b/internal/handlers/npm_registry_test.go index d89f1c7..6375e50 100644 --- a/internal/handlers/npm_registry_test.go +++ b/internal/handlers/npm_registry_test.go @@ -2,10 +2,13 @@ package handlers import ( "fmt" + "net/http" "net/http/httptest" "testing" "github.com/dependabot/proxy/internal/config" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" ) func TestNPMRegistryHandler(t *testing.T) { @@ -87,3 +90,87 @@ func TestNPMRegistryHandler(t *testing.T) { req = handleRequestAndClose(handler, req, nil) assertHasBasicAuth(t, req, nexusUser, nexusPassword, "azure devops case insensitive registry request") } + +func TestNPMRegistryHandler_OIDC_MultipleRegistriesSameHost(t *testing.T) { + // Setup environment for OIDC + t.Setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "http://oidc-url") + t.Setenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "oidc-token") + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + // Mock OIDC token endpoint + httpmock.RegisterResponder("GET", "http://oidc-url", + httpmock.NewStringResponder(200, `{"value": "github-jwt"}`)) + + // Mock AWS STS AssumeRoleWithWebIdentity + httpmock.RegisterResponder("POST", "https://sts.amazonaws.com", + func(req *http.Request) (*http.Response, error) { + roleArn := req.FormValue("RoleArn") + + // We need to return an XML response for AWS STS + xmlResp := fmt.Sprintf(` + + + + AKIA%s + secret-%s + session-%s + 2026-03-19T17:07:00Z + + +`, roleArn, roleArn, roleArn) + return httpmock.NewStringResponse(200, xmlResp), nil + }) + + // Mock AWS CodeArtifact GetAuthorizationToken + httpmock.RegisterResponder("POST", "https://codeartifact.us-east-1.amazonaws.com/v1/authorization-token", + func(req *http.Request) (*http.Response, error) { + sessionToken := req.Header.Get("X-Amz-Security-Token") + // The session token contains the role ARN in our mock + token := "final-token-for-" + sessionToken + return httpmock.NewJsonResponse(200, map[string]any{ + "authorizationToken": token, + "expiration": 3600, + }) + }) + + host := "mydomain-123456789000.d.codeartifact.us-east-1.amazonaws.com" + reg1Url := fmt.Sprintf("https://%s/npm/registry1/", host) + reg2Url := fmt.Sprintf("https://%s/npm/registry2/", host) + + credentials := config.Credentials{ + config.Credential{ + "type": "npm_registry", + "registry": reg1Url, + "aws-region": "us-east-1", + "account-id": "123456789012", + "role-name": "Role1", + "domain": "mydomain", + "domain-owner": "123456789012", + }, + config.Credential{ + "type": "npm_registry", + "registry": reg2Url, + "aws-region": "us-east-1", + "account-id": "123456789012", + "role-name": "Role2", + "domain": "mydomain", + "domain-owner": "123456789012", + }, + } + + handler := NewNPMRegistryHandler(credentials) + + // Test request to registry 1 + req1 := httptest.NewRequest("GET", reg1Url+"some-package", nil) + handleRequestAndClose(handler, req1, nil) + // Expectation: it should use Role1 + assert.Equal(t, "Bearer final-token-for-session-arn:aws:iam::123456789012:role/Role1", req1.Header.Get("Authorization"), "Registry 1 should use Role 1") + + // Test request to registry 2 + req2 := httptest.NewRequest("GET", reg2Url+"some-package", nil) + handleRequestAndClose(handler, req2, nil) + // Expectation: it should use Role2 + assert.Equal(t, "Bearer final-token-for-session-arn:aws:iam::123456789012:role/Role2", req2.Header.Get("Authorization"), "Registry 2 should use Role 2") +} diff --git a/internal/handlers/oidc_handling_test.go b/internal/handlers/oidc_handling_test.go index 59b6853..93bcd02 100644 --- a/internal/handlers/oidc_handling_test.go +++ b/internal/handlers/oidc_handling_test.go @@ -559,7 +559,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { }, urlMocks: []mockHttpRequest{}, expectedLogLines: []string{ - "registered aws OIDC credentials for npm registry: npm.example.com", + "registered aws OIDC credentials for npm registry: https://npm.example.com", }, urlsToAuthenticate: []string{ "https://npm.example.com/some-package", @@ -581,7 +581,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { }, urlMocks: []mockHttpRequest{}, expectedLogLines: []string{ - "registered azure OIDC credentials for npm registry: npm.example.com", + "registered azure OIDC credentials for npm registry: https://npm.example.com", }, urlsToAuthenticate: []string{ "https://npm.example.com/some-package", @@ -602,7 +602,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { }, urlMocks: []mockHttpRequest{}, expectedLogLines: []string{ - "registered jfrog OIDC credentials for npm registry: jfrog.example.com", + "registered jfrog OIDC credentials for npm registry: https://jfrog.example.com", }, urlsToAuthenticate: []string{ "https://jfrog.example.com/some-package",