github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/command/login.go (about)

     1  package command
     2  
     3  import (
     4  	"context"
     5  	"crypto/sha256"
     6  	"encoding/base64"
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"io/ioutil"
    11  	"log"
    12  	"math/rand"
    13  	"net"
    14  	"net/http"
    15  	"net/url"
    16  	"path/filepath"
    17  	"strings"
    18  
    19  	tfe "github.com/hashicorp/go-tfe"
    20  	svchost "github.com/hashicorp/terraform-svchost"
    21  	svcauth "github.com/hashicorp/terraform-svchost/auth"
    22  	"github.com/hashicorp/terraform-svchost/disco"
    23  	"github.com/hashicorp/terraform/internal/command/cliconfig"
    24  	"github.com/hashicorp/terraform/internal/httpclient"
    25  	"github.com/hashicorp/terraform/internal/logging"
    26  	"github.com/hashicorp/terraform/internal/terraform"
    27  	"github.com/hashicorp/terraform/internal/tfdiags"
    28  
    29  	uuid "github.com/hashicorp/go-uuid"
    30  	"golang.org/x/oauth2"
    31  )
    32  
    33  // LoginCommand is a Command implementation that runs an interactive login
    34  // flow for a remote service host. It then stashes credentials in a tfrc
    35  // file in the user's home directory.
    36  type LoginCommand struct {
    37  	Meta
    38  }
    39  
    40  // Run implements cli.Command.
    41  func (c *LoginCommand) Run(args []string) int {
    42  	args = c.Meta.process(args)
    43  	cmdFlags := c.Meta.extendedFlagSet("login")
    44  	cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
    45  	if err := cmdFlags.Parse(args); err != nil {
    46  		return 1
    47  	}
    48  
    49  	args = cmdFlags.Args()
    50  	if len(args) > 1 {
    51  		c.Ui.Error(
    52  			"The login command expects at most one argument: the host to log in to.")
    53  		cmdFlags.Usage()
    54  		return 1
    55  	}
    56  
    57  	var diags tfdiags.Diagnostics
    58  
    59  	if !c.input {
    60  		diags = diags.Append(tfdiags.Sourceless(
    61  			tfdiags.Error,
    62  			"Login is an interactive command",
    63  			"The \"terraform login\" command uses interactive prompts to obtain and record credentials, so it can't be run with input disabled.\n\nTo configure credentials in a non-interactive context, write existing credentials directly to a CLI configuration file.",
    64  		))
    65  		c.showDiagnostics(diags)
    66  		return 1
    67  	}
    68  
    69  	givenHostname := "app.terraform.io"
    70  	if len(args) != 0 {
    71  		givenHostname = args[0]
    72  	}
    73  
    74  	hostname, err := svchost.ForComparison(givenHostname)
    75  	if err != nil {
    76  		diags = diags.Append(tfdiags.Sourceless(
    77  			tfdiags.Error,
    78  			"Invalid hostname",
    79  			fmt.Sprintf("The given hostname %q is not valid: %s.", givenHostname, err.Error()),
    80  		))
    81  		c.showDiagnostics(diags)
    82  		return 1
    83  	}
    84  
    85  	// From now on, since we've validated the given hostname, we should use
    86  	// dispHostname in the UI to ensure we're presenting it in the canonical
    87  	// form, in case that helpers users with debugging when things aren't
    88  	// working as expected. (Perhaps the normalization is part of the cause.)
    89  	dispHostname := hostname.ForDisplay()
    90  
    91  	host, err := c.Services.Discover(hostname)
    92  	if err != nil {
    93  		diags = diags.Append(tfdiags.Sourceless(
    94  			tfdiags.Error,
    95  			"Service discovery failed for "+dispHostname,
    96  
    97  			// Contrary to usual Go idiom, the Discover function returns
    98  			// full sentences with initial capitalization in its error messages,
    99  			// and they are written with the end-user as the audience. We
   100  			// only need to add the trailing period to make them consistent
   101  			// with our usual error reporting standards.
   102  			err.Error()+".",
   103  		))
   104  		c.showDiagnostics(diags)
   105  		return 1
   106  	}
   107  
   108  	creds := c.Services.CredentialsSource().(*cliconfig.CredentialsSource)
   109  	filename, _ := creds.CredentialsFilePath()
   110  	credsCtx := &loginCredentialsContext{
   111  		Location:      creds.HostCredentialsLocation(hostname),
   112  		LocalFilename: filename, // empty in the very unlikely event that we can't select a config directory for this user
   113  		HelperType:    creds.CredentialsHelperType(),
   114  	}
   115  
   116  	clientConfig, err := host.ServiceOAuthClient("login.v1")
   117  	switch err.(type) {
   118  	case nil:
   119  		// Great! No problem, then.
   120  	case *disco.ErrServiceNotProvided:
   121  		// This is also fine! We'll try the manual token creation process.
   122  	case *disco.ErrVersionNotSupported:
   123  		diags = diags.Append(tfdiags.Sourceless(
   124  			tfdiags.Warning,
   125  			"Host does not support Terraform login",
   126  			fmt.Sprintf("The given hostname %q allows creating Terraform authorization tokens, but requires a newer version of Terraform CLI to do so.", dispHostname),
   127  		))
   128  	default:
   129  		diags = diags.Append(tfdiags.Sourceless(
   130  			tfdiags.Warning,
   131  			"Host does not support Terraform login",
   132  			fmt.Sprintf("The given hostname %q cannot support \"terraform login\": %s.", dispHostname, err),
   133  		))
   134  	}
   135  
   136  	// If login service is unavailable, check for a TFE v2 API as fallback
   137  	var tfeservice *url.URL
   138  	if clientConfig == nil {
   139  		tfeservice, err = host.ServiceURL("tfe.v2")
   140  		switch err.(type) {
   141  		case nil:
   142  			// Success!
   143  		case *disco.ErrServiceNotProvided:
   144  			diags = diags.Append(tfdiags.Sourceless(
   145  				tfdiags.Error,
   146  				"Host does not support Terraform tokens API",
   147  				fmt.Sprintf("The given hostname %q does not support creating Terraform authorization tokens.", dispHostname),
   148  			))
   149  		case *disco.ErrVersionNotSupported:
   150  			diags = diags.Append(tfdiags.Sourceless(
   151  				tfdiags.Error,
   152  				"Host does not support Terraform tokens API",
   153  				fmt.Sprintf("The given hostname %q allows creating Terraform authorization tokens, but requires a newer version of Terraform CLI to do so.", dispHostname),
   154  			))
   155  		default:
   156  			diags = diags.Append(tfdiags.Sourceless(
   157  				tfdiags.Error,
   158  				"Host does not support Terraform tokens API",
   159  				fmt.Sprintf("The given hostname %q cannot support \"terraform login\": %s.", dispHostname, err),
   160  			))
   161  		}
   162  	}
   163  
   164  	if credsCtx.Location == cliconfig.CredentialsInOtherFile {
   165  		diags = diags.Append(tfdiags.Sourceless(
   166  			tfdiags.Error,
   167  			fmt.Sprintf("Credentials for %s are manually configured", dispHostname),
   168  			"The \"terraform login\" command cannot log in because credentials for this host are already configured in a CLI configuration file.\n\nTo log in, first revoke the existing credentials and remove that block from the CLI configuration.",
   169  		))
   170  	}
   171  
   172  	if diags.HasErrors() {
   173  		c.showDiagnostics(diags)
   174  		return 1
   175  	}
   176  
   177  	var token svcauth.HostCredentialsToken
   178  	var tokenDiags tfdiags.Diagnostics
   179  
   180  	// Prefer Terraform login if available
   181  	if clientConfig != nil {
   182  		var oauthToken *oauth2.Token
   183  
   184  		switch {
   185  		case clientConfig.SupportedGrantTypes.Has(disco.OAuthAuthzCodeGrant):
   186  			// We prefer an OAuth code grant if the server supports it.
   187  			oauthToken, tokenDiags = c.interactiveGetTokenByCode(hostname, credsCtx, clientConfig)
   188  		case clientConfig.SupportedGrantTypes.Has(disco.OAuthOwnerPasswordGrant) && hostname == svchost.Hostname("app.terraform.io"):
   189  			// The password grant type is allowed only for Terraform Cloud SaaS.
   190  			// Note this case is purely theoretical at this point, as TFC currently uses
   191  			// its own bespoke login protocol (tfe)
   192  			oauthToken, tokenDiags = c.interactiveGetTokenByPassword(hostname, credsCtx, clientConfig)
   193  		default:
   194  			tokenDiags = tokenDiags.Append(tfdiags.Sourceless(
   195  				tfdiags.Error,
   196  				"Host does not support Terraform login",
   197  				fmt.Sprintf("The given hostname %q does not allow any OAuth grant types that are supported by this version of Terraform.", dispHostname),
   198  			))
   199  		}
   200  		if oauthToken != nil {
   201  			token = svcauth.HostCredentialsToken(oauthToken.AccessToken)
   202  		}
   203  	} else if tfeservice != nil {
   204  		token, tokenDiags = c.interactiveGetTokenByUI(hostname, credsCtx, tfeservice)
   205  	}
   206  
   207  	diags = diags.Append(tokenDiags)
   208  	if diags.HasErrors() {
   209  		c.showDiagnostics(diags)
   210  		return 1
   211  	}
   212  
   213  	err = creds.StoreForHost(hostname, token)
   214  	if err != nil {
   215  		diags = diags.Append(tfdiags.Sourceless(
   216  			tfdiags.Error,
   217  			"Failed to save API token",
   218  			fmt.Sprintf("The given host returned an API token, but Terraform failed to save it: %s.", err),
   219  		))
   220  	}
   221  
   222  	c.showDiagnostics(diags)
   223  	if diags.HasErrors() {
   224  		return 1
   225  	}
   226  
   227  	c.Ui.Output("\n---------------------------------------------------------------------------------\n")
   228  	if hostname == "app.terraform.io" { // Terraform Cloud
   229  		var motd struct {
   230  			Message string        `json:"msg"`
   231  			Errors  []interface{} `json:"errors"`
   232  		}
   233  
   234  		// Throughout the entire process of fetching a MOTD from TFC, use a default
   235  		// message if the platform-provided message is unavailable for any reason -
   236  		// be it the service isn't provided, the request failed, or any sort of
   237  		// platform error returned.
   238  
   239  		motdServiceURL, err := host.ServiceURL("motd.v1")
   240  		if err != nil {
   241  			c.logMOTDError(err)
   242  			c.outputDefaultTFCLoginSuccess()
   243  			return 0
   244  		}
   245  
   246  		req, err := http.NewRequest("GET", motdServiceURL.String(), nil)
   247  		if err != nil {
   248  			c.logMOTDError(err)
   249  			c.outputDefaultTFCLoginSuccess()
   250  			return 0
   251  		}
   252  
   253  		req.Header.Set("Authorization", "Bearer "+token.Token())
   254  
   255  		resp, err := httpclient.New().Do(req)
   256  		if err != nil {
   257  			c.logMOTDError(err)
   258  			c.outputDefaultTFCLoginSuccess()
   259  			return 0
   260  		}
   261  
   262  		body, err := ioutil.ReadAll(resp.Body)
   263  		if err != nil {
   264  			c.logMOTDError(err)
   265  			c.outputDefaultTFCLoginSuccess()
   266  			return 0
   267  		}
   268  
   269  		defer resp.Body.Close()
   270  		json.Unmarshal(body, &motd)
   271  
   272  		if motd.Errors == nil && motd.Message != "" {
   273  			c.Ui.Output(
   274  				c.Colorize().Color(motd.Message),
   275  			)
   276  			return 0
   277  		} else {
   278  			c.logMOTDError(fmt.Errorf("platform responded with errors or an empty message"))
   279  			c.outputDefaultTFCLoginSuccess()
   280  			return 0
   281  		}
   282  	}
   283  
   284  	if tfeservice != nil { // Terraform Enterprise
   285  		c.outputDefaultTFELoginSuccess(dispHostname)
   286  	} else {
   287  		c.Ui.Output(
   288  			fmt.Sprintf(
   289  				c.Colorize().Color(strings.TrimSpace(`
   290  [green][bold]Success![reset] [bold]Terraform has obtained and saved an API token.[reset]
   291  
   292  The new API token will be used for any future Terraform command that must make
   293  authenticated requests to %s.
   294  `)),
   295  				dispHostname,
   296  			) + "\n",
   297  		)
   298  	}
   299  
   300  	return 0
   301  }
   302  
   303  func (c *LoginCommand) outputDefaultTFELoginSuccess(dispHostname string) {
   304  	c.Ui.Output(
   305  		fmt.Sprintf(
   306  			c.Colorize().Color(strings.TrimSpace(`
   307  [green][bold]Success![reset] [bold]Logged in to Terraform Enterprise (%s)[reset]
   308  `)),
   309  			dispHostname,
   310  		) + "\n",
   311  	)
   312  }
   313  
   314  func (c *LoginCommand) outputDefaultTFCLoginSuccess() {
   315  	c.Ui.Output(c.Colorize().Color(strings.TrimSpace(`
   316  [green][bold]Success![reset] [bold]Logged in to Terraform Cloud[reset]
   317  ` + "\n")))
   318  }
   319  
   320  func (c *LoginCommand) logMOTDError(err error) {
   321  	log.Printf("[TRACE] login: An error occurred attempting to fetch a message of the day for Terraform Cloud: %s", err)
   322  }
   323  
   324  // Help implements cli.Command.
   325  func (c *LoginCommand) Help() string {
   326  	defaultFile := c.defaultOutputFile()
   327  	if defaultFile == "" {
   328  		// Because this is just for the help message and it's very unlikely
   329  		// that a user wouldn't have a functioning home directory anyway,
   330  		// we'll just use a placeholder here. The real command has some
   331  		// more complex behavior for this case. This result is not correct
   332  		// on all platforms, but given how unlikely we are to hit this case
   333  		// that seems okay.
   334  		defaultFile = "~/.terraform/credentials.tfrc.json"
   335  	}
   336  
   337  	helpText := fmt.Sprintf(`
   338  Usage: terraform [global options] login [hostname]
   339  
   340    Retrieves an authentication token for the given hostname, if it supports
   341    automatic login, and saves it in a credentials file in your home directory.
   342  
   343    If no hostname is provided, the default hostname is app.terraform.io, to
   344    log in to Terraform Cloud.
   345  
   346    If not overridden by credentials helper settings in the CLI configuration,
   347    the credentials will be written to the following local file:
   348        %s
   349  `, defaultFile)
   350  	return strings.TrimSpace(helpText)
   351  }
   352  
   353  // Synopsis implements cli.Command.
   354  func (c *LoginCommand) Synopsis() string {
   355  	return "Obtain and save credentials for a remote host"
   356  }
   357  
   358  func (c *LoginCommand) defaultOutputFile() string {
   359  	if c.CLIConfigDir == "" {
   360  		return "" // no default available
   361  	}
   362  	return filepath.Join(c.CLIConfigDir, "credentials.tfrc.json")
   363  }
   364  
   365  func (c *LoginCommand) interactiveGetTokenByCode(hostname svchost.Hostname, credsCtx *loginCredentialsContext, clientConfig *disco.OAuthClient) (*oauth2.Token, tfdiags.Diagnostics) {
   366  	var diags tfdiags.Diagnostics
   367  
   368  	confirm, confirmDiags := c.interactiveContextConsent(hostname, disco.OAuthAuthzCodeGrant, credsCtx)
   369  	diags = diags.Append(confirmDiags)
   370  	if !confirm {
   371  		diags = diags.Append(errors.New("Login cancelled"))
   372  		return nil, diags
   373  	}
   374  
   375  	// We'll use an entirely pseudo-random UUID for our temporary request
   376  	// state. The OAuth server must echo this back to us in the callback
   377  	// request to make it difficult for some other running process to
   378  	// interfere by sending its own request to our temporary server.
   379  	reqState, err := uuid.GenerateUUID()
   380  	if err != nil {
   381  		// This should be very unlikely, but could potentially occur if e.g.
   382  		// there's not enough pseudo-random entropy available.
   383  		diags = diags.Append(tfdiags.Sourceless(
   384  			tfdiags.Error,
   385  			"Can't generate login request state",
   386  			fmt.Sprintf("Cannot generate random request identifier for login request: %s.", err),
   387  		))
   388  		return nil, diags
   389  	}
   390  
   391  	proofKey, proofKeyChallenge, err := c.proofKey()
   392  	if err != nil {
   393  		diags = diags.Append(tfdiags.Sourceless(
   394  			tfdiags.Error,
   395  			"Can't generate login request state",
   396  			fmt.Sprintf("Cannot generate random prrof key for login request: %s.", err),
   397  		))
   398  		return nil, diags
   399  	}
   400  
   401  	listener, callbackURL, err := c.listenerForCallback(clientConfig.MinPort, clientConfig.MaxPort)
   402  	if err != nil {
   403  		diags = diags.Append(tfdiags.Sourceless(
   404  			tfdiags.Error,
   405  			"Can't start temporary login server",
   406  			fmt.Sprintf(
   407  				"The login process uses OAuth, which requires starting a temporary HTTP server on localhost. However, no TCP port numbers between %d and %d are available to create such a server.",
   408  				clientConfig.MinPort, clientConfig.MaxPort,
   409  			),
   410  		))
   411  		return nil, diags
   412  	}
   413  
   414  	// codeCh will allow our temporary HTTP server to transmit the OAuth code
   415  	// to the main execution path that follows.
   416  	codeCh := make(chan string)
   417  	server := &http.Server{
   418  		Handler: http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
   419  			log.Printf("[TRACE] login: request to callback server")
   420  			err := req.ParseForm()
   421  			if err != nil {
   422  				log.Printf("[ERROR] login: cannot ParseForm on callback request: %s", err)
   423  				resp.WriteHeader(400)
   424  				return
   425  			}
   426  			gotState := req.Form.Get("state")
   427  			if gotState != reqState {
   428  				log.Printf("[ERROR] login: incorrect \"state\" value in callback request")
   429  				resp.WriteHeader(400)
   430  				return
   431  			}
   432  			gotCode := req.Form.Get("code")
   433  			if gotCode == "" {
   434  				log.Printf("[ERROR] login: no \"code\" argument in callback request")
   435  				resp.WriteHeader(400)
   436  				return
   437  			}
   438  
   439  			log.Printf("[TRACE] login: request contains an authorization code")
   440  
   441  			// Send the code to our blocking wait below, so that the token
   442  			// fetching process can continue.
   443  			codeCh <- gotCode
   444  			close(codeCh)
   445  
   446  			log.Printf("[TRACE] login: returning response from callback server")
   447  
   448  			resp.Header().Add("Content-Type", "text/html")
   449  			resp.WriteHeader(200)
   450  			resp.Write([]byte(callbackSuccessMessage))
   451  		}),
   452  	}
   453  	go func() {
   454  		defer logging.PanicHandler()
   455  		err := server.Serve(listener)
   456  		if err != nil && err != http.ErrServerClosed {
   457  			diags = diags.Append(tfdiags.Sourceless(
   458  				tfdiags.Error,
   459  				"Can't start temporary login server",
   460  				fmt.Sprintf(
   461  					"The login process uses OAuth, which requires starting a temporary HTTP server on localhost. However, no TCP port numbers between %d and %d are available to create such a server.",
   462  					clientConfig.MinPort, clientConfig.MaxPort,
   463  				),
   464  			))
   465  			close(codeCh)
   466  		}
   467  	}()
   468  
   469  	oauthConfig := &oauth2.Config{
   470  		ClientID:    clientConfig.ID,
   471  		Endpoint:    clientConfig.Endpoint(),
   472  		RedirectURL: callbackURL,
   473  		Scopes:      clientConfig.Scopes,
   474  	}
   475  
   476  	authCodeURL := oauthConfig.AuthCodeURL(
   477  		reqState,
   478  		oauth2.SetAuthURLParam("code_challenge", proofKeyChallenge),
   479  		oauth2.SetAuthURLParam("code_challenge_method", "S256"),
   480  	)
   481  
   482  	launchBrowserManually := false
   483  	if c.BrowserLauncher != nil {
   484  		err = c.BrowserLauncher.OpenURL(authCodeURL)
   485  		if err == nil {
   486  			c.Ui.Output(fmt.Sprintf("Terraform must now open a web browser to the login page for %s.\n", hostname.ForDisplay()))
   487  			c.Ui.Output(fmt.Sprintf("If a browser does not open this automatically, open the following URL to proceed:\n    %s\n", authCodeURL))
   488  		} else {
   489  			// Assume we're on a platform where opening a browser isn't possible.
   490  			launchBrowserManually = true
   491  		}
   492  	} else {
   493  		launchBrowserManually = true
   494  	}
   495  
   496  	if launchBrowserManually {
   497  		c.Ui.Output(fmt.Sprintf("Open the following URL to access the login page for %s:\n    %s\n", hostname.ForDisplay(), authCodeURL))
   498  	}
   499  
   500  	c.Ui.Output("Terraform will now wait for the host to signal that login was successful.\n")
   501  
   502  	code, ok := <-codeCh
   503  	if !ok {
   504  		// If we got no code at all then the server wasn't able to start
   505  		// up, so we'll just give up.
   506  		return nil, diags
   507  	}
   508  
   509  	if err := server.Close(); err != nil {
   510  		// The server will close soon enough when our process exits anyway,
   511  		// so we won't fuss about it for right now.
   512  		log.Printf("[WARN] login: callback server can't shut down: %s", err)
   513  	}
   514  
   515  	ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpclient.New())
   516  	token, err := oauthConfig.Exchange(
   517  		ctx, code,
   518  		oauth2.SetAuthURLParam("code_verifier", proofKey),
   519  	)
   520  	if err != nil {
   521  		diags = diags.Append(tfdiags.Sourceless(
   522  			tfdiags.Error,
   523  			"Failed to obtain auth token",
   524  			fmt.Sprintf("The remote server did not assign an auth token: %s.", err),
   525  		))
   526  		return nil, diags
   527  	}
   528  
   529  	return token, diags
   530  }
   531  
   532  func (c *LoginCommand) interactiveGetTokenByPassword(hostname svchost.Hostname, credsCtx *loginCredentialsContext, clientConfig *disco.OAuthClient) (*oauth2.Token, tfdiags.Diagnostics) {
   533  	var diags tfdiags.Diagnostics
   534  
   535  	confirm, confirmDiags := c.interactiveContextConsent(hostname, disco.OAuthOwnerPasswordGrant, credsCtx)
   536  	diags = diags.Append(confirmDiags)
   537  	if !confirm {
   538  		diags = diags.Append(errors.New("Login cancelled"))
   539  		return nil, diags
   540  	}
   541  
   542  	c.Ui.Output("\n---------------------------------------------------------------------------------\n")
   543  	c.Ui.Output("Terraform must temporarily use your password to request an API token.\nThis password will NOT be saved locally.\n")
   544  
   545  	username, err := c.UIInput().Input(context.Background(), &terraform.InputOpts{
   546  		Id:    "username",
   547  		Query: fmt.Sprintf("Username for %s:", hostname.ForDisplay()),
   548  	})
   549  	if err != nil {
   550  		diags = diags.Append(fmt.Errorf("Failed to request username: %s", err))
   551  		return nil, diags
   552  	}
   553  	password, err := c.UIInput().Input(context.Background(), &terraform.InputOpts{
   554  		Id:     "password",
   555  		Query:  fmt.Sprintf("Password for %s:", hostname.ForDisplay()),
   556  		Secret: true,
   557  	})
   558  	if err != nil {
   559  		diags = diags.Append(fmt.Errorf("Failed to request password: %s", err))
   560  		return nil, diags
   561  	}
   562  
   563  	oauthConfig := &oauth2.Config{
   564  		ClientID: clientConfig.ID,
   565  		Endpoint: clientConfig.Endpoint(),
   566  		Scopes:   clientConfig.Scopes,
   567  	}
   568  	token, err := oauthConfig.PasswordCredentialsToken(context.Background(), username, password)
   569  	if err != nil {
   570  		// FIXME: The OAuth2 library generates errors that are not appropriate
   571  		// for a Terraform end-user audience, so once we have more experience
   572  		// with which errors are most common we should try to recognize them
   573  		// here and produce better error messages for them.
   574  		diags = diags.Append(tfdiags.Sourceless(
   575  			tfdiags.Error,
   576  			"Failed to retrieve API token",
   577  			fmt.Sprintf("The remote host did not issue an API token: %s.", err),
   578  		))
   579  	}
   580  
   581  	return token, diags
   582  }
   583  
   584  func (c *LoginCommand) interactiveGetTokenByUI(hostname svchost.Hostname, credsCtx *loginCredentialsContext, service *url.URL) (svcauth.HostCredentialsToken, tfdiags.Diagnostics) {
   585  	var diags tfdiags.Diagnostics
   586  
   587  	confirm, confirmDiags := c.interactiveContextConsent(hostname, disco.OAuthGrantType(""), credsCtx)
   588  	diags = diags.Append(confirmDiags)
   589  	if !confirm {
   590  		diags = diags.Append(errors.New("Login cancelled"))
   591  		return "", diags
   592  	}
   593  
   594  	c.Ui.Output("\n---------------------------------------------------------------------------------\n")
   595  
   596  	tokensURL := url.URL{
   597  		Scheme:   "https",
   598  		Host:     service.Hostname(),
   599  		Path:     "/app/settings/tokens",
   600  		RawQuery: "source=terraform-login",
   601  	}
   602  
   603  	launchBrowserManually := false
   604  	if c.BrowserLauncher != nil {
   605  		err := c.BrowserLauncher.OpenURL(tokensURL.String())
   606  		if err == nil {
   607  			c.Ui.Output(fmt.Sprintf("Terraform must now open a web browser to the tokens page for %s.\n", hostname.ForDisplay()))
   608  			c.Ui.Output(fmt.Sprintf("If a browser does not open this automatically, open the following URL to proceed:\n    %s\n", tokensURL.String()))
   609  		} else {
   610  			log.Printf("[DEBUG] error opening web browser: %s", err)
   611  			// Assume we're on a platform where opening a browser isn't possible.
   612  			launchBrowserManually = true
   613  		}
   614  	} else {
   615  		launchBrowserManually = true
   616  	}
   617  
   618  	if launchBrowserManually {
   619  		c.Ui.Output(fmt.Sprintf("Open the following URL to access the tokens page for %s:\n    %s\n", hostname.ForDisplay(), tokensURL.String()))
   620  	}
   621  
   622  	c.Ui.Output("\n---------------------------------------------------------------------------------\n")
   623  	c.Ui.Output("Generate a token using your browser, and copy-paste it into this prompt.\n")
   624  
   625  	// credsCtx might not be set if we're using a mock credentials source
   626  	// in a test, but it should always be set in normal use.
   627  	if credsCtx != nil {
   628  		switch credsCtx.Location {
   629  		case cliconfig.CredentialsViaHelper:
   630  			c.Ui.Output(fmt.Sprintf("Terraform will store the token in the configured %q credentials helper\nfor use by subsequent commands.\n", credsCtx.HelperType))
   631  		case cliconfig.CredentialsInPrimaryFile, cliconfig.CredentialsNotAvailable:
   632  			c.Ui.Output(fmt.Sprintf("Terraform will store the token in plain text in the following file\nfor use by subsequent commands:\n    %s\n", credsCtx.LocalFilename))
   633  		}
   634  	}
   635  
   636  	token, err := c.UIInput().Input(context.Background(), &terraform.InputOpts{
   637  		Id:     "token",
   638  		Query:  fmt.Sprintf("Token for %s:", hostname.ForDisplay()),
   639  		Secret: true,
   640  	})
   641  	if err != nil {
   642  		diags := diags.Append(fmt.Errorf("Failed to retrieve token: %s", err))
   643  		return "", diags
   644  	}
   645  
   646  	token = strings.TrimSpace(token)
   647  	cfg := &tfe.Config{
   648  		Address:  service.String(),
   649  		BasePath: service.Path,
   650  		Token:    token,
   651  		Headers:  make(http.Header),
   652  	}
   653  	client, err := tfe.NewClient(cfg)
   654  	if err != nil {
   655  		diags = diags.Append(fmt.Errorf("Failed to create API client: %s", err))
   656  		return "", diags
   657  	}
   658  	user, err := client.Users.ReadCurrent(context.Background())
   659  	if err == tfe.ErrUnauthorized {
   660  		diags = diags.Append(fmt.Errorf("Token is invalid: %s", err))
   661  		return "", diags
   662  	} else if err != nil {
   663  		diags = diags.Append(fmt.Errorf("Failed to retrieve user account details: %s", err))
   664  		return "", diags
   665  	}
   666  	c.Ui.Output(fmt.Sprintf(c.Colorize().Color("\nRetrieved token for user [bold]%s[reset]\n"), user.Username))
   667  
   668  	return svcauth.HostCredentialsToken(token), nil
   669  }
   670  
   671  func (c *LoginCommand) interactiveContextConsent(hostname svchost.Hostname, grantType disco.OAuthGrantType, credsCtx *loginCredentialsContext) (bool, tfdiags.Diagnostics) {
   672  	var diags tfdiags.Diagnostics
   673  	mechanism := "OAuth"
   674  	if grantType == "" {
   675  		mechanism = "your browser"
   676  	}
   677  
   678  	c.Ui.Output(fmt.Sprintf("Terraform will request an API token for %s using %s.\n", hostname.ForDisplay(), mechanism))
   679  
   680  	if grantType.UsesAuthorizationEndpoint() {
   681  		c.Ui.Output(
   682  			"This will work only if you are able to use a web browser on this computer to\ncomplete a login process. If not, you must obtain an API token by another\nmeans and configure it in the CLI configuration manually.\n",
   683  		)
   684  	}
   685  
   686  	// credsCtx might not be set if we're using a mock credentials source
   687  	// in a test, but it should always be set in normal use.
   688  	if credsCtx != nil {
   689  		switch credsCtx.Location {
   690  		case cliconfig.CredentialsViaHelper:
   691  			c.Ui.Output(fmt.Sprintf("If login is successful, Terraform will store the token in the configured\n%q credentials helper for use by subsequent commands.\n", credsCtx.HelperType))
   692  		case cliconfig.CredentialsInPrimaryFile, cliconfig.CredentialsNotAvailable:
   693  			c.Ui.Output(fmt.Sprintf("If login is successful, Terraform will store the token in plain text in\nthe following file for use by subsequent commands:\n    %s\n", credsCtx.LocalFilename))
   694  		}
   695  	}
   696  
   697  	v, err := c.UIInput().Input(context.Background(), &terraform.InputOpts{
   698  		Id:          "approve",
   699  		Query:       "Do you want to proceed?",
   700  		Description: `Only 'yes' will be accepted to confirm.`,
   701  	})
   702  	if err != nil {
   703  		// Should not happen because this command checks that input is enabled
   704  		// before we get to this point.
   705  		diags = diags.Append(err)
   706  		return false, diags
   707  	}
   708  
   709  	return strings.ToLower(v) == "yes", diags
   710  }
   711  
   712  func (c *LoginCommand) listenerForCallback(minPort, maxPort uint16) (net.Listener, string, error) {
   713  	if minPort < 1024 || maxPort < 1024 {
   714  		// This should never happen because it should've been checked by
   715  		// the svchost/disco package when reading the service description,
   716  		// but we'll prefer to fail hard rather than inadvertently trying
   717  		// to open an unprivileged port if there are bugs at that layer.
   718  		panic("listenerForCallback called with privileged port number")
   719  	}
   720  
   721  	availCount := int(maxPort) - int(minPort)
   722  
   723  	// We're going to try port numbers within the range at random, so we need
   724  	// to terminate eventually in case _none_ of the ports are available.
   725  	// We'll make that 150% of the number of ports just to give us some room
   726  	// for the random number generator to generate the same port more than
   727  	// once.
   728  	// Note that we don't really care about true randomness here... we're just
   729  	// trying to hop around in the available port space rather than always
   730  	// working up from the lowest, because we have no information to predict
   731  	// that any particular number will be more likely to be available than
   732  	// another.
   733  	maxTries := availCount + (availCount / 2)
   734  
   735  	for tries := 0; tries < maxTries; tries++ {
   736  		port := rand.Intn(availCount) + int(minPort)
   737  		addr := fmt.Sprintf("127.0.0.1:%d", port)
   738  		log.Printf("[TRACE] login: trying %s as a listen address for temporary OAuth callback server", addr)
   739  		l, err := net.Listen("tcp4", addr)
   740  		if err == nil {
   741  			// We use a path that doesn't end in a slash here because some
   742  			// OAuth server implementations don't allow callback URLs to
   743  			// end with slashes.
   744  			callbackURL := fmt.Sprintf("http://localhost:%d/login", port)
   745  			log.Printf("[TRACE] login: callback URL will be %s", callbackURL)
   746  			return l, callbackURL, nil
   747  		}
   748  	}
   749  
   750  	return nil, "", fmt.Errorf("no suitable TCP ports (between %d and %d) are available for the temporary OAuth callback server", minPort, maxPort)
   751  }
   752  
   753  func (c *LoginCommand) proofKey() (key, challenge string, err error) {
   754  	// Wel use a UUID-like string as the "proof key for code exchange" (PKCE)
   755  	// that will eventually authenticate our request to the token endpoint.
   756  	// Standard UUIDs are explicitly not suitable as secrets according to the
   757  	// UUID spec, but our go-uuid just generates totally random number sequences
   758  	// formatted in the conventional UUID syntax, so that concern does not
   759  	// apply here: this is just a 128-bit crypto-random number.
   760  	uu, err := uuid.GenerateUUID()
   761  	if err != nil {
   762  		return "", "", err
   763  	}
   764  
   765  	key = fmt.Sprintf("%s.%09d", uu, rand.Intn(999999999))
   766  
   767  	h := sha256.New()
   768  	h.Write([]byte(key))
   769  	challenge = base64.RawURLEncoding.EncodeToString(h.Sum(nil))
   770  
   771  	return key, challenge, nil
   772  }
   773  
   774  type loginCredentialsContext struct {
   775  	Location      cliconfig.CredentialsLocation
   776  	LocalFilename string
   777  	HelperType    string
   778  }
   779  
   780  const callbackSuccessMessage = `
   781  <html>
   782  <head>
   783  <title>Terraform Login</title>
   784  <style type="text/css">
   785  body {
   786  	font-family: monospace;
   787  	color: #fff;
   788  	background-color: #000;
   789  }
   790  </style>
   791  </head>
   792  <body>
   793  
   794  <p>The login server has returned an authentication code to Terraform.</p>
   795  <p>Now close this page and return to the terminal where <tt>terraform login</tt>
   796  is running to see the result of the login process.</p>
   797  
   798  </body>
   799  </html>
   800  `