Skip to content

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

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

feat(tool/http): support Google access tokens from ADC#3792
hugosmoreira wants to merge 2 commits into
googleapis:mainfrom
hugosmoreira:feat/http-adc-access-token

Conversation

@hugosmoreira

Copy link
Copy Markdown

Description

Adds an opt-in sendGoogleAccessToken field to HTTP tools. When enabled, Toolbox lazily resolves Application Default Credentials with the Google Cloud Platform scope, reuses the resulting OAuth token source, and sends the current token as a Bearer Authorization header.

The ADC header is applied after source, static, and dynamic headers so stale or caller-supplied authorization values cannot override it. ADC discovery remains lazy, preserving existing startup behavior when credentials are unavailable, and authentication failures are returned as invocation errors. The default remains false, so existing HTTP tools are unchanged.

This also adds:

  • unit coverage for lazy initialization, header precedence, and token errors
  • an end-to-end Toolbox integration test using a local OAuth token endpoint, including token reuse across invocations
  • configuration parsing coverage and HTTP tool documentation, including an HTTPS trust warning

Validation

  • go test -race -v ./internal/tools/http
  • go test -race -v ./tests/http -run '^TestHttpToolSendsGoogleAccessToken$' -count=1
  • go vet ./internal/tools/http
  • golangci-lint run --timeout 10m on the changed HTTP files (0 issues)
  • go mod tidy (no module-file diff)
  • git diff --check

PR Checklist

  • Reviewed CONTRIBUTING.md
  • The feature request is open and labeled status: help wanted
  • Unit and integration tests pass with the race detector
  • Code coverage does not decrease
  • Documentation was updated
  • This change is not breaking

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")},
},
)

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.

Support ADC fetching for HTTP tool

1 participant