Skip to content

Commit 667c53b

Browse files
Use a free port in u2m authentication flows rather than 8020. (#1239)
## What changes are proposed in this pull request? This PR modifies the `U2M` flow so that the listener server uses a port that is free rather than the hardcoded `8020`. ## How is this tested? Added a unit tests to verify that different ports are assigned to different instances of `PersistentAuth`.
1 parent 806b42e commit 667c53b

3 files changed

Lines changed: 71 additions & 10 deletions

File tree

NEXT_CHANGELOG.md

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

55
### New Features and Improvements
66

7+
* Use a free port in `u2m` authentication flows rather than 8020.
8+
79
### Bug Fixes
810

911
### Documentation

credentials/u2m/persistent_auth.go

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ const (
2626
appClientID = "databricks-cli"
2727

2828
// appRedirectAddr is the default address for the OAuth2 callback server.
29-
appRedirectAddr = "localhost:8020"
29+
// Using ":0" tells the system to pick a random available port.
30+
appRedirectAddr = "localhost:0"
3031

3132
// listenerTimeout is the maximum amount of time to acquire listener on
3233
// appRedirectAddr.
@@ -57,6 +58,11 @@ type PersistentAuth struct {
5758
// ctx is the context to use for underlying operations. This is needed in
5859
// order to implement the oauth2.TokenSource interface.
5960
ctx context.Context
61+
// redirectAddr is the redirect address for OAuth2 callbacks. The value is
62+
// set to localhost:PORT by startListener which will dynamically assign a
63+
// random port. If a value is already provided, it will be used instead
64+
// (e.g. for testing).
65+
redirectAddr string
6066
}
6167

6268
type PersistentAuthOption func(*PersistentAuth)
@@ -128,9 +134,6 @@ func NewPersistentAuth(ctx context.Context, opts ...PersistentAuthOption) (*Pers
128134
return nil, fmt.Errorf("cache: %w", err)
129135
}
130136
}
131-
if p.oAuthArgument == nil {
132-
return nil, errors.New("missing OAuthArgument")
133-
}
134137
if err := p.validateArg(); err != nil {
135138
return nil, err
136139
}
@@ -162,7 +165,9 @@ func (a *PersistentAuth) Token() (t *oauth2.Token, err error) {
162165
return nil, fmt.Errorf("token refresh: %w", err)
163166
}
164167
}
165-
// do not print refresh token to end-user
168+
169+
// Do not include the refresh token for security reasons. Refresh tokens are
170+
// long-lived credentials that we do not want to expose unnecessarily.
166171
t.RefreshToken = ""
167172
return t, nil
168173
}
@@ -269,12 +274,19 @@ func (a *PersistentAuth) Challenge() error {
269274
// startListener starts a listener on appRedirectAddr, retrying if the address
270275
// is already in use.
271276
func (a *PersistentAuth) startListener(ctx context.Context) error {
277+
// Use the value of redirectURL if it is already set. This is only expected
278+
// in tests to set a fixed redirect URL.
279+
addr := a.redirectAddr
280+
if addr == "" {
281+
addr = appRedirectAddr
282+
}
283+
272284
listener, err := retries.Poll(ctx, listenerTimeout,
273285
func() (*net.Listener, *retries.Err) {
274286
var lc net.ListenConfig
275-
l, err := lc.Listen(ctx, "tcp", appRedirectAddr)
287+
l, err := lc.Listen(ctx, "tcp", addr)
276288
if err != nil {
277-
logger.Debugf(ctx, "failed to listen on %s: %v, retrying", appRedirectAddr, err)
289+
logger.Debugf(ctx, "failed to listen on %s: %v, retrying", addr, err)
278290
return nil, retries.Continue(err)
279291
}
280292
return &l, nil
@@ -283,6 +295,10 @@ func (a *PersistentAuth) startListener(ctx context.Context) error {
283295
return fmt.Errorf("listener: %w", err)
284296
}
285297
a.ln = *listener
298+
299+
// Get the actual address that was assigned (including the port).
300+
a.redirectAddr = a.ln.Addr().String()
301+
logger.Debugf(ctx, "OAuth callback server listening on %s", a.redirectAddr)
286302
return nil
287303
}
288304

@@ -296,6 +312,9 @@ func (a *PersistentAuth) Close() error {
296312
// validateArg ensures that the OAuthArgument is either a WorkspaceOAuthArgument
297313
// or an AccountOAuthArgument.
298314
func (a *PersistentAuth) validateArg() error {
315+
if a.oAuthArgument == nil {
316+
return errors.New("missing OAuthArgument")
317+
}
299318
_, isWorkspaceArg := a.oAuthArgument.(WorkspaceOAuthArgument)
300319
_, isAccountArg := a.oAuthArgument.(AccountOAuthArgument)
301320
if !isWorkspaceArg && !isAccountArg {
@@ -331,7 +350,7 @@ func (a *PersistentAuth) oauth2Config() (*oauth2.Config, error) {
331350
TokenURL: endpoints.TokenEndpoint,
332351
AuthStyle: oauth2.AuthStyleInParams,
333352
},
334-
RedirectURL: fmt.Sprintf("http://%s", appRedirectAddr),
353+
RedirectURL: fmt.Sprintf("http://%s", a.redirectAddr),
335354
Scopes: scopes,
336355
}, nil
337356
}

credentials/u2m/persistent_auth_test.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"net/http"
88
"net/url"
9+
"regexp"
910
"strings"
1011
"testing"
1112
"time"
@@ -297,6 +298,9 @@ func TestChallenge(t *testing.T) {
297298
}
298299
defer p.Close()
299300

301+
// Set a fixed redirect URL for the test.
302+
p.redirectAddr = "localhost:1337"
303+
300304
errc := make(chan error)
301305
go func() {
302306
err := p.Challenge()
@@ -305,7 +309,7 @@ func TestChallenge(t *testing.T) {
305309
}()
306310

307311
state := <-browserOpened
308-
resp, err := http.Get(fmt.Sprintf("http://localhost:8020?code=__THIS__&state=%s", state))
312+
resp, err := http.Get(fmt.Sprintf("http://localhost:1337?code=__THIS__&state=%s", state))
309313
if err != nil {
310314
t.Fatalf("http.Get(): want no error, got %v", err)
311315
}
@@ -347,6 +351,8 @@ func TestChallenge_ReturnsErrorOnFailure(t *testing.T) {
347351
}
348352
defer p.Close()
349353

354+
p.redirectAddr = "localhost:1337" // set a fixed redirect URL for the test
355+
350356
errc := make(chan error)
351357
go func() {
352358
err := p.Challenge()
@@ -355,7 +361,7 @@ func TestChallenge_ReturnsErrorOnFailure(t *testing.T) {
355361
}()
356362

357363
<-browserOpened
358-
resp, err := http.Get("http://localhost:8020?error=access_denied&error_description=Policy%20evaluation%20failed%20for%20this%20request")
364+
resp, err := http.Get("http://localhost:1337?error=access_denied&error_description=Policy%20evaluation%20failed%20for%20this%20request")
359365
if err != nil {
360366
t.Fatalf("http.Get(): want no error, got %v", err)
361367
}
@@ -373,3 +379,37 @@ func TestChallenge_ReturnsErrorOnFailure(t *testing.T) {
373379
t.Fatalf("p.Challenge(): want error containing 'authorize: access_denied: Policy evaluation failed for this request', got %v", err)
374380
}
375381
}
382+
383+
// Verifies that startListener assigns a random port to the redirectAddr.
384+
func TestPersistentAuth_startListener_useDifferentPorts(t *testing.T) {
385+
ctx := context.Background()
386+
arg, err := NewBasicAccountOAuthArgument("https://accounts.cloud.databricks.com", "xyz")
387+
if err != nil {
388+
t.Fatalf("NewBasicAccountOAuthArgument(): want no error, got %v", err)
389+
}
390+
391+
p1, err := NewPersistentAuth(ctx, WithOAuthArgument(arg))
392+
if err != nil {
393+
t.Fatalf("NewPersistentAuth(): want no error, got %v", err)
394+
}
395+
defer p1.Close()
396+
397+
p2, err := NewPersistentAuth(ctx, WithOAuthArgument(arg))
398+
if err != nil {
399+
t.Fatalf("NewPersistentAuth(): want no error, got %v", err)
400+
}
401+
defer p2.Close()
402+
403+
p1.startListener(ctx)
404+
p2.startListener(ctx)
405+
406+
if !regexp.MustCompile(`^127\.0\.0\.1:\d+$`).MatchString(p1.redirectAddr) {
407+
t.Errorf("p1.redirectAddr should be random localhost port, got %s", p1.redirectAddr)
408+
}
409+
if !regexp.MustCompile(`^127\.0\.0\.1:\d+$`).MatchString(p2.redirectAddr) {
410+
t.Errorf("p2.redirectAddr should be random localhost port, got %s", p2.redirectAddr)
411+
}
412+
if p1.redirectAddr == p2.redirectAddr {
413+
t.Errorf("p1.redirectURL and p2.redirectURL should be different, got %s", p1.redirectAddr)
414+
}
415+
}

0 commit comments

Comments
 (0)