From ac2edfccf7ec0c2c9ed56ebd82546735da8e03f2 Mon Sep 17 00:00:00 2001 From: simon Date: Wed, 25 Feb 2026 11:00:54 +0100 Subject: [PATCH 1/3] Return clear error when `auth token` is used with M2M profile Previously, `databricks auth token --profile ` would silently return the wrong token (a cached U2M token for the same host) or trigger an interactive browser login. This was confusing because the command gave no indication that M2M profiles are unsupported. Now the command detects when the resolved profile has a client_id configured and returns an explicit error explaining that `auth token` only supports U2M authentication. Fixes #1939 --- cmd/auth/token.go | 11 ++++++++++ cmd/auth/token_test.go | 31 +++++++++++++++++++++++++++ libs/databrickscfg/profile/file.go | 1 + libs/databrickscfg/profile/profile.go | 1 + 4 files changed, 44 insertions(+) diff --git a/cmd/auth/token.go b/cmd/auth/token.go index f0f2e0a765..dd6e3f82f8 100644 --- a/cmd/auth/token.go +++ b/cmd/auth/token.go @@ -204,6 +204,17 @@ func loadToken(ctx context.Context, args loadTokenArgs) (*oauth2.Token, error) { } } + // Check if the resolved profile uses M2M authentication (client credentials). + // The auth token command only supports U2M OAuth tokens. + if existingProfile != nil && existingProfile.ClientID != "" { + return nil, fmt.Errorf( + "profile %q uses M2M authentication (client_id/client_secret). "+ + "`databricks auth token` only supports U2M (user-to-machine) authentication tokens. "+ + "To authenticate as a service principal, use the Databricks SDK directly", + args.profileName, + ) + } + args.authArguments.Profile = args.profileName ctx, cancel := context.WithTimeout(ctx, args.tokenTimeout) diff --git a/cmd/auth/token_test.go b/cmd/auth/token_test.go index 3ec29fd515..0188810ada 100644 --- a/cmd/auth/token_test.go +++ b/cmd/auth/token_test.go @@ -125,6 +125,11 @@ func TestToken_loadToken(t *testing.T) { Name: "legacy-ws", Host: "https://legacy-ws.cloud.databricks.com", }, + { + Name: "m2m-profile", + Host: "https://m2m.cloud.databricks.com", + ClientID: "my-client-id", + }, }, } tokenCache := &inMemoryTokenCache{ @@ -526,6 +531,32 @@ func TestToken_loadToken(t *testing.T) { }, wantErr: "no profiles configured. Run 'databricks auth login' to create a profile", }, + { + name: "M2M profile returns clear error", + args: loadTokenArgs{ + authArguments: &auth.AuthArguments{}, + profileName: "m2m-profile", + args: []string{}, + tokenTimeout: 1 * time.Hour, + profiler: profiler, + }, + wantErr: `profile "m2m-profile" uses M2M authentication (client_id/client_secret). ` + + "`databricks auth token` only supports U2M (user-to-machine) authentication tokens. " + + "To authenticate as a service principal, use the Databricks SDK directly", + }, + { + name: "M2M profile detected via positional arg", + args: loadTokenArgs{ + authArguments: &auth.AuthArguments{}, + profileName: "", + args: []string{"m2m-profile"}, + tokenTimeout: 1 * time.Hour, + profiler: profiler, + }, + wantErr: `profile "m2m-profile" uses M2M authentication (client_id/client_secret). ` + + "`databricks auth token` only supports U2M (user-to-machine) authentication tokens. " + + "To authenticate as a service principal, use the Databricks SDK directly", + }, { name: "no args, DATABRICKS_HOST env resolves", setupCtx: func(ctx context.Context) context.Context { diff --git a/libs/databrickscfg/profile/file.go b/libs/databrickscfg/profile/file.go index 7661baf956..d83cbf31f5 100644 --- a/libs/databrickscfg/profile/file.go +++ b/libs/databrickscfg/profile/file.go @@ -86,6 +86,7 @@ func (f FileProfilerImpl) LoadProfiles(ctx context.Context, fn ProfileMatchFunct IsUnifiedHost: all["experimental_is_unified_host"] == "true", ClusterID: all["cluster_id"], ServerlessComputeID: all["serverless_compute_id"], + ClientID: all["client_id"], } if fn(profile) { profiles = append(profiles, profile) diff --git a/libs/databrickscfg/profile/profile.go b/libs/databrickscfg/profile/profile.go index 0358b8f7ec..56ca7d7a2d 100644 --- a/libs/databrickscfg/profile/profile.go +++ b/libs/databrickscfg/profile/profile.go @@ -17,6 +17,7 @@ type Profile struct { IsUnifiedHost bool ClusterID string ServerlessComputeID string + ClientID string } func (p Profile) Cloud() string { From f111706b2b1c8a603ccb6414b166a2e298d51822 Mon Sep 17 00:00:00 2001 From: simon Date: Wed, 25 Feb 2026 12:46:50 +0100 Subject: [PATCH 2/3] Reload existingProfile after host-based profile resolution The M2M guard checked existingProfile, but that variable was not updated when a profile was resolved later via host matching. This meant `--host ` that uniquely matched an M2M profile would bypass the check and fall through to the U2M token lookup. Now existingProfile is updated in both branches of the host-matching block (single match and interactive selection), and a new test covers the host-resolved path. --- cmd/auth/token.go | 5 +++++ cmd/auth/token_test.go | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/cmd/auth/token.go b/cmd/auth/token.go index dd6e3f82f8..7bac5a48d9 100644 --- a/cmd/auth/token.go +++ b/cmd/auth/token.go @@ -199,8 +199,13 @@ func loadToken(ctx context.Context, args loadTokenArgs) (*oauth2.Token, error) { return nil, err } args.profileName = selected + existingProfile, err = loadProfileByName(ctx, selected, args.profiler) + if err != nil { + return nil, err + } } else if len(matchingProfiles) == 1 { args.profileName = matchingProfiles[0].Name + existingProfile = &matchingProfiles[0] } } diff --git a/cmd/auth/token_test.go b/cmd/auth/token_test.go index 0188810ada..c7ffdff4c7 100644 --- a/cmd/auth/token_test.go +++ b/cmd/auth/token_test.go @@ -557,6 +557,21 @@ func TestToken_loadToken(t *testing.T) { "`databricks auth token` only supports U2M (user-to-machine) authentication tokens. " + "To authenticate as a service principal, use the Databricks SDK directly", }, + { + name: "M2M profile detected via host resolution", + args: loadTokenArgs{ + authArguments: &auth.AuthArguments{ + Host: "https://m2m.cloud.databricks.com", + }, + profileName: "", + args: []string{}, + tokenTimeout: 1 * time.Hour, + profiler: profiler, + }, + wantErr: `profile "m2m-profile" uses M2M authentication (client_id/client_secret). ` + + "`databricks auth token` only supports U2M (user-to-machine) authentication tokens. " + + "To authenticate as a service principal, use the Databricks SDK directly", + }, { name: "no args, DATABRICKS_HOST env resolves", setupCtx: func(ctx context.Context) context.Context { From 6b78d4579703768c87f280aa5b01092dccdd8d28 Mon Sep 17 00:00:00 2001 From: simon Date: Wed, 25 Feb 2026 13:12:01 +0100 Subject: [PATCH 3/3] Use HasClientCredentials bool instead of ClientID string Replace ClientID string with HasClientCredentials bool that requires both client_id and client_secret in the profile. Fix gofmt alignment. --- cmd/auth/token.go | 2 +- cmd/auth/token_test.go | 6 +++--- libs/databrickscfg/profile/file.go | 16 ++++++++-------- libs/databrickscfg/profile/profile.go | 16 ++++++++-------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/cmd/auth/token.go b/cmd/auth/token.go index 7bac5a48d9..de30fb2785 100644 --- a/cmd/auth/token.go +++ b/cmd/auth/token.go @@ -211,7 +211,7 @@ func loadToken(ctx context.Context, args loadTokenArgs) (*oauth2.Token, error) { // Check if the resolved profile uses M2M authentication (client credentials). // The auth token command only supports U2M OAuth tokens. - if existingProfile != nil && existingProfile.ClientID != "" { + if existingProfile != nil && existingProfile.HasClientCredentials { return nil, fmt.Errorf( "profile %q uses M2M authentication (client_id/client_secret). "+ "`databricks auth token` only supports U2M (user-to-machine) authentication tokens. "+ diff --git a/cmd/auth/token_test.go b/cmd/auth/token_test.go index c7ffdff4c7..8becf5f43c 100644 --- a/cmd/auth/token_test.go +++ b/cmd/auth/token_test.go @@ -126,9 +126,9 @@ func TestToken_loadToken(t *testing.T) { Host: "https://legacy-ws.cloud.databricks.com", }, { - Name: "m2m-profile", - Host: "https://m2m.cloud.databricks.com", - ClientID: "my-client-id", + Name: "m2m-profile", + Host: "https://m2m.cloud.databricks.com", + HasClientCredentials: true, }, }, } diff --git a/libs/databrickscfg/profile/file.go b/libs/databrickscfg/profile/file.go index d83cbf31f5..b078e67d3a 100644 --- a/libs/databrickscfg/profile/file.go +++ b/libs/databrickscfg/profile/file.go @@ -79,14 +79,14 @@ func (f FileProfilerImpl) LoadProfiles(ctx context.Context, fn ProfileMatchFunct continue } profile := Profile{ - Name: v.Name(), - Host: host, - AccountID: all["account_id"], - WorkspaceID: all["workspace_id"], - IsUnifiedHost: all["experimental_is_unified_host"] == "true", - ClusterID: all["cluster_id"], - ServerlessComputeID: all["serverless_compute_id"], - ClientID: all["client_id"], + Name: v.Name(), + Host: host, + AccountID: all["account_id"], + WorkspaceID: all["workspace_id"], + IsUnifiedHost: all["experimental_is_unified_host"] == "true", + ClusterID: all["cluster_id"], + ServerlessComputeID: all["serverless_compute_id"], + HasClientCredentials: all["client_id"] != "" && all["client_secret"] != "", } if fn(profile) { profiles = append(profiles, profile) diff --git a/libs/databrickscfg/profile/profile.go b/libs/databrickscfg/profile/profile.go index 56ca7d7a2d..9cdf24f82b 100644 --- a/libs/databrickscfg/profile/profile.go +++ b/libs/databrickscfg/profile/profile.go @@ -10,14 +10,14 @@ import ( // It should only be used for prompting and filtering. // Use its name to construct a config.Config. type Profile struct { - Name string - Host string - AccountID string - WorkspaceID string - IsUnifiedHost bool - ClusterID string - ServerlessComputeID string - ClientID string + Name string + Host string + AccountID string + WorkspaceID string + IsUnifiedHost bool + ClusterID string + ServerlessComputeID string + HasClientCredentials bool } func (p Profile) Cloud() string {