Skip to content

Add GCP OIDC token exchange for Google Artifact Registry#108

Merged
kbukum1 merged 1 commit intomainfrom
kbukum1/gcp-oidc-support
Apr 23, 2026
Merged

Add GCP OIDC token exchange for Google Artifact Registry#108
kbukum1 merged 1 commit intomainfrom
kbukum1/gcp-oidc-support

Conversation

@kbukum1
Copy link
Copy Markdown
Contributor

@kbukum1 kbukum1 commented Apr 21, 2026

What are you trying to accomplish?

Add Google Artifact Registry as the fifth OIDC provider in the Dependabot proxy, alongside Azure DevOps, AWS CodeArtifact, JFrog, and Cloudsmith. The proxy mints a short-lived Google access token from the GitHub Actions OIDC JWT and injects it on outbound registry requests — no static credentials stored.

Token exchange flow:

  • GitHub Actions OIDC JWT → Google STS token exchange
  • Optional IAM Credentials generateAccessToken impersonation (when service-account is configured)
  • Direct Workload Identity Federation when no service-account is present

Auth injection:

  • Authorization: Bearer <token> for most registry types (Maven, npm, Python, etc.)
  • Basic oauth2accesstoken:<token> for Docker hosts (*-docker.pkg.dev)

Anything you want to highlight for special attention from reviewers?

  • Follows the Cloudsmith OIDC pattern: parameter struct, factory branch, token exchange function, ForDevOps wrapper, and header injection in OIDCRegistry.TryAuth.
  • The Docker Basic auth pattern (oauth2accesstoken:<token>) is new — all 4 existing providers use Bearer or X-Api-Key. GCP Artifact Registry's Docker API requires this specific format.
  • STS request uses camelCase JSON (Google's gRPC-transcoded convention), not RFC 8693 snake_case. The response uses snake_case.
  • No new Go dependencies — uses raw net/http matching existing providers.

How will you know you have accomplished your goal?

  • go test ./... -count=1 — all packages pass
  • go vet ./... — clean
  • go build ./... — succeeds

Checklist

  • I have run the complete test suite to ensure all tests and linters pass.
  • I have thoroughly tested my code changes to ensure they work as expected, including adding additional tests for new functionality.
  • I have written clear and descriptive commit messages.
  • I have provided a detailed description of the changes in the pull request, including the problem it addresses, how it fixes the problem, and any relevant details about the implementation.
  • I have ensured that the code is well-documented and easy to understand.

Copilot AI review requested due to automatic review settings April 21, 2026 19:21
@kbukum1 kbukum1 requested a review from a team as a code owner April 21, 2026 19:21
@kbukum1 kbukum1 marked this pull request as draft April 21, 2026 19:21
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Google Artifact Registry as an additional OIDC provider in the Dependabot MITM proxy, enabling short-lived GCP access tokens to be minted from GitHub Actions OIDC and injected on outbound registry requests (including Docker’s oauth2accesstoken:<token> Basic auth requirement).

Changes:

  • Introduces GCPOIDCParameters and wires GCP into OIDC credential creation and token refresh flow.
  • Implements GCP token exchange via Google STS, with optional IAM Credentials generateAccessToken impersonation.
  • Extends OIDC request auth injection logic and adds/updates tests to cover Bearer vs Docker Basic auth behavior.
Show a summary per file
File Description
internal/oidc/actions_oidc.go Adds GCP STS token exchange + optional IAM impersonation functions and DevOps wrapper.
internal/oidc/actions_oidc_test.go Adds unit tests for GCP STS/IAM flows and error handling.
internal/oidc/oidc_credential.go Adds GCPOIDCParameters and integrates GCP into CreateOIDCCredential + token refresh switch.
internal/oidc/oidc_credential_test.go Adds coverage for GCP parameter parsing/default audience behavior.
internal/oidc/oidc_registry.go Adds GCP-specific auth injection (Bearer vs Docker Basic auth).
internal/oidc/oidc_registry_test.go Adds registry-level tests verifying GCP Bearer and Docker Basic auth injection.
internal/handlers/oidc_handling_test.go Adds end-to-end handler test cases ensuring GCP OIDC auth is applied across multiple registries/handlers.

Copilot's findings

  • Files reviewed: 7/7 changed files
  • Comments generated: 1

Comment thread internal/oidc/actions_oidc.go
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

Comments suppressed due to low confidence (1)

internal/oidc/actions_oidc.go:802

  • The IAM token expiry check uses remaining < 5*time.Minute, but the OIDC token cache also subtracts 5 minutes from ExpiresIn when setting tokenExpiry. If remaining is exactly 5 minutes, the cached token will be considered expired immediately and trigger a refresh loop. Consider using <= 5*time.Minute here (or increasing the minimum remaining time) to ensure ExpiresIn stays positive after the cache’s 5-minute buffer.
	remaining := time.Until(expireTime)
	if remaining < 5*time.Minute {
		return nil, fmt.Errorf("GCP IAM token expires too soon (%v remaining, service-account: %s)", remaining, params.ServiceAccount)
	}
  • Files reviewed: 7/7 changed files
  • Comments generated: 2

Comment thread internal/oidc/actions_oidc.go Outdated
Comment thread internal/oidc/actions_oidc.go
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 7/7 changed files
  • Comments generated: 0 new

Copy link
Copy Markdown
Contributor Author

kbukum1 commented Apr 21, 2026

Follow-up: Commonize OIDC handler test cases

While working on this PR, I noticed that internal/handlers/oidc_handling_test.go has a highly repetitive structure — each ecosystem (Cargo, Composer, NPM, Maven, Python, NuGet, Hex, Pub, RubyGems, Go, Docker, Helm, Terraform) repeats nearly identical test blocks for each OIDC provider (AWS, Azure, JFrog, Cloudsmith, GCP), totaling ~65 hand-written cases.

This could be collapsed with a table-driven approach: define each ecosystem's metadata once (handler factory, credential type, URL key, log label, URLs), then generate provider variants in a loop — reducing to ~13 ecosystem entries + a shared template (~75% less test code, same coverage).

Suggesting this as a follow-up refactor to keep this PR focused.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 7/7 changed files
  • Comments generated: 1

Comment thread internal/oidc/actions_oidc.go Outdated
@kbukum1 kbukum1 requested a review from Copilot April 21, 2026 21:01
Copy link
Copy Markdown
Contributor Author

kbukum1 commented Apr 21, 2026

Follow-up: Extract shared ForDevOps wrapper

All 5 Get*AccessTokenForDevOps functions (actions_oidc.go:272,373,549,637,817) repeat the same flow:

  1. IsOIDCConfigured() check
  2. GetToken(ctx, audience) (Azure uses GetTokenForAzureADExchange)
  3. GetXxxAccessToken(ctx, params, githubToken)
  4. Wrap error with provider name

A shared helper with a tokenExchangeFn callback could eliminate ~15 lines per provider. Worth evaluating as a follow-up refactor.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 7/7 changed files
  • Comments generated: 0 new

Copy link
Copy Markdown
Contributor Author

kbukum1 commented Apr 21, 2026

Follow-up: Extract JSON POST helpers for OIDC token exchange

All OIDC providers (Azure, JFrog, AWS, Cloudsmith, GCP) repeat the same ~15-line HTTP boilerplate: marshal → create request → set standard headers → execute → read body. GCP does it twice (STS + IAM).

Proposed approach: build/execute split

Rather than a single helper with growing parameter lists, split into two:

// buildJSONRequest creates a JSON POST request with standard proxy headers.
// Caller can modify the request (add headers, etc.) before executing it.
func buildJSONRequest(ctx context.Context, url string, body any) (*http.Request, error) {
    bodyJSON, err := json.Marshal(body)
    if err != nil {
        return nil, fmt.Errorf("failed to marshal request: %w", err)
    }
    req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(bodyJSON))
    if err != nil {
        return nil, fmt.Errorf("failed to create request: %w", err)
    }
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Accept", "application/json")
    req.Header.Set("User-Agent", "dependabot-proxy/1.0")
    return req, nil
}

