github.com/koron/hk@v0.0.0-20150303213137-b8aeaa3ab34c/auth.go (about)

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"net/url"
     6  	"os"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/heroku/hk/Godeps/_workspace/src/github.com/bgentry/heroku-go"
    11  	"github.com/heroku/hk/Godeps/_workspace/src/github.com/bgentry/speakeasy"
    12  	"github.com/heroku/hk/hkclient"
    13  	"github.com/heroku/hk/term"
    14  )
    15  
    16  var cmdAuthorize = &Command{
    17  	Run:      runAuthorize,
    18  	Usage:    "authorize",
    19  	Category: "hk",
    20  	Short:    "procure a temporary privileged token" + extra,
    21  	Long: `
    22  Have heroku-agent procure and store a temporary privileged token
    23  that will bypass any requirement for a second authentication factor.
    24  
    25  Example:
    26  
    27      $ hk authorize
    28      Enter email: user@test.com
    29  	Enter two-factor auth code: 
    30      Authorization successful.
    31  `,
    32  }
    33  
    34  func runAuthorize(cmd *Command, args []string) {
    35  	if len(args) != 0 {
    36  		cmd.PrintUsage()
    37  		os.Exit(2)
    38  	}
    39  
    40  	if os.Getenv("HEROKU_AGENT_SOCK") == "" {
    41  		printFatal("Authorize must be used with heroku-agent; please set " +
    42  			"HEROKU_AGENT_SOCK")
    43  	}
    44  
    45  	var twoFactorCode string
    46  	fmt.Printf("Enter two-factor auth code: ")
    47  	if _, err := fmt.Scanln(&twoFactorCode); err != nil {
    48  		printFatal("reading two-factor auth code: " + err.Error())
    49  	}
    50  
    51  	client.AdditionalHeaders.Set("Heroku-Two-Factor-Code", twoFactorCode)
    52  
    53  	// No-op: GET /apps with max=0. heroku-agent will detect that a two-factor
    54  	// code was included and attempt to procure a temporary token. This token
    55  	// will then be re-used automatically on subsequent requests.
    56  	_, err := client.AppList(&heroku.ListRange{Field: "name", Max: 0})
    57  	must(err)
    58  
    59  	fmt.Println("Authorization successful.")
    60  }
    61  
    62  var cmdCreds = &Command{
    63  	Run:      runCreds,
    64  	Usage:    "creds",
    65  	Category: "hk",
    66  	Short:    "show credentials" + extra,
    67  	Long:     `Creds shows credentials that will be used for API calls.`,
    68  }
    69  
    70  func runCreds(cmd *Command, args []string) {
    71  	var err error
    72  
    73  	nrc, err = hkclient.LoadNetRc()
    74  	if err != nil {
    75  		printFatal(err.Error())
    76  	}
    77  
    78  	u, err := url.Parse(apiURL)
    79  	if err != nil {
    80  		printFatal("could not parse API url: " + err.Error())
    81  	}
    82  
    83  	user, pass, err := nrc.GetCreds(u)
    84  	if err != nil {
    85  		printFatal("could not get credentials: " + err.Error())
    86  	}
    87  
    88  	fmt.Println(user, pass)
    89  }
    90  
    91  var cmdLogin = &Command{
    92  	Run:      runLogin,
    93  	Usage:    "login",
    94  	Category: "hk",
    95  	Short:    "log in to your Heroku account" + extra,
    96  	Long: `
    97  Log in with your Heroku credentials. Input is accepted by typing
    98  on the terminal. On unix machines, you can also pipe a password
    99  on standard input.
   100  
   101  Example:
   102  
   103      $ hk login
   104      Enter email: user@test.com
   105      Enter password: 
   106      Login successful.
   107  `,
   108  }
   109  
   110  func runLogin(cmd *Command, args []string) {
   111  	if len(args) != 0 {
   112  		cmd.PrintUsage()
   113  		os.Exit(2)
   114  	}
   115  
   116  	oldEmail := client.Username
   117  	var email string
   118  	if oldEmail == "" {
   119  		fmt.Printf("Enter email: ")
   120  	} else {
   121  		fmt.Printf("Enter email [%s]: ", oldEmail)
   122  	}
   123  	_, err := fmt.Scanln(&email)
   124  	switch {
   125  	case err != nil && err.Error() != "unexpected newline":
   126  		printFatal(err.Error())
   127  	case email == "" && oldEmail == "":
   128  		printFatal("email is required.")
   129  	case email == "":
   130  		email = oldEmail
   131  	}
   132  
   133  	// NOTE: gopass doesn't support multi-byte chars on Windows
   134  	password, err := readPassword("Enter password: ")
   135  	switch {
   136  	case err == nil:
   137  	case err.Error() == "unexpected newline":
   138  		printFatal("password is required.")
   139  	default:
   140  		printFatal(err.Error())
   141  	}
   142  
   143  	hostname, token, err := attemptLogin(email, password, "")
   144  	if err != nil {
   145  		if herror, ok := err.(heroku.Error); ok && herror.Id == "two_factor" {
   146  			// 2FA requested, attempt 2FA login
   147  			var twoFactorCode string
   148  			fmt.Printf("Enter two-factor auth code: ")
   149  			if _, err := fmt.Scanln(&twoFactorCode); err != nil {
   150  				printFatal("reading two-factor auth code: " + err.Error())
   151  			}
   152  			hostname, token, err = attemptLogin(email, password, twoFactorCode)
   153  			must(err)
   154  		} else {
   155  			must(err)
   156  		}
   157  	}
   158  
   159  	nrc, err = hkclient.LoadNetRc()
   160  	if err != nil {
   161  		printFatal("loading netrc: " + err.Error())
   162  	}
   163  
   164  	err = nrc.SaveCreds(hostname, email, token)
   165  	if err != nil {
   166  		printFatal("saving new token: " + err.Error())
   167  	}
   168  	fmt.Println("Logged in.")
   169  }
   170  
   171  func readPassword(prompt string) (password string, err error) {
   172  	if acceptPasswordFromStdin && !term.IsTerminal(os.Stdin) {
   173  		_, err = fmt.Scanln(&password)
   174  		return
   175  	}
   176  	// NOTE: speakeasy may not support multi-byte chars on Windows
   177  	return speakeasy.Ask("Enter password: ")
   178  }
   179  
   180  func attemptLogin(username, password, twoFactorCode string) (hostname, token string, err error) {
   181  	description := "hk login from " + time.Now().UTC().Format(time.RFC3339)
   182  	expires := 2592000 // 30 days
   183  	opts := heroku.OAuthAuthorizationCreateOpts{
   184  		Description: &description,
   185  		ExpiresIn:   &expires,
   186  	}
   187  
   188  	req, err := client.NewRequest("POST", "/oauth/authorizations", &opts)
   189  	if err != nil {
   190  		return "", "", fmt.Errorf("unknown error when creating login request: %s", err.Error())
   191  	}
   192  	req.SetBasicAuth(username, password)
   193  
   194  	if twoFactorCode != "" {
   195  		req.Header.Set("Heroku-Two-Factor-Code", twoFactorCode)
   196  	}
   197  
   198  	var auth heroku.OAuthAuthorization
   199  	if err = client.DoReq(req, &auth); err != nil {
   200  		return
   201  	}
   202  	if auth.AccessToken == nil {
   203  		return "", "", fmt.Errorf("access token missing from Heroku API login response")
   204  	}
   205  	return strings.Split(req.Host, ":")[0], auth.AccessToken.Token, nil
   206  }
   207  
   208  var cmdLogout = &Command{
   209  	Run:      runLogout,
   210  	Usage:    "logout",
   211  	Category: "hk",
   212  	Short:    "log out of your Heroku account" + extra,
   213  	Long: `
   214  Log out of your Heroku account and remove credentials from
   215  this machine.
   216  
   217  Example:
   218  
   219      $ hk logout
   220      Logged out.
   221  `,
   222  }
   223  
   224  func runLogout(cmd *Command, args []string) {
   225  	if len(args) != 0 {
   226  		cmd.PrintUsage()
   227  		os.Exit(2)
   228  	}
   229  	u, err := url.Parse(client.URL)
   230  	if err != nil {
   231  		printFatal("couldn't parse client URL: " + err.Error())
   232  	}
   233  
   234  	nrc, err = hkclient.LoadNetRc()
   235  	if err != nil {
   236  		printError(err.Error())
   237  	}
   238  
   239  	err = removeCreds(strings.Split(u.Host, ":")[0])
   240  	if err != nil {
   241  		printFatal("saving new netrc: " + err.Error())
   242  	}
   243  	fmt.Println("Logged out.")
   244  }