Skip to content

feat(tool/http): support Google access tokens from ADC - #3792

Draft
hugosmoreira wants to merge 5 commits into
googleapis:mainfrom
hugosmoreira:feat/http-adc-access-token
Draft

hugosmoreira wants to merge 5 commits into
googleapis:mainfrom
hugosmoreira:feat/http-adc-access-token

Conversation

@hugosmoreira

@hugosmoreira hugosmoreira commented Aug 8, 2026 •

Copy link
Copy Markdown

Description

Adds optional sendGoogleAccessToken support to HTTP tools. When enabled, Toolbox lazily obtains a Google OAuth access token from Application Default Credentials (ADC), using the Cloud Platform scope, and sets the outgoing Bearer Authorization header after configured headers. The option defaults to false.

The provider caches valid tokens and uses the active invocation context for refresh. Tests cover cancellation, retry, concurrent reuse, and refresh after the previous request ends.

Integrated main 06ea49d9a (1.16.0), retaining the text/template NOLINT comment. Updated the metadata regression for upstream's removal of Tool.Authorized: it checks the required-auth getter and shared authorization helper, while retaining annotations and scope assertions. Token-handling code is unchanged by this integration.

Security: credentials come from server-side ADC rather than tool parameters. The HTTP tool preserves configured auth/scope metadata and does not override server authorization checks. The documentation limits this option to trusted HTTPS endpoints. This PR does not change existing REST or MCP authorization enforcement.

Validation

Local Windows/amd64, Go 1.27.3 and Zig 0.15.2:

  • HTTP/shared-tool race tests: 26 top-level tests pass, plus 42 subtests.
  • The fake-ADC command integration test passes three repetitions, including refresh after the first request ends and exactly two token fetches per run.
  • Negative controls catch four cancellation failures, dropped annotations, and dropped auth requirements. A separate local Invoke probe catches bypassed invocation-context forwarding.
  • Complete command/internal race suite: 417 packages pass, 10 have no test files, one fails; no race warnings or skipped individual tests. The sole failure is internal/sources/http.TestRedirectLoopbackIntegration, reporting expected request to fail connecting to 10.0.0.1. The same focused command fails at the same assertion on unchanged main 06ea49d9a in this environment. The full suite is not green.
  • HTTP package statement coverage is 53.8%, compared with 35.2% on unchanged main; this is not the hosted global coverage gate.
  • go mod tidy -diff, normal package and binary builds, CLI help, changed-file formatting and all three documentation linters pass.
  • Full golangci-lint 2.15.0 passes with zero issues.

Real Google credentials, the remaining credentialed HTTP integration suites, Linux/macOS and hosted global coverage, and Hugo rendering were not tested locally. The PR remains draft pending maintainer-authorized hosted verification.

Local verification commands

Windows race tests use Zig 0.15.2, GOMAXPROCS=2, and the test-binary linker settings below. The final full suite uses four package workers. Normal builds do not use the race-specific linker flags.

$taskRaceLinkFlags = '-extldflags "-lapi-ms-win-core-synch-l1-2-0 -Wl,--no-dynamicbase"'
go test -race -ldflags $taskRaceLinkFlags -mod=readonly -p 1 -timeout 90s -v ./internal/tools/http ./internal/tools -count=1
go test -race -ldflags $taskRaceLinkFlags -mod=readonly -p 1 -timeout 90s -v ./tests/http -run '^TestHttpToolSendsGoogleAccessToken$' -count=3
go test -race -ldflags $taskRaceLinkFlags -mod=readonly -p 4 -timeout 120s -json ./cmd/... ./internal/... -count=1
# Run on candidate and unchanged current main:
go test -race -ldflags $taskRaceLinkFlags -mod=readonly -p 4 -timeout 120s -json ./internal/sources/http -run '^TestRedirectLoopbackIntegration$' -count=1
go mod tidy -diff
go build -mod=readonly -p 4 ./...
golangci-lint run --timeout 10m -j 4 --modules-download-mode readonly --max-same-issues 0 --max-issues-per-linter 0 ./...

PR Checklist

  • Reviewed CONTRIBUTING.md
  • Existing issue describes the feature
  • Manually reviewed the complete diff
  • Tests and linter pass
  • Code coverage does not decrease (HTTP package comparison passes; hosted gate pending)
  • Appropriate docs updated
  • Breaking-change marker considered; the new option is disabled by default

Fixes #1030 🦕

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for obtaining and sending Google OAuth 2.0 access tokens from Application Default Credentials (ADC) in HTTP tools, including documentation, configuration updates, and integration tests. The reviewer feedback correctly identifies a critical Go anti-pattern where context.Context is stored inside the adcTokenProvider struct. Because the initialization context is short-lived, this can lead to runtime failures due to context cancellation when the tool is invoked later. The reviewer recommends refactoring the token provider and helper functions to accept the active request context dynamically during invocation, and updating the corresponding tests.

