diff --git a/remotes/docker/auth/fetch.go b/remotes/docker/auth/fetch.go index f3cf48a1f..0a25b3359 100644 --- a/remotes/docker/auth/fetch.go +++ b/remotes/docker/auth/fetch.go @@ -57,6 +57,7 @@ func newUnexpectedStatusErr(resp *http.Response) error { return ErrUnexpectedStatus{Status: resp.Status, StatusCode: resp.StatusCode, Body: b} } +// GenerateTokenOptions generates options for fetching a token based on a challenge func GenerateTokenOptions(ctx context.Context, host, username, secret string, c Challenge) (TokenOptions, error) { realm, ok := c.Parameters["realm"] if !ok { @@ -94,7 +95,8 @@ type TokenOptions struct { Secret string } -type postTokenResponse struct { +// OAuthTokenResponse is response from fetching token with a OAuth POST request +type OAuthTokenResponse struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` ExpiresIn int `json:"expires_in"` @@ -102,7 +104,8 @@ type postTokenResponse struct { Scope string `json:"scope"` } -func FetchTokenWithOAuth(ctx context.Context, client *http.Client, headers http.Header, clientID string, to TokenOptions) (string, error) { +// FetchTokenWithOAuth fetches a token using a POST request +func FetchTokenWithOAuth(ctx context.Context, client *http.Client, headers http.Header, clientID string, to TokenOptions) (*OAuthTokenResponse, error) { form := url.Values{} if len(to.Scopes) > 0 { form.Set("scope", strings.Join(to.Scopes, " ")) @@ -121,7 +124,7 @@ func FetchTokenWithOAuth(ctx context.Context, client *http.Client, headers http. req, err := http.NewRequest("POST", to.Realm, strings.NewReader(form.Encode())) if err != nil { - return "", err + return nil, err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8") if headers != nil { @@ -132,25 +135,30 @@ func FetchTokenWithOAuth(ctx context.Context, client *http.Client, headers http. resp, err := ctxhttp.Do(ctx, client, req) if err != nil { - return "", err + return nil, err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 400 { - return "", errors.WithStack(newUnexpectedStatusErr(resp)) + return nil, errors.WithStack(newUnexpectedStatusErr(resp)) } decoder := json.NewDecoder(resp.Body) - var tr postTokenResponse + var tr OAuthTokenResponse if err = decoder.Decode(&tr); err != nil { - return "", errors.Errorf("unable to decode token response: %s", err) + return nil, errors.Wrap(err, "unable to decode token response") } - return tr.AccessToken, nil + if tr.AccessToken == "" { + return nil, errors.WithStack(ErrNoToken) + } + + return &tr, nil } -type getTokenResponse struct { +// FetchTokenResponse is response from fetching token with GET request +type FetchTokenResponse struct { Token string `json:"token"` AccessToken string `json:"access_token"` ExpiresIn int `json:"expires_in"` @@ -159,10 +167,10 @@ type getTokenResponse struct { } // FetchToken fetches a token using a GET request -func FetchToken(ctx context.Context, client *http.Client, headers http.Header, to TokenOptions) (string, error) { +func FetchToken(ctx context.Context, client *http.Client, headers http.Header, to TokenOptions) (*FetchTokenResponse, error) { req, err := http.NewRequest("GET", to.Realm, nil) if err != nil { - return "", err + return nil, err } if headers != nil { @@ -189,19 +197,19 @@ func FetchToken(ctx context.Context, client *http.Client, headers http.Header, t resp, err := ctxhttp.Do(ctx, client, req) if err != nil { - return "", err + return nil, err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 400 { - return "", errors.WithStack(newUnexpectedStatusErr(resp)) + return nil, errors.WithStack(newUnexpectedStatusErr(resp)) } decoder := json.NewDecoder(resp.Body) - var tr getTokenResponse + var tr FetchTokenResponse if err = decoder.Decode(&tr); err != nil { - return "", errors.Errorf("unable to decode token response: %s", err) + return nil, errors.Wrap(err, "unable to decode token response") } // `access_token` is equivalent to `token` and if both are specified @@ -212,8 +220,8 @@ func FetchToken(ctx context.Context, client *http.Client, headers http.Header, t } if tr.Token == "" { - return "", ErrNoToken + return nil, errors.WithStack(ErrNoToken) } - return tr.Token, nil + return &tr, nil } diff --git a/remotes/docker/authorizer.go b/remotes/docker/authorizer.go index 4552de338..787310e05 100644 --- a/remotes/docker/authorizer.go +++ b/remotes/docker/authorizer.go @@ -241,7 +241,7 @@ func (ah *authHandler) doBasicAuth(ctx context.Context) (string, error) { return fmt.Sprintf("Basic %s", auth), nil } -func (ah *authHandler) doBearerAuth(ctx context.Context) (string, error) { +func (ah *authHandler) doBearerAuth(ctx context.Context) (token string, err error) { // copy common tokenOptions to := ah.common @@ -263,15 +263,20 @@ func (ah *authHandler) doBearerAuth(ctx context.Context) (string, error) { ah.scopedTokens[scoped] = r ah.Unlock() + defer func() { + token = fmt.Sprintf("Bearer %s", token) + r.token, r.err = token, err + r.Done() + }() + // fetch token for the resource scope - var ( - token string - err error - ) if to.Secret != "" { + defer func() { + err = errors.Wrap(err, "failed to fetch oauth token") + }() // credential information is provided, use oauth POST endpoint // TODO: Allow setting client_id - token, err = auth.FetchTokenWithOAuth(ctx, ah.client, ah.header, "containerd-client", to) + resp, err := auth.FetchTokenWithOAuth(ctx, ah.client, ah.header, "containerd-client", to) if err != nil { var errStatus auth.ErrUnexpectedStatus if errors.As(err, &errStatus) { @@ -279,26 +284,27 @@ func (ah *authHandler) doBearerAuth(ctx context.Context) (string, error) { // As of September 2017, GCR is known to return 404. // As of February 2018, JFrog Artifactory is known to return 401. if (errStatus.StatusCode == 405 && to.Username != "") || errStatus.StatusCode == 404 || errStatus.StatusCode == 401 { - token, err = auth.FetchToken(ctx, ah.client, ah.header, to) - } else { - log.G(ctx).WithFields(logrus.Fields{ - "status": errStatus.Status, - "body": string(errStatus.Body), - }).Debugf("token request failed") + resp, err := auth.FetchToken(ctx, ah.client, ah.header, to) + if err != nil { + return "", err + } + return resp.Token, nil } + log.G(ctx).WithFields(logrus.Fields{ + "status": errStatus.Status, + "body": string(errStatus.Body), + }).Debugf("token request failed") } + return "", err } - err = errors.Wrap(err, "failed to fetch oauth token") - } else { - // do request anonymously - token, err = auth.FetchToken(ctx, ah.client, ah.header, to) - err = errors.Wrap(err, "failed to fetch anonymous token") + return resp.AccessToken, nil } - token = fmt.Sprintf("Bearer %s", token) - - r.token, r.err = token, err - r.Done() - return r.token, r.err + // do request anonymously + resp, err := auth.FetchToken(ctx, ah.client, ah.header, to) + if err != nil { + return "", errors.Wrap(err, "failed to fetch anonymous token") + } + return resp.Token, nil } func invalidAuthorization(c auth.Challenge, responses []*http.Response) error {