Skip to content

Commit c2087e1

Browse files
authored
credentials/u2m: add WithDiscoveryAccountTarget option (#1701)
## Changes Adds a new `PersistentAuth` option, `WithDiscoveryAccountTarget()`, that sets `target=ACCOUNT` on the `login.databricks.com` authorize URL during discovery login. - Before: discovery login always lands the user on the workspace selector. - Now: when `WithDiscoveryAccountTarget()` is set alongside `WithDiscoveryLogin()`, the authorize URL includes `target=ACCOUNT` so the user lands directly on the account selector. The new top-level `target` parameter is set alongside `destination_url`; the rest of the URL shape is unchanged. This is needed for clients (e.g. the Databricks CLI) that want an account-only login path (`databricks auth login --skip-workspace`) without forcing the user through a workspace pick they will never use. ## Tests - New `TestBuildDiscoveryAuthorizeURL_Target` covers both the no-target default and `target=ACCOUNT`. - New `TestWithDiscoveryAccountTarget` verifies the option sets the field. - Existing tests updated for the internal `buildDiscoveryAuthorizeURL` signature change (added trailing `target` arg). The exported `BuildDiscoveryAuthorizeURL` signature is unchanged. Signed-off-by: simon <simon.faltum@databricks.com>
1 parent b084eb4 commit c2087e1

4 files changed

Lines changed: 88 additions & 5 deletions

File tree

NEXT_CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
### New Features and Improvements
88

9+
* Add `u2m.WithDiscoveryAccountTarget()` option that sets `target=ACCOUNT` on the login.databricks.com authorize URL, so the discovery flow lands the user on the account selector instead of the workspace selector.
10+
911
### Bug Fixes
1012

1113
### Documentation

credentials/u2m/discovery_token_source.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,26 @@ func DeriveTokenEndpoint(issuer string) string {
4848
return strings.TrimRight(issuer, "/") + "/v1/token"
4949
}
5050

51+
// discoveryTargetAccount is the value of the `target` query parameter that
52+
// tells login.databricks.com to land the user on the account selector instead
53+
// of the workspace selector. Used when the caller has signalled (e.g. via
54+
// WithDiscoveryAccountTarget) that they only want account-level access.
55+
const discoveryTargetAccount = "ACCOUNT"
56+
5157
// BuildDiscoveryAuthorizeURL builds the login.databricks.com URL that initiates
5258
// the discovery OAuth flow. The OIDC authorize path with all OAuth query params
5359
// is URL-encoded as the destination_url parameter.
5460
func BuildDiscoveryAuthorizeURL(redirectAddr, state string, pkce PKCEParams, scopes []string) string {
55-
return buildDiscoveryAuthorizeURL(defaultLoginDatabricksHost, redirectAddr, state, pkce, scopes)
61+
return buildDiscoveryAuthorizeURL(defaultLoginDatabricksHost, redirectAddr, state, pkce, scopes, "")
5662
}
5763

5864
// buildDiscoveryAuthorizeURL builds the discovery authorize URL against the
5965
// given host. Trailing slashes on host are trimmed so the result is
60-
// well-formed regardless of how an override is written.
61-
func buildDiscoveryAuthorizeURL(host, redirectAddr, state string, pkce PKCEParams, scopes []string) string {
66+
// well-formed regardless of how an override is written. When target is
67+
// non-empty it is set as the top-level `target` query parameter, which
68+
// login.databricks.com uses to route the user to a specific selector page
69+
// (e.g. "ACCOUNT" for the account selector).
70+
func buildDiscoveryAuthorizeURL(host, redirectAddr, state string, pkce PKCEParams, scopes []string, target string) string {
6271
// Build the nested OIDC authorize path with query parameters.
6372
authParams := url.Values{}
6473
authParams.Set("client_id", appClientID)
@@ -73,6 +82,9 @@ func buildDiscoveryAuthorizeURL(host, redirectAddr, state string, pkce PKCEParam
7382
// Wrap the authorize path as the destination_url query parameter on the
7483
// discovery host.
7584
topParams := url.Values{}
85+
if target != "" {
86+
topParams.Set("target", target)
87+
}
7688
topParams.Set("destination_url", destinationURL)
7789
return strings.TrimRight(host, "/") + "/?" + topParams.Encode()
7890
}
@@ -93,6 +105,10 @@ type discoveryTokenSource struct {
93105
pa *PersistentAuth
94106
// host overrides defaultLoginDatabricksHost when non-empty.
95107
host string
108+
// target is the value of the top-level `target` query parameter on the
109+
// authorize URL. When non-empty (e.g. "ACCOUNT"), login.databricks.com
110+
// routes the user directly to the corresponding selector.
111+
target string
96112
}
97113

98114
// challenge initiates the discovery OAuth flow through login.databricks.com.
@@ -122,7 +138,7 @@ func (d *discoveryTokenSource) challenge() error {
122138
if host == "" {
123139
host = defaultLoginDatabricksHost
124140
}
125-
authorizeURL := buildDiscoveryAuthorizeURL(host, d.pa.redirectAddr, state, pkce, scopes)
141+
authorizeURL := buildDiscoveryAuthorizeURL(host, d.pa.redirectAddr, state, pkce, scopes, d.target)
126142

127143
code, returnedState, issuer, err := cb.handlerWithIssuer(authorizeURL)
128144
if err != nil {

credentials/u2m/discovery_token_source_test.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ func TestBuildDiscoveryAuthorizeURL_HostOverride(t *testing.T) {
187187
}
188188
for _, tc := range tests {
189189
t.Run(tc.name, func(t *testing.T) {
190-
got := buildDiscoveryAuthorizeURL(tc.host, "localhost:8020", "s", pkce, scopes)
190+
got := buildDiscoveryAuthorizeURL(tc.host, "localhost:8020", "s", pkce, scopes, "")
191191
u, err := url.Parse(got)
192192
if err != nil {
193193
t.Fatalf("parsing URL: %v", err)
@@ -199,6 +199,50 @@ func TestBuildDiscoveryAuthorizeURL_HostOverride(t *testing.T) {
199199
}
200200
}
201201

202+
func TestBuildDiscoveryAuthorizeURL_Target(t *testing.T) {
203+
pkce := PKCEParams{
204+
Challenge: "c",
205+
ChallengeMethod: "S256",
206+
Verifier: "v",
207+
}
208+
scopes := []string{"offline_access", "all-apis"}
209+
tests := []struct {
210+
name string
211+
target string
212+
wantTarget string
213+
}{
214+
{name: "no target", target: "", wantTarget: ""},
215+
{name: "account target", target: "ACCOUNT", wantTarget: "ACCOUNT"},
216+
}
217+
for _, tc := range tests {
218+
t.Run(tc.name, func(t *testing.T) {
219+
got := buildDiscoveryAuthorizeURL(defaultLoginDatabricksHost, "localhost:8020", "s", pkce, scopes, tc.target)
220+
u, err := url.Parse(got)
221+
if err != nil {
222+
t.Fatalf("parsing URL: %v", err)
223+
}
224+
if g := u.Query().Get("target"); g != tc.wantTarget {
225+
t.Errorf("target = %q, want %q", g, tc.wantTarget)
226+
}
227+
// destination_url must still be present in every variant.
228+
if u.Query().Get("destination_url") == "" {
229+
t.Error("destination_url should be set regardless of target")
230+
}
231+
})
232+
}
233+
}
234+
235+
func TestWithDiscoveryAccountTarget(t *testing.T) {
236+
var a PersistentAuth
237+
if a.discoveryAccountTarget {
238+
t.Fatal("discoveryAccountTarget should default to false")
239+
}
240+
WithDiscoveryAccountTarget()(&a)
241+
if !a.discoveryAccountTarget {
242+
t.Error("WithDiscoveryAccountTarget did not set discoveryAccountTarget")
243+
}
244+
}
245+
202246
func TestWithDiscoveryHost_NormalizesScheme(t *testing.T) {
203247
tests := []struct {
204248
name string

credentials/u2m/persistent_auth.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@ type PersistentAuth struct {
111111
// discoveryHost overrides the default login.databricks.com host used by
112112
// the discovery flow. Empty means the production host.
113113
discoveryHost string
114+
115+
// discoveryAccountTarget, when true, instructs the discovery flow to set
116+
// the top-level `target=ACCOUNT` query parameter on the authorize URL so
117+
// login.databricks.com lands the user on the account selector instead of
118+
// the workspace selector. Use for account-only logins.
119+
discoveryAccountTarget bool
114120
}
115121

116122
type PersistentAuthOption func(*PersistentAuth)
@@ -200,6 +206,18 @@ func WithDiscoveryHost(host string) PersistentAuthOption {
200206
}
201207
}
202208

209+
// WithDiscoveryAccountTarget sets the top-level `target=ACCOUNT` query
210+
// parameter on the discovery authorize URL so login.databricks.com lands the
211+
// user on the account selector instead of the workspace selector. Use for
212+
// account-only logins where workspace selection would be a wasted step.
213+
//
214+
// Has no effect unless WithDiscoveryLogin is also set.
215+
func WithDiscoveryAccountTarget() PersistentAuthOption {
216+
return func(a *PersistentAuth) {
217+
a.discoveryAccountTarget = true
218+
}
219+
}
220+
203221
// NewPersistentAuth creates a new PersistentAuth with the provided options.
204222
func NewPersistentAuth(ctx context.Context, opts ...PersistentAuthOption) (*PersistentAuth, error) {
205223
p := &PersistentAuth{}
@@ -424,6 +442,9 @@ func (a *PersistentAuth) discoveryChallenge() error {
424442
}
425443
defer a.Close()
426444
ds := &discoveryTokenSource{pa: a, host: a.discoveryHost}
445+
if a.discoveryAccountTarget {
446+
ds.target = discoveryTargetAccount
447+
}
427448
return ds.challenge()
428449
}
429450

0 commit comments

Comments
 (0)