Comment on lines +88 to +91
var googleAccessTokenProvider *adcTokenProvider
if cfg.SendGoogleAccessToken {
googleAccessTokenProvider = &adcTokenProvider{ctx: ctx}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Storing context.Context in a struct is a Go anti-pattern. Additionally, using the initialization context ctx (which is typically short-lived or cancelled after startup) for lazy credential loading during invocation will cause runtime failures (e.g., context canceled) when the tool is invoked later. Instead, we should pass the request context dynamically during Invoke.

Suggested change
var googleAccessTokenProvider *adcTokenProvider
if cfg.SendGoogleAccessToken {
googleAccessTokenProvider = &adcTokenProvider{ctx: ctx}
}
var googleAccessTokenProvider *adcTokenProvider
if cfg.SendGoogleAccessToken {
googleAccessTokenProvider = &adcTokenProvider{}
}

Comment on lines +282 to +336
type adcTokenProvider struct {
ctx context.Context
mu sync.Mutex
tokenSource oauth2.TokenSource
}

func (p *adcTokenProvider) getTokenSource() (oauth2.TokenSource, error) {
if p == nil {
return nil, fmt.Errorf("google ADC token provider is not initialized")
}
if p.ctx == nil {
return nil, fmt.Errorf("google ADC token provider context is not initialized")
}

p.mu.Lock()
defer p.mu.Unlock()
if p.tokenSource == nil {
credentials, err := google.FindDefaultCredentials(p.ctx, sources.CloudPlatformScope)
if err != nil {
return nil, fmt.Errorf("unable to initialize Google ADC: %w", err)
}
if credentials.TokenSource == nil {
return nil, fmt.Errorf("google ADC did not provide a token source")
}
p.tokenSource = credentials.TokenSource
}
return p.tokenSource, nil
}

func (p *adcTokenProvider) Token() (*oauth2.Token, error) {
tokenSource, err := p.getTokenSource()
if err != nil {
return nil, err
}
token, err := tokenSource.Token()
if err != nil {
return nil, fmt.Errorf("unable to get Google access token from ADC: %w", err)
}
return token, nil
}

func setGoogleAccessToken(
req *http.Request,
tokenProvider *adcTokenProvider,
) error {
token, err := tokenProvider.Token()
if err != nil {
return err
}
if !token.Valid() {
return fmt.Errorf("google ADC returned an invalid or expired access token")
}
req.Header.Set("Authorization", "Bearer "+token.AccessToken)
return nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Remove the ctx field from adcTokenProvider and update its methods to accept context.Context dynamically. This ensures that the active request context is used for credential discovery and token fetching, avoiding potential context cancellation issues.

type adcTokenProvider struct {
	mu          sync.Mutex
	tokenSource oauth2.TokenSource
}

func (p *adcTokenProvider) getTokenSource(ctx context.Context) (oauth2.TokenSource, error) {
	if p == nil {
		return nil, fmt.Errorf("google ADC token provider is not initialized")
	}

	p.mu.Lock()
	defer p.mu.Unlock()
	if p.tokenSource == nil {
		credentials, err := google.FindDefaultCredentials(ctx, sources.CloudPlatformScope)
		if err != nil {
			return nil, fmt.Errorf("unable to initialize Google ADC: %w", err)
		}
		if credentials.TokenSource == nil {
			return nil, fmt.Errorf("google ADC did not provide a token source")
		}
		p.tokenSource = credentials.TokenSource
	}
	return p.tokenSource, nil
}

func (p *adcTokenProvider) Token(ctx context.Context) (*oauth2.Token, error) {
	tokenSource, err := p.getTokenSource(ctx)
	if err != nil {
		return nil, err
	}
	token, err := tokenSource.Token()
	if err != nil {
		return nil, fmt.Errorf("unable to get Google access token from ADC: %w", err)
	}
	return token, nil
}

func setGoogleAccessToken(
	ctx context.Context,
	req *http.Request,
	tokenProvider *adcTokenProvider,
) error {
	token, err := tokenProvider.Token(ctx)
	if err != nil {
		return err
	}
	if !token.Valid() {
		return fmt.Errorf("google ADC returned an invalid or expired access token")
	}
	req.Header.Set("Authorization", "Bearer "+token.AccessToken)
	return nil
}

Comment on lines +377 to +381
if t.Cfg.SendGoogleAccessToken {
if err := setGoogleAccessToken(req, t.googleAccessTokenProvider); err != nil {
return nil, util.NewClientServerError("error authenticating HTTP request with Google ADC", http.StatusInternalServerError, err)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Pass the active request context ctx to setGoogleAccessToken during tool invocation.

Suggested change
if t.Cfg.SendGoogleAccessToken {
if err := setGoogleAccessToken(req, t.googleAccessTokenProvider); err != nil {
return nil, util.NewClientServerError("error authenticating HTTP request with Google ADC", http.StatusInternalServerError, err)
}
}
if t.Cfg.SendGoogleAccessToken {
if err := setGoogleAccessToken(ctx, req, t.googleAccessTokenProvider); err != nil {
return nil, util.NewClientServerError("error authenticating HTTP request with Google ADC", http.StatusInternalServerError, err)
}
}

Comment on lines +62 to +68
err = setGoogleAccessToken(
req,
&adcTokenProvider{
ctx: context.Background(),
tokenSource: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: "adc-token"}),
},
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Update the test to pass context.Background() to setGoogleAccessToken and remove the ctx field from the adcTokenProvider initialization.

Suggested change
err = setGoogleAccessToken(
req,
&adcTokenProvider{
ctx: context.Background(),
tokenSource: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: "adc-token"}),
},
)
err = setGoogleAccessToken(
context.Background(),
req,
&adcTokenProvider{
tokenSource: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: "adc-token"}),
},
)

