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