github.com/arunkumar7540/cli@v6.45.0+incompatible/cf/api/authentication/authentication.go (about)

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