Skip to content

auth: userTokenProvider.exchangeToken does not set Content-Type, causing failures #12633

@alexrjones

Description

@alexrjones

Client

Directly using the auth library

Environment

Local machine

$ go version
go version go1.23.4 darwin/arm64

Code and Dependencies

package main

import (
	"context"
	"fmt"
	"log"

	"cloud.google.com/go/auth/credentials/impersonate"
)

const sa = "your-sa-here"
const subject = "[email protected]"

var scopes = []string{}

func main() {

	baseCreds, err := impersonate.NewCredentials(&impersonate.CredentialsOptions{
		TargetPrincipal: sa,
		Scopes:          scopes,
		Subject:         subject,
	})
	if err != nil {
		log.Fatal(err)
		return
	}
	tok, err := baseCreds.Token(context.Background())
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(tok.Value)
}
go.mod
module auth-impersonate-repro-issue

go 1.23.4

require cloud.google.com/go/auth v0.16.3

require (
	cloud.google.com/go/compute/metadata v0.7.0 // indirect
	github.com/felixge/httpsnoop v1.0.4 // indirect
	github.com/go-logr/logr v1.4.2 // indirect
	github.com/go-logr/stdr v1.2.2 // indirect
	github.com/google/s2a-go v0.1.9 // indirect
	github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
	github.com/googleapis/gax-go/v2 v2.14.2 // indirect
	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
	go.opentelemetry.io/otel v1.36.0 // indirect
	go.opentelemetry.io/otel/metric v1.36.0 // indirect
	go.opentelemetry.io/otel/trace v1.36.0 // indirect
	golang.org/x/crypto v0.39.0 // indirect
	golang.org/x/net v0.41.0 // indirect
	golang.org/x/sys v0.33.0 // indirect
	golang.org/x/text v0.26.0 // indirect
	google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
	google.golang.org/grpc v1.73.0 // indirect
	google.golang.org/protobuf v1.36.6 // indirect
)

Expected behavior

The token value is printed out.

Actual behavior

2025/08/01 16:58:44 impersonate: status code 400: {
  "error": {
    "code": 400,
    "message": "Invalid JSON payload received. Unexpected token.\nassertion=eyJhbGciOi\n^",
    "status": "INVALID_ARGUMENT"
  }
}

Additional context

Examining auth/credentials/impersonate/user.go, method exchangeToken, you can see that the payload sent to the OAuth2 token endpoint here is a form, but no Content-Type header is set, which appears to cause the server to attempt to interpret the payload as JSON. If I step through with a debugger and set the header before sending the request, then the request succeeds.

func (u userTokenProvider) exchangeToken(ctx context.Context, signedJWT string) (*auth.Token, error) {
v := url.Values{}
v.Set("grant_type", "assertion")
v.Set("assertion_type", "http://oauth.net/grant_type/jwt/1.0/bearer")
v.Set("assertion", signedJWT)
req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/token", oauth2Endpoint), strings.NewReader(v.Encode()))
if err != nil {
return nil, err
}
u.logger.DebugContext(ctx, "impersonated user token exchange request", "request", internallog.HTTPRequest(req, []byte(v.Encode())))
resp, body, err := internal.DoRequest(u.client, req)
if err != nil {
return nil, fmt.Errorf("impersonate: unable to exchange token: %w", err)
}

Metadata

Metadata

Assignees

Labels

priority: p1Important issue which blocks shipping the next release. Will be fixed prior to next release.type: bugError or flaw in code with unintended results or allowing sub-optimal usage patterns.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions