package tokencmd import ( "errors" "fmt" "io" "io/ioutil" "net/http" "net/url" "strings" "github.com/golang/glog" apierrs "k8s.io/kubernetes/pkg/api/errors" "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/client/restclient" "k8s.io/kubernetes/pkg/util/sets" ) // CSRFTokenHeader is a marker header that indicates we are not a browser that got tricked into requesting basic auth // Corresponds to the header expected by basic-auth challenging authenticators const CSRFTokenHeader = "X-CSRF-Token" // ChallengeHandler handles responses to WWW-Authenticate challenges. type ChallengeHandler interface { // CanHandle returns true if the handler recognizes a challenge it thinks it can handle. CanHandle(headers http.Header) bool // HandleChallenge lets the handler attempt to handle a challenge. // It is only invoked if CanHandle() returned true for the given headers. // Returns response headers and true if the challenge is successfully handled. // Returns false if the challenge was not handled, and an optional error in error cases. HandleChallenge(requestURL string, headers http.Header) (http.Header, bool, error) // CompleteChallenge is invoked with the headers from a successful server response // received after having handled one or more challenges. // Returns an error if the handler does not consider the challenge/response interaction complete. CompleteChallenge(requestURL string, headers http.Header) error // Release gives the handler a chance to release any resources held during a challenge/response sequence. // It is always invoked, even in cases where no challenges were received or handled. Release() error } type RequestTokenOptions struct { ClientConfig *restclient.Config Handler ChallengeHandler } // RequestToken uses the cmd arguments to locate an openshift oauth server and attempts to authenticate // it returns the access token if it gets one. An error if it does not func RequestToken(clientCfg *restclient.Config, reader io.Reader, defaultUsername string, defaultPassword string) (string, error) { handlers := []ChallengeHandler{} if GSSAPIEnabled() { handlers = append(handlers, NewNegotiateChallengeHandler(NewGSSAPINegotiator(defaultUsername))) } if BasicEnabled() { handlers = append(handlers, &BasicChallengeHandler{Host: clientCfg.Host, Reader: reader, Username: defaultUsername, Password: defaultPassword}) } var handler ChallengeHandler if len(handlers) == 1 { handler = handlers[0] } else { handler = NewMultiHandler(handlers...) } opts := &RequestTokenOptions{ ClientConfig: clientCfg, Handler: handler, } return opts.RequestToken() } // RequestToken locates an openshift oauth server and attempts to authenticate. // It returns the access token if it gets one, or an error if it does not. // It should only be invoked once on a given RequestTokenOptions instance. // The Handler held by the options is released as part of this call. func (o *RequestTokenOptions) RequestToken() (string, error) { defer func() { // Always release the handler if err := o.Handler.Release(); err != nil { // Release errors shouldn't fail the token request, just log glog.V(4).Infof("error releasing handler: %v", err) } }() rt, err := restclient.TransportFor(o.ClientConfig) if err != nil { return "", err } // requestURL holds the current URL to make requests to. This can change if the server responds with a redirect requestURL := o.ClientConfig.Host + "/oauth/authorize?response_type=token&client_id=openshift-challenging-client" // requestHeaders holds additional headers to add to the request. This can be changed by o.Handlers requestHeaders := http.Header{} // requestedURLSet/requestedURLList hold the URLs we have requested, to prevent redirect loops. Gets reset when a challenge is handled. requestedURLSet := sets.NewString() requestedURLList := []string{} handledChallenge := false for { // Make the request resp, err := request(rt, requestURL, requestHeaders) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode == http.StatusUnauthorized { if resp.Header.Get("WWW-Authenticate") != "" { if !o.Handler.CanHandle(resp.Header) { return "", apierrs.NewUnauthorized("unhandled challenge") } // Handle the challenge newRequestHeaders, shouldRetry, err := o.Handler.HandleChallenge(requestURL, resp.Header) if err != nil { return "", err } if !shouldRetry { return "", apierrs.NewUnauthorized("challenger chose not to retry the request") } // Remember if we've ever handled a challenge handledChallenge = true // Reset request set/list. Since we're setting different headers, it is legitimate to request the same urls requestedURLSet = sets.NewString() requestedURLList = []string{} // Use the response to the challenge as the new headers requestHeaders = newRequestHeaders continue } // Unauthorized with no challenge unauthorizedError := apierrs.NewUnauthorized("") // Attempt to read body content and include as an error detail if details, err := ioutil.ReadAll(resp.Body); err == nil && len(details) > 0 { unauthorizedError.ErrStatus.Details = &unversioned.StatusDetails{ Causes: []unversioned.StatusCause{ {Message: string(details)}, }, } } return "", unauthorizedError } // if we've ever handled a challenge, see if the handler also considers the interaction complete. // this is required for negotiate flows with mutual authentication. if handledChallenge { if err := o.Handler.CompleteChallenge(requestURL, resp.Header); err != nil { return "", err } } if resp.StatusCode == http.StatusFound { redirectURL := resp.Header.Get("Location") // OAuth response case (access_token or error parameter) accessToken, err := oauthAuthorizeResult(redirectURL) if err != nil { return "", err } if len(accessToken) > 0 { return accessToken, err } // Non-OAuth response, just follow the URL // add to our list of redirects requestedURLList = append(requestedURLList, redirectURL) // detect loops if !requestedURLSet.Has(redirectURL) { requestedURLSet.Insert(redirectURL) requestURL = redirectURL continue } return "", apierrs.NewInternalError(fmt.Errorf("redirect loop: %s", strings.Join(requestedURLList, " -> "))) } // Unknown response return "", apierrs.NewInternalError(fmt.Errorf("unexpected response: %d", resp.StatusCode)) } } func oauthAuthorizeResult(location string) (string, error) { u, err := url.Parse(location) if err != nil { return "", err } if errorCode := u.Query().Get("error"); len(errorCode) > 0 { errorDescription := u.Query().Get("error_description") return "", errors.New(errorCode + " " + errorDescription) } // Grab the raw fragment ourselves, since the stdlib URL parsing decodes parts of it fragment := "" if parts := strings.SplitN(location, "#", 2); len(parts) == 2 { fragment = parts[1] } fragmentValues, err := url.ParseQuery(fragment) if err != nil { return "", err } if accessToken := fragmentValues.Get("access_token"); len(accessToken) > 0 { return accessToken, nil } return "", nil } func request(rt http.RoundTripper, requestURL string, requestHeaders http.Header) (*http.Response, error) { // Build the request req, err := http.NewRequest("GET", requestURL, nil) if err != nil { return nil, err } for k, v := range requestHeaders { req.Header[k] = v } req.Header.Set(CSRFTokenHeader, "1") // Make the request return rt.RoundTrip(req) }