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