Skip to content

OAuth M2M: token requests ignore the connector's User-Agent (WithUserAgentEntry not threaded into the token exchange) #413

Description

@sevbanbayrak

Summary

When a connector authenticates with OAuth M2M (WithClientCredentials, or client credentials via DSN), the driver's token requests are issued with Go's default HTTP client, so they reach the workspace as Go-http-client/2.0 instead of the User-Agent configured through WithUserAgentEntry. Query traffic is attributed correctly — only the token exchange is anonymous.

This matters for ISV partner attribution: the Databricks partner program asks integrations to identify themselves via a programmatic User-Agent, and these token grants show up in system.access.audit as an unidentified generic Go client.

Current behavior

Connector configured as:

connector, err := dbsql.NewConnector(
    dbsql.WithServerHostname(host),
    dbsql.WithPort(443),
    dbsql.WithHTTPPath(httpPath),
    dbsql.WithClientCredentials(clientID, clientSecret),
    dbsql.WithUserAgentEntry("MyISV_MyProduct/1.0"),
)

system.access.audit rows for the resulting activity (identifiers redacted):

service_name action_name user_agent
workspace workspaceInHouseOAuthClientAuthentication Go-http-client/2.0
workspace mintOAuthToken Go-http-client/2.0
accounts oidcTokenAuthorization godatabrickssqlconnector/1.13.0 (MyISV_MyProduct/1.0)

The Thrift/query path carries the configured entry as expected; the token-mint requests do not.

Root cause

BuildUserAgent(cfg) is applied only to the Thrift HTTP client:

  • internal/client/client.gothriftHttpClient.SetHeader("User-Agent", BuildUserAgent(cfg))

The M2M authenticator builds its token source with a hardcoded background context, so a caller has no way to supply an *http.Clientoauth2 takes its HTTP client from the context (oauth2.HTTPClient), and that context never leaves the package:

  • auth/oauth/m2m/m2m.go (on main) — GetConfig(context.Background(), ...) and config.TokenSource(context.Background())

Because of that, neither WithUserAgentEntry nor WithTransport can influence token requests.

This is adjacent to a limitation already acknowledged in-tree for the endpoint-resolution path (auth/oauth/oauth.go):

NOTE: this client uses the default transport, matching the existing oidc.NewProvider discovery below. A connector-supplied transport / TLS config (WithTransport, WithSkipTLSHostVerify) is not yet threaded into the OAuth endpoint-resolution path; that is a pre-existing limitation, tracked separately.

This report is that same limitation one step further along: the token exchange itself.

Expected behavior

Token requests (and ideally OIDC discovery) carry the same User-Agent as the rest of the connector's traffic — which is what BuildUserAgent's own doc comment states as the intent:

…used by the driver for Thrift, telemetry, and feature-flag requests so all traffic from a single connection is attributable to the same identifier in access logs.

Possible directions

  1. Thread the connector's HTTP client/context into the authenticator — e.g. have the connector pass an *http.Client built with the driver's transport + User-Agent, and create the token source with context.WithValue(ctx, oauth2.HTTPClient, client).
  2. Or expose an option to supply the token-exchange HTTP client, so callers can set headers without re-implementing the M2M flow.

We verified direction 1 works: injecting an *http.Client whose RoundTripper sets the User-Agent into the token source makes every mintOAuthToken audit row correctly attributed. We did not keep that as a workaround — replacing the built-in authenticator with an application-side copy of the credential flow would risk drifting from upstream on every release, which is exactly why we'd prefer this handled in the driver.

Environment

  • databricks-sql-go v1.13.0 — code path verified unchanged in v1.14.0 and on main
  • Auth: OAuth M2M (client credentials, Databricks-managed service principal)
  • Go 1.25, Linux

Happy to open a PR for direction 1 if you'd like it contributed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions