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 }