Comment on lines +83 to +89
err = setGoogleAccessToken(
req,
&adcTokenProvider{
ctx: context.Background(),
tokenSource: errorTokenSource{err: errors.New("credentials unavailable")},
},
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Update the test to pass context.Background() to setGoogleAccessToken and remove the ctx field from the adcTokenProvider initialization.

Suggested change
err = setGoogleAccessToken(
req,
&adcTokenProvider{
ctx: context.Background(),
tokenSource: errorTokenSource{err: errors.New("credentials unavailable")},
},
)
err = setGoogleAccessToken(
context.Background(),
req,
&adcTokenProvider{
tokenSource: errorTokenSource{err: errors.New("credentials unavailable")},
},
)

@helloeve helloeve added the type: feature request ‘Nice-to-have’ improvement, new feature or different behavior or design. label Aug 18, 2026
@duwenxin99 duwenxin99 added the priority: p2 Moderately-important priority. Fix may not be included in next release. label Sep 1, 2026
@hugosmoreira

Copy link
Copy Markdown
Author

Pushed 99b4401 to address the token-source context lifetime and cancellation behavior.

The provider now caches only the token and refreshes it using the active invocation context. Concurrent callers share an active refresh, waiting callers can honor their own deadlines, and a completed request does not leave a canceled context attached to future refreshes. Regression tests cover cancellation, retry, and concurrent reuse. Restoring the previous implementation reproduces four expected cancellation failures.

Local verification on Windows with Go 1.26.4:

  • Full golangci-lint run: zero issues. go mod tidy -diff: no changes.
  • Focused HTTP race tests pass. The local token-refresh integration test passed three additional race-enabled repetitions, using fake ADC and loopback endpoints (not live Google credentials).
  • The wider command/internal race run completed: 380 packages passed, 11 had no test files, and one package failed. No race warnings were reported.
  • The only failing test was internal/sources/http.TestRedirectLoopbackIntegration, reporting expected request to fail connecting to 10.0.0.1. I reproduced the same failure on unchanged commit e7f0506 in a clean checkout. The full suite is therefore not green in this environment; I have left that unrelated fixture unchanged.
Verification commands

Windows race builds used Zig 0.15.2, GOMAXPROCS=2, and these test-binary linker flags:

$taskRaceLinkFlags = '-extldflags "-lapi-ms-win-core-synch-l1-2-0 -Wl,--no-dynamicbase"'
go test -race -ldflags $taskRaceLinkFlags -mod=readonly -p 1 -timeout 120s -json ./cmd/... ./internal/...
go test -race -ldflags $taskRaceLinkFlags -mod=readonly -p 1 -timeout 90s -v ./tests/http -run '^TestHttpToolSendsGoogleAccessToken$' -count=3
# Also run from the unchanged checkout to confirm the wider-suite failure:
go test -race -ldflags $taskRaceLinkFlags -mod=readonly -p 1 -timeout 60s -v ./internal/sources/http -run '^TestRedirectLoopbackIntegration$' -count=1
golangci-lint run --timeout 10m -j 1 --modules-download-mode readonly --max-same-issues 0 --max-issues-per-linter 0 ./...
go mod tidy -diff

The hosted integration and evaluation checks on this revision both report that they need /gcbrun from a collaborator. Could a maintainer authorize those tests? I'm keeping the PR in draft pending hosted verification.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: p2 Moderately-important priority. Fix may not be included in next release. type: feature request ‘Nice-to-have’ improvement, new feature or different behavior or design.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support ADC fetching for HTTP tool

3 participants