// executeJSONRequest executes a request and returns the status code and raw body.
func executeJSONRequest(req *http.Request) (int, []byte, error) {
    client := &http.Client{Timeout: 10 * time.Second}
    // ... execute, read body, return statusCode, body, err
}

Usage example (GCP IAM with custom auth header):

req, err := buildJSONRequest(ctx, iamURL, iamReqBody)
if err != nil { ... }
req.Header.Set("Authorization", "Bearer "+stsToken)  // custom header

status, body, err := executeJSONRequest(req)
if err != nil { ... }

Why this design:

  • buildJSONRequest standardizes the proxy OIDC conventions (JSON content type, User-Agent, timeout)
  • Caller has full control between build and execute — add any headers, query params, or modifications without changing the helper
  • Provider-specific logic stays inline — status checks, error context, response parsing remain visible at the call site
  • No parameter proliferation — future needs don't require helper signature changes

Copy link
Copy Markdown
Contributor Author

kbukum1 commented Apr 21, 2026

Follow-up: Consider provider interface to replace type switches

Adding a new OIDC provider currently requires synchronized edits in 3 places:

  1. oidc_credential.go:114-168 — parse config fields into params struct
  2. oidc_credential.go:202-214 — type switch to dispatch token exchange
  3. oidc_registry.go:143-158 — type switch to apply auth to requests

An OIDCProvider interface with ExchangeToken() and ApplyAuth() methods could eliminate switches #2 and #3, reducing new-provider additions to a single edit point (config parsing). Worth considering if more providers are expected.

Copy link
Copy Markdown
Contributor Author

kbukum1 commented Apr 21, 2026

Follow-up: Simplify oidc_credential_test.go parameter assertions

The type switch at oidc_credential_test.go:388-436 does field-by-field comparison of simple value structs across all 5 providers (~50 lines). Since these parameter types have no unexported fields or special comparison logic, the entire block could be replaced with:

assert.Equal(t, tc.expectedParameters, actual.parameters)

assert.Equal uses reflect.DeepEqual, which compares all fields and gives clear diffs on failure. The type check is already covered by line 386. This would reduce 50 lines to 1 line with the same coverage.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 7/7 changed files
  • Comments generated: 0 new

@kbukum1 kbukum1 force-pushed the kbukum1/gcp-oidc-support branch from 5a60283 to 9caf5c0 Compare April 21, 2026 21:29
@kbukum1 kbukum1 requested a review from Copilot April 21, 2026 21:30
@kbukum1 kbukum1 marked this pull request as ready for review April 21, 2026 21:30
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 7/7 changed files
  • Comments generated: 0 new

Add Google Artifact Registry as the fifth OIDC provider in the
Dependabot proxy, alongside Azure DevOps, AWS CodeArtifact, JFrog,
and Cloudsmith.

Token exchange flow:
- GitHub Actions OIDC JWT → Google STS token exchange
- Optional IAM Credentials generateAccessToken impersonation
  (when service-account is configured)
- Direct Workload Identity Federation when no service-account is present

Auth injection:
- Authorization: Bearer for most registry types (Maven, npm, Python, etc.)
- Basic oauth2accesstoken:<token> for Docker hosts (*-docker.pkg.dev)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kbukum1 kbukum1 force-pushed the kbukum1/gcp-oidc-support branch from 9caf5c0 to 9a38819 Compare April 21, 2026 22:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants