Skip to content

Commit d64a777

Browse files
authored
[Feature] Resolve TokenAudience from default_oidc_audience in host metadata (#1597)
## Summary Adds `DefaultOIDCAudience` to the `HostMetadata` struct so the SDK can parse the new `default_oidc_audience` field from the `/.well-known/databricks-config` discovery endpoint and use it to populate `TokenAudience` during config resolution. ## Why Today, `TokenAudience` for account hosts is inferred by a heuristic: if the host metadata has no `workspace_id` and has an `account_id`, the SDK sets `TokenAudience` to the account ID. For workspace hosts, no audience is set at all — credential strategies must know the right value independently. The server-side `/.well-known/databricks-config` endpoint is being updated to return a `default_oidc_audience` field that provides the correct OIDC audience directly: - **Workspace hosts**: `https://<workspace_host>/oidc/v1/token` - **Account/unified hosts**: the resolved account ID Using this authoritative value from the server removes the need for host-pattern guessing and ensures correctness for vanity domains, unified hosts, and future host types. ## What changed ### Interface changes - **`HostMetadata.DefaultOIDCAudience string`** — New field (`json:"default_oidc_audience"`) that holds the default OIDC audience from the discovery endpoint. ### Behavioral changes - During `resolveHostMetadata`, if `TokenAudience` is empty and `meta.DefaultOIDCAudience` is non-empty, `TokenAudience` is set from it. This takes priority over the existing account-ID-based fallback. - User-set `TokenAudience` is never overwritten. - Servers that do not return the field fall back to the existing behavior (no breaking change). ### Internal changes None. ## How is this tested? Unit tests covering all resolution paths: - `TestGetHostMetadata_WithDefaultOIDCAudience` — JSON parsing for workspace audience, account audience, and missing field - `TestApplyHostMetadata_SetsTokenAudienceFromDefaultOIDCAudience` — audience resolved from well-known for workspace host - `TestApplyHostMetadata_DefaultOIDCAudienceTakesPriorityOverAccountIDFallback` — new field wins over account-ID fallback - `TestApplyHostMetadata_DefaultOIDCAudienceDoesNotOverrideExisting` — user-set `TokenAudience` preserved - `TestApplyHostMetadata_FallsBackToAccountIDWhenNoDefaultOIDCAudience` — backward compat with old servers
1 parent 78469e6 commit d64a777

5 files changed

Lines changed: 141 additions & 0 deletions

File tree

NEXT_CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
* Added `HostMetadataResolver` hook to allow callers to customize host metadata resolution, e.g. with caching ([#1572](https://github.com/databricks/databricks-sdk-go/pull/1572)).
1212
* Added `NewLimitIterator` to `listing` package for lazy iteration with a cap on output items ([#1555](https://github.com/databricks/databricks-sdk-go/pull/1555)).
13+
* Resolve `TokenAudience` from `default_oidc_audience` in host metadata discovery endpoint.
1314

1415
### Bug Fixes
1516

config/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,10 @@ func (c *Config) resolveHostMetadata(ctx context.Context) {
702702
c.Cloud = c.Environment().Cloud
703703
logger.Debugf(ctx, "Resolved cloud from hostname: %q", c.Cloud)
704704
}
705+
if c.TokenAudience == "" && meta.DefaultOIDCAudience != "" {
706+
logger.Debugf(ctx, "Resolved token_audience from host metadata default_oidc_audience: %q", meta.DefaultOIDCAudience)
707+
c.TokenAudience = meta.DefaultOIDCAudience
708+
}
705709
if c.TokenAudience == "" && meta.WorkspaceID == "" && c.AccountID != "" {
706710
logger.Debugf(ctx, "Setting token_audience to account_id for account host: %q", c.AccountID)
707711
c.TokenAudience = c.AccountID

config/config_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,89 @@ func TestApplyHostMetadata_DoesNotOverrideExistingTokenAudience(t *testing.T) {
745745
assert.Equal(t, "custom-audience", cfg.TokenAudience)
746746
}
747747

748+
func TestApplyHostMetadata_SetsTokenAudienceFromDefaultOIDCAudience(t *testing.T) {
749+
noopLoader := mockLoader(func(cfg *Config) error { return nil })
750+
cfg := &Config{
751+
Host: testHMHost,
752+
Loaders: []Loader{noopLoader},
753+
HTTPTransport: fixtures.SliceTransport{
754+
{
755+
Method: "GET",
756+
Resource: "/.well-known/databricks-config",
757+
ReuseRequest: true,
758+
Status: 200,
759+
Response: `{"oidc_endpoint": "` + testHMHost + `/oidc", "account_id": "` + testHMAccountID + `", "workspace_id": "` + testHMWorkspaceID + `", "cloud": "AWS", "default_oidc_audience": "` + testHMHost + `/oidc/v1/token"}`,
760+
},
761+
},
762+
}
763+
err := cfg.EnsureResolved()
764+
require.NoError(t, err)
765+
assert.Equal(t, testHMHost+"/oidc/v1/token", cfg.TokenAudience)
766+
}
767+
768+
func TestApplyHostMetadata_DefaultOIDCAudienceTakesPriorityOverAccountIDFallback(t *testing.T) {
769+
noopLoader := mockLoader(func(cfg *Config) error { return nil })
770+
cfg := &Config{
771+
Host: testHMHost,
772+
Loaders: []Loader{noopLoader},
773+
HTTPTransport: fixtures.SliceTransport{
774+
{
775+
Method: "GET",
776+
Resource: "/.well-known/databricks-config",
777+
ReuseRequest: true,
778+
Status: 200,
779+
Response: `{"oidc_endpoint": "` + testHMHost + `/oidc", "account_id": "` + testHMAccountID + `", "cloud": "AWS", "default_oidc_audience": "custom-audience-from-server"}`,
780+
},
781+
},
782+
}
783+
err := cfg.EnsureResolved()
784+
require.NoError(t, err)
785+
// default_oidc_audience should take priority over the account_id fallback
786+
assert.Equal(t, "custom-audience-from-server", cfg.TokenAudience)
787+
}
788+
789+
func TestApplyHostMetadata_DefaultOIDCAudienceDoesNotOverrideExisting(t *testing.T) {
790+
noopLoader := mockLoader(func(cfg *Config) error { return nil })
791+
cfg := &Config{
792+
Host: testHMHost,
793+
TokenAudience: "user-set-audience",
794+
Loaders: []Loader{noopLoader},
795+
HTTPTransport: fixtures.SliceTransport{
796+
{
797+
Method: "GET",
798+
Resource: "/.well-known/databricks-config",
799+
ReuseRequest: true,
800+
Status: 200,
801+
Response: `{"oidc_endpoint": "` + testHMHost + `/oidc", "account_id": "` + testHMAccountID + `", "cloud": "AWS", "default_oidc_audience": "` + testHMHost + `/oidc/v1/token"}`,
802+
},
803+
},
804+
}
805+
err := cfg.EnsureResolved()
806+
require.NoError(t, err)
807+
assert.Equal(t, "user-set-audience", cfg.TokenAudience)
808+
}
809+
810+
func TestApplyHostMetadata_FallsBackToAccountIDWhenNoDefaultOIDCAudience(t *testing.T) {
811+
noopLoader := mockLoader(func(cfg *Config) error { return nil })
812+
cfg := &Config{
813+
Host: testHMHost,
814+
Loaders: []Loader{noopLoader},
815+
HTTPTransport: fixtures.SliceTransport{
816+
{
817+
Method: "GET",
818+
Resource: "/.well-known/databricks-config",
819+
ReuseRequest: true,
820+
Status: 200,
821+
Response: `{"oidc_endpoint": "` + testHMHost + `/oidc", "account_id": "` + testHMAccountID + `", "cloud": "AWS"}`,
822+
},
823+
},
824+
}
825+
err := cfg.EnsureResolved()
826+
require.NoError(t, err)
827+
// No default_oidc_audience and no workspace_id → falls back to account_id
828+
assert.Equal(t, testHMAccountID, cfg.TokenAudience)
829+
}
830+
748831
func TestEnsureResolved_UsesCustomHostMetadataResolver(t *testing.T) {
749832
noopLoader := mockLoader(func(cfg *Config) error { return nil })
750833
cfg := &Config{

config/host_metadata.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ type HostMetadata struct {
2222

2323
// Cloud is the cloud provider for this Databricks deployment (AWS, Azure, or GCP).
2424
Cloud environment.Cloud `json:"cloud"`
25+
26+
// DefaultOIDCAudience is the default OIDC audience for token requests.
27+
// For workspace hosts: "https://<workspace_host>/oidc/v1/token"
28+
// For account/unified hosts: the resolved account ID.
29+
DefaultOIDCAudience string `json:"default_oidc_audience"`
2530
}
2631

2732
// HostMetadataResolver, when set on [Config], overrides the default HTTP fetch

config/host_metadata_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,54 @@ func TestGetHostMetadata_HTTPError(t *testing.T) {
8686
}
8787
}
8888

89+
func TestGetHostMetadata_WithDefaultOIDCAudience(t *testing.T) {
90+
tests := []struct {
91+
name string
92+
audience string
93+
wantAudience string
94+
}{
95+
{
96+
name: "workspace audience",
97+
audience: testHMHost + "/oidc/v1/token",
98+
wantAudience: testHMHost + "/oidc/v1/token",
99+
},
100+
{
101+
name: "account audience",
102+
audience: testHMAccountID,
103+
wantAudience: testHMAccountID,
104+
},
105+
{
106+
name: "missing field",
107+
audience: "",
108+
wantAudience: "",
109+
},
110+
}
111+
for _, tc := range tests {
112+
t.Run(tc.name, func(t *testing.T) {
113+
response := map[string]string{
114+
"oidc_endpoint": testHMHost + "/oidc",
115+
"account_id": testHMAccountID,
116+
}
117+
if tc.audience != "" {
118+
response["default_oidc_audience"] = tc.audience
119+
}
120+
client := newTestAPIClient(fixtures.MappingTransport{
121+
"GET /.well-known/databricks-config": {
122+
Status: 200,
123+
Response: response,
124+
},
125+
})
126+
meta, err := getHostMetadata(context.Background(), testHMHost, client)
127+
if err != nil {
128+
t.Fatal(err)
129+
}
130+
if meta.DefaultOIDCAudience != tc.wantAudience {
131+
t.Errorf("DefaultOIDCAudience mismatch: got %q, want %q", meta.DefaultOIDCAudience, tc.wantAudience)
132+
}
133+
})
134+
}
135+
}
136+
89137
func TestGetHostMetadata_WithCloudField(t *testing.T) {
90138
tests := []struct {
91139
name string

0 commit comments

Comments
 (0)