1package commands
2
3import (
4	"context"
5	"encoding/json"
6	"errors"
7	"fmt"
8	"io"
9	"io/ioutil"
10	"net"
11	"net/http"
12	"os"
13	"strings"
14
15	"github.com/concourse/concourse/atc"
16	"github.com/concourse/concourse/fly/pty"
17	"github.com/concourse/concourse/fly/rc"
18	"github.com/concourse/concourse/go-concourse/concourse"
19	semisemanticversion "github.com/cppforlife/go-semi-semantic/version"
20	"github.com/skratchdot/open-golang/open"
21	"github.com/vito/go-interact/interact"
22	"golang.org/x/crypto/ssh/terminal"
23	"golang.org/x/oauth2"
24)
25
26type LoginCommand struct {
27	ATCURL         string       `short:"c" long:"concourse-url" description:"Concourse URL to authenticate with"`
28	Insecure       bool         `short:"k" long:"insecure" description:"Skip verification of the endpoint's SSL certificate"`
29	Username       string       `short:"u" long:"username" description:"Username for basic auth"`
30	Password       string       `short:"p" long:"password" description:"Password for basic auth"`
31	TeamName       string       `short:"n" long:"team-name" description:"Team to authenticate with"`
32	CACert         atc.PathFlag `long:"ca-cert" description:"Path to Concourse PEM-encoded CA certificate file."`
33	ClientCertPath atc.PathFlag `long:"client-cert" description:"Path to a PEM-encoded client certificate file."`
34	ClientKeyPath  atc.PathFlag `long:"client-key" description:"Path to a PEM-encoded client key file."`
35	OpenBrowser    bool         `short:"b" long:"open-browser" description:"Open browser to the auth endpoint"`
36
37	BrowserOnly bool
38}
39
40func (command *LoginCommand) Execute(args []string) error {
41	if Fly.Target == "" {
42		return errors.New("name for the target must be specified (--target/-t)")
43	}
44
45	var target rc.Target
46	var err error
47
48	var caCert string
49	if command.CACert != "" {
50		caCertBytes, err := ioutil.ReadFile(string(command.CACert))
51		if err != nil {
52			return err
53		}
54		caCert = string(caCertBytes)
55	}
56
57	if command.ATCURL != "" {
58		if command.TeamName == "" {
59			command.TeamName = atc.DefaultTeamName
60		}
61
62		target, err = rc.NewUnauthenticatedTarget(
63			Fly.Target,
64			command.ATCURL,
65			command.TeamName,
66			command.Insecure,
67			caCert,
68			string(command.ClientCertPath),
69			string(command.ClientKeyPath),
70			Fly.Verbose,
71		)
72	} else {
73		target, err = rc.LoadUnauthenticatedTarget(
74			Fly.Target,
75			command.TeamName,
76			command.Insecure,
77			caCert,
78			string(command.ClientCertPath),
79			string(command.ClientKeyPath),
80			Fly.Verbose,
81		)
82	}
83	if err != nil {
84		return err
85	}
86
87	client := target.Client()
88	command.TeamName = target.Team().Name()
89
90	fmt.Printf("logging in to team '%s'\n\n", command.TeamName)
91
92	if len(args) != 0 {
93		return errors.New("unexpected argument [" + strings.Join(args, ", ") + "]")
94	}
95
96	err = target.ValidateWithWarningOnly()
97	if err != nil {
98		return err
99	}
100
101	var tokenType string
102	var tokenValue string
103
104	version, err := target.Version()
105	if err != nil {
106		return err
107	}
108
109	semver, err := semisemanticversion.NewVersionFromString(version)
110	if err != nil {
111		return err
112	}
113
114	legacySemver, err := semisemanticversion.NewVersionFromString("3.14.1")
115	if err != nil {
116		return err
117	}
118
119	devSemver, err := semisemanticversion.NewVersionFromString("0.0.0-dev")
120	if err != nil {
121		return err
122	}
123
124	isRawMode := pty.IsTerminal() && !command.BrowserOnly
125	if isRawMode {
126		state, err := terminal.MakeRaw(int(os.Stdin.Fd()))
127		if err != nil {
128			isRawMode = false
129		} else {
130			defer func() {
131				terminal.Restore(int(os.Stdin.Fd()), state)
132				fmt.Print("\r")
133			}()
134		}
135	}
136
137	if semver.Compare(legacySemver) <= 0 && semver.Compare(devSemver) != 0 {
138		// Legacy Auth Support
139		tokenType, tokenValue, err = command.legacyAuth(target, command.BrowserOnly, isRawMode)
140	} else {
141		if command.Username != "" && command.Password != "" {
142			tokenType, tokenValue, err = command.passwordGrant(client, command.Username, command.Password)
143		} else {
144			tokenType, tokenValue, err = command.authCodeGrant(client.URL(), command.BrowserOnly, isRawMode)
145		}
146	}
147
148	if errors.Is(err, pty.ErrInterrupted) {
149		fmt.Println("^C\r")
150		return nil
151	}
152
153	if err != nil {
154		return err
155	}
156
157	fmt.Println("")
158
159	err = command.verifyTeamExists(client.URL(), rc.TargetToken{
160		Type:  tokenType,
161		Value: tokenValue,
162	}, target.CACert(), target.ClientCertPath(), target.ClientKeyPath())
163
164	if err != nil {
165		return err
166	}
167
168	return command.saveTarget(
169		client.URL(),
170		&rc.TargetToken{
171			Type:  tokenType,
172			Value: tokenValue,
173		},
174		target.CACert(),
175		target.ClientCertPath(),
176		target.ClientKeyPath(),
177	)
178}
179
180func (command *LoginCommand) passwordGrant(client concourse.Client, username, password string) (string, string, error) {
181
182	oauth2Config := oauth2.Config{
183		ClientID:     "fly",
184		ClientSecret: "Zmx5",
185		Endpoint:     oauth2.Endpoint{TokenURL: client.URL() + "/sky/issuer/token"},
186		Scopes:       []string{"openid", "profile", "email", "federated:id", "groups"},
187	}
188
189	ctx := context.WithValue(context.Background(), oauth2.HTTPClient, client.HTTPClient())
190
191	token, err := oauth2Config.PasswordCredentialsToken(ctx, username, password)
192	if err != nil {
193		return "", "", err
194	}
195
196	return token.TokenType, token.AccessToken, nil
197}
198
199func (command *LoginCommand) authCodeGrant(targetUrl string, browserOnly bool, isRawMode bool) (string, string, error) {
200	var tokenStr string
201
202	stdinChannel := make(chan string)
203	tokenChannel := make(chan string)
204	errorChannel := make(chan error)
205	portChannel := make(chan string)
206
207	go listenForTokenCallback(tokenChannel, errorChannel, portChannel, targetUrl)
208
209	port := <-portChannel
210
211	var openURL string
212
213	fmt.Println("navigate to the following URL in your browser:\r")
214	fmt.Println("\r")
215
216	openURL = fmt.Sprintf("%s/login?fly_port=%s", targetUrl, port)
217
218	fmt.Printf("  %s\r\n", openURL)
219
220	if command.OpenBrowser {
221		// try to open the browser window, but don't get all hung up if it
222		// fails, since we already printed about it.
223		_ = open.Start(openURL)
224	}
225
226	if !browserOnly {
227		go waitForTokenInput(stdinChannel, errorChannel, isRawMode)
228	}
229
230	select {
231	case tokenStrMsg := <-tokenChannel:
232		tokenStr = tokenStrMsg
233	case tokenStrMsg := <-stdinChannel:
234		tokenStr = tokenStrMsg
235	case errorMsg := <-errorChannel:
236		return "", "", errorMsg
237	}
238
239	segments := strings.SplitN(tokenStr, " ", 2)
240
241	if len(segments) > 1 {
242		return segments[0], segments[1], nil
243	} else {
244		return "", "", fmt.Errorf("invalid token: %v", tokenStr)
245	}
246}
247
248func listenForTokenCallback(tokenChannel chan string, errorChannel chan error, portChannel chan string, targetUrl string) {
249	s := &http.Server{
250		Addr: "127.0.0.1:0",
251		Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
252			w.Header().Set("Access-Control-Allow-Origin", targetUrl)
253			tokenChannel <- r.FormValue("token")
254			if r.Header.Get("Upgrade-Insecure-Requests") != "" {
255				http.Redirect(w, r, fmt.Sprintf("%s/fly_success?noop=true", targetUrl), http.StatusFound)
256			}
257		}),
258	}
259
260	err := listenAndServeWithPort(s, portChannel)
261
262	if err != nil {
263		errorChannel <- err
264	}
265}
266
267func listenAndServeWithPort(srv *http.Server, portChannel chan string) error {
268	addr := srv.Addr
269	ln, err := net.Listen("tcp", addr)
270	if err != nil {
271		return err
272	}
273
274	_, port, err := net.SplitHostPort(ln.Addr().String())
275	if err != nil {
276		return err
277	}
278
279	portChannel <- port
280
281	return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
282}
283
284type tcpKeepAliveListener struct {
285	*net.TCPListener
286}
287
288func waitForTokenInput(tokenChannel chan string, errorChannel chan error, isRawMode bool) {
289	fmt.Println()
290
291	for {
292		if isRawMode {
293			fmt.Print("or enter token manually (input hidden): ")
294		} else {
295			fmt.Print("or enter token manually: ")
296		}
297		tokenBytes, err := pty.ReadLine(os.Stdin)
298		token := strings.TrimSpace(string(tokenBytes))
299		if len(token) == 0 && err == io.EOF {
300			return
301		}
302		if err != nil && err != io.EOF {
303			errorChannel <- err
304			return
305		}
306
307		parts := strings.Split(token, " ")
308		if len(parts) != 2 {
309			fmt.Println("\rtoken must be of the format 'TYPE VALUE', e.g. 'Bearer ...'\r")
310			continue
311		}
312
313		tokenChannel <- token
314		break
315	}
316}
317
318func (command *LoginCommand) saveTarget(url string, token *rc.TargetToken, caCert string, clientCertPath string, clientKeyPath string) error {
319	err := rc.SaveTarget(
320		Fly.Target,
321		url,
322		command.Insecure,
323		command.TeamName,
324		&rc.TargetToken{
325			Type:  token.Type,
326			Value: token.Value,
327		},
328		caCert,
329		clientCertPath,
330		clientKeyPath,
331	)
332	if err != nil {
333		return err
334	}
335
336	fmt.Println("\rtarget saved\r")
337
338	return nil
339}
340
341func (command *LoginCommand) legacyAuth(target rc.Target, browserOnly bool, isRawMode bool) (string, string, error) {
342
343	httpClient := target.Client().HTTPClient()
344
345	authResponse, err := httpClient.Get(target.URL() + "/api/v1/teams/" + target.Team().Name() + "/auth/methods")
346	if err != nil {
347		return "", "", err
348	}
349
350	type authMethod struct {
351		Type        string `json:"type"`
352		DisplayName string `json:"display_name"`
353		AuthURL     string `json:"auth_url"`
354	}
355
356	defer authResponse.Body.Close()
357
358	var authMethods []authMethod
359	json.NewDecoder(authResponse.Body).Decode(&authMethods)
360
361	var chosenMethod authMethod
362
363	if command.Username != "" || command.Password != "" {
364		for _, method := range authMethods {
365			if method.Type == "basic" {
366				chosenMethod = method
367				break
368			}
369		}
370
371		if chosenMethod.Type == "" {
372			return "", "", errors.New("basic auth is not available")
373		}
374	} else {
375		choices := make([]interact.Choice, len(authMethods))
376
377		for i, method := range authMethods {
378			choices[i] = interact.Choice{
379				Display: method.DisplayName,
380				Value:   method,
381			}
382		}
383
384		if len(choices) == 0 {
385			chosenMethod = authMethod{
386				Type: "none",
387			}
388		}
389
390		if len(choices) == 1 {
391			chosenMethod = authMethods[0]
392		}
393
394		if len(choices) > 1 {
395			err = interact.NewInteraction("choose an auth method", choices...).Resolve(&chosenMethod)
396			if err != nil {
397				return "", "", err
398			}
399
400			fmt.Println("")
401		}
402	}
403
404	switch chosenMethod.Type {
405	case "oauth":
406		var tokenStr string
407
408		stdinChannel := make(chan string)
409		tokenChannel := make(chan string)
410		errorChannel := make(chan error)
411		portChannel := make(chan string)
412
413		go listenForTokenCallback(tokenChannel, errorChannel, portChannel, target.Client().URL())
414
415		port := <-portChannel
416
417		theURL := fmt.Sprintf("%s&fly_local_port=%s\n", chosenMethod.AuthURL, port)
418
419		fmt.Println("navigate to the following URL in your browser:\r")
420		fmt.Println("")
421		fmt.Printf("    %s\r\n", theURL)
422
423		if command.OpenBrowser {
424			// try to open the browser window, but don't get all hung up if it
425			// fails, since we already printed about it.
426			_ = open.Start(theURL)
427		}
428
429		if !browserOnly {
430			go waitForTokenInput(stdinChannel, errorChannel, isRawMode)
431		}
432
433		select {
434		case tokenStrMsg := <-tokenChannel:
435			tokenStr = tokenStrMsg
436		case tokenStrMsg := <-stdinChannel:
437			tokenStr = tokenStrMsg
438		case errorMsg := <-errorChannel:
439			return "", "", errorMsg
440		}
441
442		segments := strings.SplitN(tokenStr, " ", 2)
443
444		return segments[0], segments[1], nil
445
446	case "basic":
447		var username string
448		if command.Username != "" {
449			username = command.Username
450		} else {
451			err := interact.NewInteraction("username").Resolve(interact.Required(&username))
452			if err != nil {
453				return "", "", err
454			}
455		}
456
457		var password string
458		if command.Password != "" {
459			password = command.Password
460		} else {
461			var interactivePassword interact.Password
462			err := interact.NewInteraction("password").Resolve(interact.Required(&interactivePassword))
463			if err != nil {
464				return "", "", err
465			}
466			password = string(interactivePassword)
467		}
468
469		request, err := http.NewRequest("GET", target.URL()+"/api/v1/teams/"+target.Team().Name()+"/auth/token", nil)
470		if err != nil {
471			return "", "", err
472		}
473		request.SetBasicAuth(username, password)
474
475		tokenResponse, err := httpClient.Do(request)
476		if err != nil {
477			return "", "", err
478		}
479
480		type authToken struct {
481			Type  string `json:"token_type"`
482			Value string `json:"token_value"`
483		}
484
485		defer tokenResponse.Body.Close()
486
487		var token authToken
488		json.NewDecoder(tokenResponse.Body).Decode(&token)
489
490		return token.Type, token.Value, nil
491
492	case "none":
493		request, err := http.NewRequest("GET", target.URL()+"/api/v1/teams/"+target.Team().Name()+"/auth/token", nil)
494		if err != nil {
495			return "", "", err
496		}
497
498		tokenResponse, err := httpClient.Do(request)
499		if err != nil {
500			return "", "", err
501		}
502
503		type authToken struct {
504			Type  string `json:"token_type"`
505			Value string `json:"token_value"`
506		}
507
508		defer tokenResponse.Body.Close()
509
510		var token authToken
511		json.NewDecoder(tokenResponse.Body).Decode(&token)
512
513		return token.Type, token.Value, nil
514	}
515
516	return "", "", nil
517}
518
519func (command *LoginCommand) verifyTeamExists(clientUrl string, token rc.TargetToken, caCert string, clientCertPath string,
520	clientKeyPath string) error {
521	verifyTarget, err := rc.NewAuthenticatedTarget("verify",
522		clientUrl,
523		command.TeamName,
524		command.Insecure,
525		&token,
526		caCert,
527		clientCertPath,
528		clientKeyPath,
529		false)
530	if err != nil {
531		return err
532	}
533
534	userInfo, err := verifyTarget.Client().UserInfo()
535	if err != nil {
536		return err
537	}
538
539	if !userInfo.IsAdmin {
540		if userInfo.Teams != nil {
541			_, ok := userInfo.Teams[command.TeamName]
542			if !ok {
543				return errors.New("you are not a member of '" + command.TeamName + "' or the team does not exist")
544			}
545		} else {
546			return errors.New("unable to verify role on team")
547		}
548	} else {
549		teams, err := verifyTarget.Client().ListTeams()
550		if err != nil {
551			return err
552		}
553		var found bool
554		for _, team := range teams {
555			if team.Name == command.TeamName {
556				found = true
557				break
558			}
559		}
560		if !found {
561			return errors.New("team '" + command.TeamName + "' does not exist")
562		}
563	}
564
565	return nil
566}
567