grpc/grpc-go · error

unable to transfer TokenSource PerRPCCredentials: %v

Error message

unable to transfer TokenSource PerRPCCredentials: %v

What it means

Thrown from oauth.TokenSource.GetRequestMetadata (oauth.go:48) when CheckSecurityLevel fails. oauth.TokenSource wraps an oauth2.TokenSource and injects the access token as an Authorization header; because that token is a reusable bearer secret, gRPC will not attach it to a connection below PrivacyAndIntegrity. RequireTransportSecurity() returns true.

Source

Thrown at credentials/oauth/oauth.go:48

	"golang.org/x/oauth2/google"
	"golang.org/x/oauth2/jwt"
	"google.golang.org/grpc/credentials"
)

// TokenSource supplies PerRPCCredentials from an oauth2.TokenSource.
type TokenSource struct {
	oauth2.TokenSource
}

// GetRequestMetadata gets the request metadata as a map from a TokenSource.
func (ts TokenSource) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {
	token, err := ts.Token()
	if err != nil {
		return nil, err
	}
	ri, _ := credentials.RequestInfoFromContext(ctx)
	if err = credentials.CheckSecurityLevel(ri.AuthInfo, credentials.PrivacyAndIntegrity); err != nil {
		return nil, fmt.Errorf("unable to transfer TokenSource PerRPCCredentials: %v", err)
	}
	return map[string]string{
		"authorization": token.Type() + " " + token.AccessToken,
	}, nil
}

// RequireTransportSecurity indicates whether the credentials requires transport security.
func (ts TokenSource) RequireTransportSecurity() bool {
	return true
}

// removeServiceNameFromJWTURI removes RPC service name from URI.
func removeServiceNameFromJWTURI(uri string) (string, error) {
	parsed, err := url.Parse(uri)
	if err != nil {
		return "", err
	}
	parsed.Path = "/"

View on GitHub (pinned to 03255a9237)

Solutions

  1. Pair the per-RPC oauth creds with TLS transport credentials (credentials.NewTLS / NewClientTLSFromFile).
  2. On GCE/GKE prefer alts.NewClientCreds so the security level is satisfied without managing certs.
  3. Remove the per-RPC credentials if the channel is intentionally plaintext.

Example fix

// before
conn, _ := grpc.NewClient(addr,
    grpc.WithTransportCredentials(insecure.NewCredentials()),
    grpc.WithPerRPCCredentials(oauth.TokenSource{TokenSource: ts}),
)

// after
conn, _ := grpc.NewClient(addr,
    grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(caPool, "")),
    grpc.WithPerRPCCredentials(oauth.TokenSource{TokenSource: ts}),
)
Defensive patterns

Strategy: validation

Validate before calling

// Enforce: bearer per-RPC credentials ride on TLS.
tlsCreds := credentials.NewTLS(&tls.Config{ServerName: host, RootCAs: caPool})
conn, err := grpc.NewClient(addr,
    grpc.WithTransportCredentials(tlsCreds),
    grpc.WithPerRPCCredentials(oauth.TokenSource{TokenSource: ts}),
)

Try / catch

if st, ok := status.FromError(err); ok && st.Code() == codes.Unavailable {
    if strings.Contains(st.Message(), "unable to transfer TokenSource PerRPCCredentials") {
        // channel is insecure; re-dial with TLS
    }
}

Prevention

When it happens

Trigger: Using grpc.WithPerRPCCredentials(oauth.TokenSource{...}) on a channel created with insecure.NewCredentials() or with no transport credentials, so the negotiated security level is below PrivacyAndIntegrity.

Common situations: Reusing an oauth2 token source from a Google client library on a plaintext dev channel; dropping TLS during debugging; wiring ADC (NewApplicationDefault) result onto an insecure dial.

Related errors


AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07). Data as JSON: /api/errors/cc3163aec8a9cca5. Report an issue: GitHub.