github.com/mook-as/cf-cli@v7.0.0-beta.28.0.20200120190804-b91c115fae48+incompatible/cf/api/authentication/authentication.go (about)

     1  package authentication
     2  
     3  import (
     4  	"encoding/base64"
     5  	"fmt"
     6  	"net/http"
     7  	"net/url"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/SermoDigital/jose/jws"
    12  
    13  	"code.cloudfoundry.org/cli/cf/configuration/coreconfig"
    14  	"code.cloudfoundry.org/cli/cf/errors"
    15  	. "code.cloudfoundry.org/cli/cf/i18n"
    16  	"code.cloudfoundry.org/cli/cf/net"
    17  	"code.cloudfoundry.org/cli/util"
    18  )
    19  
    20  //go:generate counterfeiter . TokenRefresher
    21  
    22  const accessTokenExpirationMargin = time.Minute
    23  
    24  type TokenRefresher interface {
    25  	RefreshAuthToken() (updatedToken string, apiErr error)
    26  }
    27  
    28  //go:generate counterfeiter . Repository
    29  
    30  type Repository interface {
    31  	net.RequestDumperInterface
    32  
    33  	RefreshAuthToken() (updatedToken string, apiErr error)
    34  	RefreshToken(token string) (updatedToken string, apiErr error)
    35  	Authenticate(credentials map[string]string) (apiErr error)
    36  	Authorize(token string) (string, error)
    37  	GetLoginPromptsAndSaveUAAServerURL() (map[string]coreconfig.AuthPrompt, error)
    38  }
    39  
    40  type UAARepository struct {
    41  	config  coreconfig.ReadWriter
    42  	gateway net.Gateway
    43  	dumper  net.RequestDumper
    44  }
    45  
    46  var ErrPreventRedirect = errors.New("prevent-redirect")
    47  
    48  func NewUAARepository(gateway net.Gateway, config coreconfig.ReadWriter, dumper net.RequestDumper) UAARepository {
    49  	return UAARepository{
    50  		config:  config,
    51  		gateway: gateway,
    52  		dumper:  dumper,
    53  	}
    54  }
    55  
    56  func (uaa UAARepository) Authorize(token string) (string, error) {
    57  	httpClient := &http.Client{
    58  		CheckRedirect: func(req *http.Request, _ []*http.Request) error {
    59  			uaa.DumpRequest(req)
    60  			return ErrPreventRedirect
    61  		},
    62  		Timeout: 30 * time.Second,
    63  		Transport: &http.Transport{
    64  			DisableKeepAlives:   true,
    65  			TLSClientConfig:     util.NewTLSConfig(nil, uaa.config.IsSSLDisabled()),
    66  			Proxy:               http.ProxyFromEnvironment,
    67  			TLSHandshakeTimeout: 10 * time.Second,
    68  		},
    69  	}
    70  
    71  	authorizeURL, err := url.Parse(uaa.config.UaaEndpoint())
    72  	if err != nil {
    73  		return "", err
    74  	}
    75  
    76  	values := url.Values{}
    77  	values.Set("response_type", "code")
    78  	values.Set("grant_type", "authorization_code")
    79  	values.Set("client_id", uaa.config.SSHOAuthClient())
    80  
    81  	authorizeURL.Path = "/oauth/authorize"
    82  	authorizeURL.RawQuery = values.Encode()
    83  
    84  	authorizeReq, err := http.NewRequest("GET", authorizeURL.String(), nil)
    85  	if err != nil {
    86  		return "", err
    87  	}
    88  
    89  	authorizeReq.Header.Add("authorization", token)
    90  
    91  	resp, err := httpClient.Do(authorizeReq)
    92  	if resp != nil {
    93  		uaa.DumpResponse(resp)
    94  	}
    95  	if err == nil {
    96  		return "", errors.New(T("Authorization server did not redirect with one time code"))
    97  	}
    98  
    99  	if netErr, ok := err.(*url.Error); !ok || netErr.Err != ErrPreventRedirect {
   100  		return "", errors.New(T("Error requesting one time code from server: {{.Error}}", map[string]interface{}{"Error": err.Error()}))
   101  	}
   102  
   103  	loc, err := resp.Location()
   104  	if err != nil {
   105  		return "", errors.New(T("Error getting the redirected location: {{.Error}}", map[string]interface{}{"Error": err.Error()}))
   106  	}
   107  
   108  	codes := loc.Query()["code"]
   109  	if len(codes) != 1 {
   110  		return "", errors.New(T("Unable to acquire one time code from authorization response"))
   111  	}
   112  
   113  	return codes[0], nil
   114  }
   115  
   116  func (uaa UAARepository) Authenticate(credentials map[string]string) error {
   117  	data := url.Values{
   118  		"grant_type": {"password"},
   119  		"scope":      {""},
   120  	}
   121  	for key, val := range credentials {
   122  		data[key] = []string{val}
   123  	}
   124  
   125  	err := uaa.getAuthToken(data)
   126  	if err != nil {
   127  		httpError, ok := err.(errors.HTTPError)
   128  		if ok {
   129  			switch {
   130  			case httpError.StatusCode() == http.StatusUnauthorized:
   131  				return errors.New(T("Credentials were rejected, please try again."))
   132  			case httpError.StatusCode() >= http.StatusInternalServerError:
   133  				return errors.New(T("The targeted API endpoint could not be reached."))
   134  			}
   135  		}
   136  
   137  		return err
   138  	}
   139  
   140  	return nil
   141  }
   142  
   143  func (uaa UAARepository) DumpRequest(req *http.Request) {
   144  	uaa.dumper.DumpRequest(req)
   145  }
   146  
   147  func (uaa UAARepository) DumpResponse(res *http.Response) {
   148  	uaa.dumper.DumpResponse(res)
   149  }
   150  
   151  type LoginResource struct {
   152  	Prompts map[string][]string
   153  	Links   map[string]string
   154  }
   155  
   156  var knownAuthPromptTypes = map[string]coreconfig.AuthPromptType{
   157  	"text":     coreconfig.AuthPromptTypeText,
   158  	"password": coreconfig.AuthPromptTypePassword,
   159  }
   160  
   161  func (r *LoginResource) parsePrompts() (prompts map[string]coreconfig.AuthPrompt) {
   162  	prompts = make(map[string]coreconfig.AuthPrompt)
   163  	for key, val := range r.Prompts {
   164  		prompts[key] = coreconfig.AuthPrompt{
   165  			Type:        knownAuthPromptTypes[val[0]],
   166  			DisplayName: val[1],
   167  		}
   168  	}
   169  	return
   170  }
   171  
   172  func (uaa UAARepository) GetLoginPromptsAndSaveUAAServerURL() (prompts map[string]coreconfig.AuthPrompt, apiErr error) {
   173  	url := fmt.Sprintf("%s/login", uaa.config.AuthenticationEndpoint())
   174  	resource := &LoginResource{}
   175  	apiErr = uaa.gateway.GetResource(url, resource)
   176  
   177  	prompts = resource.parsePrompts()
   178  	if resource.Links["uaa"] == "" {
   179  		uaa.config.SetUaaEndpoint(uaa.config.AuthenticationEndpoint())
   180  	} else {
   181  		uaa.config.SetUaaEndpoint(resource.Links["uaa"])
   182  	}
   183  	return
   184  }
   185  
   186  func (uaa UAARepository) RefreshAuthToken() (string, error) {
   187  	return uaa.RefreshToken(uaa.config.AccessToken())
   188  }
   189  
   190  func (uaa UAARepository) RefreshToken(t string) (string, error) {
   191  	tokenStr := strings.TrimPrefix(t, "bearer ")
   192  	token, err := jws.ParseJWT([]byte(tokenStr))
   193  	if err != nil {
   194  		return "", err
   195  	}
   196  	expiration, ok := token.Claims().Expiration()
   197  	if ok && expiration.Sub(time.Now()) > accessTokenExpirationMargin {
   198  		return t, nil
   199  	}
   200  
   201  	data := url.Values{}
   202  
   203  	switch uaa.config.UAAGrantType() {
   204  	case "client_credentials":
   205  		data.Add("client_id", uaa.config.UAAOAuthClient())
   206  		data.Add("client_secret", uaa.config.UAAOAuthClientSecret())
   207  		data.Add("grant_type", "client_credentials")
   208  	case "", "password": // CLI used to leave field blank for password; preserve compatibility with old files
   209  		data.Add("grant_type", "refresh_token")
   210  		data.Add("refresh_token", uaa.config.RefreshToken())
   211  		data.Add("scope", "")
   212  	}
   213  
   214  	apiErr := uaa.getAuthToken(data)
   215  	updatedToken := uaa.config.AccessToken()
   216  
   217  	return updatedToken, apiErr
   218  }
   219  
   220  func (uaa UAARepository) getAuthToken(data url.Values) error {
   221  	var accessToken string
   222  
   223  	type uaaErrorResponse struct {
   224  		Code        string `json:"error"`
   225  		Description string `json:"error_description"`
   226  	}
   227  
   228  	type AuthenticationResponse struct {
   229  		AccessToken  string           `json:"access_token"`
   230  		TokenType    string           `json:"token_type"`
   231  		RefreshToken string           `json:"refresh_token"`
   232  		Error        uaaErrorResponse `json:"error"`
   233  	}
   234  
   235  	path := fmt.Sprintf("%s/oauth/token", uaa.config.AuthenticationEndpoint())
   236  
   237  	if uaa.config.UAAGrantType() != "client_credentials" {
   238  		accessToken = "Basic " + base64.StdEncoding.EncodeToString([]byte(uaa.config.UAAOAuthClient()+":"+uaa.config.UAAOAuthClientSecret()))
   239  	}
   240  
   241  	request, err := uaa.gateway.NewRequest("POST", path, accessToken, strings.NewReader(data.Encode()))
   242  
   243  	if err != nil {
   244  		return fmt.Errorf("%s: %s", T("Failed to start oauth request"), err.Error())
   245  	}
   246  	request.HTTPReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
   247  
   248  	response := new(AuthenticationResponse)
   249  	_, err = uaa.gateway.PerformRequestForJSONResponse(request, &response)
   250  
   251  	switch err.(type) {
   252  	case nil:
   253  	case errors.HTTPError:
   254  		return err
   255  	case *errors.InvalidTokenError:
   256  		return errors.New(T("Authentication has expired.  Please log back in to re-authenticate.\n\nTIP: Use `cf login -a <endpoint> -u <user> -o <org> -s <space>` to log back in and re-authenticate."))
   257  	default:
   258  		return fmt.Errorf("%s: %s", T("auth request failed"), err.Error())
   259  	}
   260  
   261  	// TODO: get the actual status code
   262  	if response.Error.Code != "" {
   263  		return errors.NewHTTPError(0, response.Error.Code, response.Error.Description)
   264  	}
   265  
   266  	uaa.config.SetAccessToken(fmt.Sprintf("%s %s", response.TokenType, response.AccessToken))
   267  	uaa.config.SetRefreshToken(response.RefreshToken)
   268  
   269  	return nil
   270  }