diff --git a/.changelog/182.txt b/.changelog/182.txt new file mode 100644 index 00000000..15b18e8a --- /dev/null +++ b/.changelog/182.txt @@ -0,0 +1,3 @@ +```release-note:improvement +Added option to NewHCPConfig to fail rather than auto login with web browser +``` diff --git a/README.md b/README.md index 058b7d23..41ff64da 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ User session is ideal for getting started or one-off usage. It also works for lo To obtain user credentials, the client credential environment variables `HCP_CLIENT_ID` and `HCP_CLIENT_SECRET` must be unset. When no client credentials are detected, the HCP Go client will prompt the user with a browser login window. Once authenticated, the user session stays refreshed without intervention until it expires after 24 hours. +If you have a use-case with the SDK to leverage the browser login as a feature but want to control if the browser is opened, or even to understand if the system already has a valid token present, you can pass in the option func of `WithoutBrowserLogin()` to your `NewHCPConfig()`. This will use either the provided `ClientID`:`ClientSecret` combo or a valid token that has been previously written to the system. If neither option exists, then `auth.ErrorNoLocalCredsFound` is returned to indicate that the user is not yet logged in. + ### User Profile An HCP Organization ID and Project ID are required to call most HCP APIs. They can be set to the environment variables `HCP_ORGANIZATION_ID` and `HCP_PROJECT_ID`, as in the example below. The HCP Go SDK will read them from the environment and save them in its state as the user's Profile. The Profile Project and Organization IDs will be applied as default values to any request missing them. diff --git a/auth/user.go b/auth/user.go index f957e896..15ae3cfb 100644 --- a/auth/user.go +++ b/auth/user.go @@ -5,6 +5,7 @@ package auth import ( "context" + "errors" "fmt" "log" "time" @@ -12,9 +13,15 @@ import ( "golang.org/x/oauth2" ) +var ( + // ErrorNoLocalCredsFound is returned if no client or user credentials were found and the invoker created the config with the option WithoutBrowserLogin + ErrorNoLocalCredsFound = errors.New("there were no credentials found present on the machine") +) + // UserSession implements the auth package's Session interface type UserSession struct { - browser Browser + browser Browser + NoBrowserLogin bool } // GetToken returns an access token obtained from either an existing session or new browser login. @@ -33,6 +40,12 @@ func (s *UserSession) GetToken(ctx context.Context, conf *oauth2.Config) (*oauth // If session expiry or the AccessTokenExpiry has passed, then reauthenticate with browser login and reassign token. if readErr != nil || cache.SessionExpiry.Before(time.Now()) || cache.AccessTokenExpiry.Before(time.Now()) { + // This is a configuration option set by WithoutBrowserLogin to provide control over when or if the browser is opened + // When the flag is true and no valid credentials are found on the system via ClientID:ClientSecret pairing or a previous unexpired browser login, the client returns an error instead of automatically opening a browser + if s.NoBrowserLogin { + return nil, ErrorNoLocalCredsFound + } + // Login with browser. log.Print("No credentials found, proceeding with browser login.") diff --git a/config/hcp.go b/config/hcp.go index 9861f2ae..e6d90645 100644 --- a/config/hcp.go +++ b/config/hcp.go @@ -95,6 +95,9 @@ type hcpConfig struct { // profile is the user's organization id and project id profile *profile.UserProfile + + // noBrowserLogin is an option to prevent automatic browser login when no local credentials are found. + noBrowserLogin bool } func (c *hcpConfig) Profile() *profile.UserProfile { diff --git a/config/new.go b/config/new.go index f26db213..9273e963 100644 --- a/config/new.go +++ b/config/new.go @@ -96,6 +96,12 @@ func NewHCPConfig(opts ...HCPConfigOption) (HCPConfig, error) { } } + if config.noBrowserLogin { + config.session = &auth.UserSession{ + NoBrowserLogin: true, + } + } + // Set up a token context with the custom auth TLS config tokenTransport := cleanhttp.DefaultPooledTransport() tokenTransport.TLSClientConfig = config.authTLSConfig diff --git a/config/with.go b/config/with.go index d7eaff41..b78962a7 100644 --- a/config/with.go +++ b/config/with.go @@ -137,3 +137,12 @@ func WithProfile(p *profile.UserProfile) HCPConfigOption { return nil } } + +// WithoutBrowserLogin disables the automatic opening of the browser login if no valid auth method is found +// instead force the return of a typed error for users to catch +func WithoutBrowserLogin() HCPConfigOption { + return func(config *hcpConfig) error { + config.noBrowserLogin = true + return nil + } +} diff --git a/config/with_test.go b/config/with_test.go index 283bcc96..d8ec6c43 100644 --- a/config/with_test.go +++ b/config/with_test.go @@ -123,3 +123,14 @@ func TestWith_Profile(t *testing.T) { require.Equal("project-id-1234", config.Profile().ProjectID) } + +func TestWithout_BrowserLogin(t *testing.T) { + require := requirepkg.New(t) + + // Exercise + config := &hcpConfig{} + require.NoError(apply(config, WithoutBrowserLogin())) + + // Ensure browser login is disabled + require.True(config.noBrowserLogin) +}