Skip to content

Commit

Permalink
Merge pull request containerd#6396 from AkihiroSuda/refresh-token
Browse files Browse the repository at this point in the history
remotes/docker: allow fetching "refresh token" (aka "identity token", "offline token")
  • Loading branch information
dmcgowan authored Jan 6, 2022
2 parents 857b35d + 97623ab commit 3ccd43c
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 26 deletions.
16 changes: 16 additions & 0 deletions remotes/docker/auth/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ type TokenOptions struct {
Scopes []string
Username string
Secret string

// FetchRefreshToken enables fetching a refresh token (aka "identity token", "offline token") along with the bearer token.
//
// For HTTP GET mode (FetchToken), FetchRefreshToken sets `offline_token=true` in the request.
// https://docs.docker.com/registry/spec/auth/token/#requesting-a-token
//
// For HTTP POST mode (FetchTokenWithOAuth), FetchRefreshToken sets `access_type=offline` in the request.
// https://docs.docker.com/registry/spec/auth/oauth/#getting-a-token
FetchRefreshToken bool
}

// OAuthTokenResponse is response from fetching token with a OAuth POST request
Expand Down Expand Up @@ -101,6 +110,9 @@ func FetchTokenWithOAuth(ctx context.Context, client *http.Client, headers http.
form.Set("username", to.Username)
form.Set("password", to.Secret)
}
if to.FetchRefreshToken {
form.Set("access_type", "offline")
}

req, err := http.NewRequest("POST", to.Realm, strings.NewReader(form.Encode()))
if err != nil {
Expand Down Expand Up @@ -175,6 +187,10 @@ func FetchToken(ctx context.Context, client *http.Client, headers http.Header, t
req.SetBasicAuth(to.Username, to.Secret)
}

if to.FetchRefreshToken {
reqParams.Add("offline_token", "true")
}

req.URL.RawQuery = reqParams.Encode()

resp, err := ctxhttp.Do(ctx, client, req)
Expand Down
75 changes: 50 additions & 25 deletions remotes/docker/authorizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ type dockerAuthorizer struct {

client *http.Client
header http.Header
mu sync.Mutex
mu sync.RWMutex

// indexed by host name
handlers map[string]*authHandler

onFetchRefreshToken OnFetchRefreshToken
}

// NewAuthorizer creates a Docker authorizer using the provided function to
Expand All @@ -51,9 +53,10 @@ func NewAuthorizer(client *http.Client, f func(string) (string, string, error))
}

type authorizerConfig struct {
credentials func(string) (string, string, error)
client *http.Client
header http.Header
credentials func(string) (string, string, error)
client *http.Client
header http.Header
onFetchRefreshToken OnFetchRefreshToken
}

// AuthorizerOpt configures an authorizer
Expand All @@ -80,6 +83,16 @@ func WithAuthHeader(hdr http.Header) AuthorizerOpt {
}
}

// OnFetchRefreshToken is called on fetching request token.
type OnFetchRefreshToken func(ctx context.Context, refreshToken string, req *http.Request)

// WithFetchRefreshToken enables fetching "refresh token" (aka "identity token", "offline token").
func WithFetchRefreshToken(f OnFetchRefreshToken) AuthorizerOpt {
return func(opt *authorizerConfig) {
opt.onFetchRefreshToken = f
}
}

// NewDockerAuthorizer creates an authorizer using Docker's registry
// authentication spec.
// See https://docs.docker.com/registry/spec/auth/
Expand All @@ -94,10 +107,11 @@ func NewDockerAuthorizer(opts ...AuthorizerOpt) Authorizer {
}

return &dockerAuthorizer{
credentials: ao.credentials,
client: ao.client,
header: ao.header,
handlers: make(map[string]*authHandler),
credentials: ao.credentials,
client: ao.client,
header: ao.header,
handlers: make(map[string]*authHandler),
onFetchRefreshToken: ao.onFetchRefreshToken,
}
}

Expand All @@ -109,12 +123,21 @@ func (a *dockerAuthorizer) Authorize(ctx context.Context, req *http.Request) err
return nil
}

auth, err := ah.authorize(ctx)
auth, refreshToken, err := ah.authorize(ctx)
if err != nil {
return err
}

req.Header.Set("Authorization", auth)

if refreshToken != "" {
a.mu.RLock()
onFetchRefreshToken := a.onFetchRefreshToken
a.mu.RUnlock()
if onFetchRefreshToken != nil {
onFetchRefreshToken(ctx, refreshToken, req)
}
}
return nil
}

Expand Down Expand Up @@ -161,6 +184,7 @@ func (a *dockerAuthorizer) AddResponses(ctx context.Context, responses []*http.R
if err != nil {
return err
}
common.FetchRefreshToken = a.onFetchRefreshToken != nil

a.handlers[host] = newAuthHandler(a.client, a.header, c.Scheme, common)
return nil
Expand All @@ -187,8 +211,9 @@ func (a *dockerAuthorizer) AddResponses(ctx context.Context, responses []*http.R
// authResult is used to control limit rate.
type authResult struct {
sync.WaitGroup
token string
err error
token string
refreshToken string
err error
}

// authHandler is used to handle auth request per registry server.
Expand Down Expand Up @@ -220,29 +245,29 @@ func newAuthHandler(client *http.Client, hdr http.Header, scheme auth.Authentica
}
}

func (ah *authHandler) authorize(ctx context.Context) (string, error) {
func (ah *authHandler) authorize(ctx context.Context) (string, string, error) {
switch ah.scheme {
case auth.BasicAuth:
return ah.doBasicAuth(ctx)
case auth.BearerAuth:
return ah.doBearerAuth(ctx)
default:
return "", errors.Wrapf(errdefs.ErrNotImplemented, "failed to find supported auth scheme: %s", string(ah.scheme))
return "", "", errors.Wrapf(errdefs.ErrNotImplemented, "failed to find supported auth scheme: %s", string(ah.scheme))
}
}

func (ah *authHandler) doBasicAuth(ctx context.Context) (string, error) {
func (ah *authHandler) doBasicAuth(ctx context.Context) (string, string, error) {
username, secret := ah.common.Username, ah.common.Secret

if username == "" || secret == "" {
return "", fmt.Errorf("failed to handle basic auth because missing username or secret")
return "", "", fmt.Errorf("failed to handle basic auth because missing username or secret")
}

auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + secret))
return fmt.Sprintf("Basic %s", auth), nil
return fmt.Sprintf("Basic %s", auth), "", nil
}

func (ah *authHandler) doBearerAuth(ctx context.Context) (token string, err error) {
func (ah *authHandler) doBearerAuth(ctx context.Context) (token, refreshToken string, err error) {
// copy common tokenOptions
to := ah.common

Expand All @@ -255,7 +280,7 @@ func (ah *authHandler) doBearerAuth(ctx context.Context) (token string, err erro
if r, exist := ah.scopedTokens[scoped]; exist {
ah.Unlock()
r.Wait()
return r.token, r.err
return r.token, r.refreshToken, r.err
}

// only one fetch token job
Expand All @@ -266,7 +291,7 @@ func (ah *authHandler) doBearerAuth(ctx context.Context) (token string, err erro

defer func() {
token = fmt.Sprintf("Bearer %s", token)
r.token, r.err = token, err
r.token, r.refreshToken, r.err = token, refreshToken, err
r.Done()
}()

Expand All @@ -287,25 +312,25 @@ func (ah *authHandler) doBearerAuth(ctx context.Context) (token string, err erro
if (errStatus.StatusCode == 405 && to.Username != "") || errStatus.StatusCode == 404 || errStatus.StatusCode == 401 {
resp, err := auth.FetchToken(ctx, ah.client, ah.header, to)
if err != nil {
return "", err
return "", "", err
}
return resp.Token, nil
return resp.Token, resp.RefreshToken, nil
}
log.G(ctx).WithFields(logrus.Fields{
"status": errStatus.Status,
"body": string(errStatus.Body),
}).Debugf("token request failed")
}
return "", err
return "", "", err
}
return resp.AccessToken, nil
return resp.AccessToken, resp.RefreshToken, nil
}
// 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 "", "", errors.Wrap(err, "failed to fetch anonymous token")
}
return resp.Token, nil
return resp.Token, resp.RefreshToken, nil
}

func invalidAuthorization(c auth.Challenge, responses []*http.Response) error {
Expand Down
4 changes: 3 additions & 1 deletion remotes/docker/config/hosts.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ type HostOptions struct {
DefaultTLS *tls.Config
DefaultScheme string
// UpdateClient will be called after creating http.Client object, so clients can provide extra configuration
UpdateClient UpdateClientFunc
UpdateClient UpdateClientFunc
AuthorizerOpts []docker.AuthorizerOpt
}

// ConfigureHosts creates a registry hosts function from the provided
Expand Down Expand Up @@ -143,6 +144,7 @@ func ConfigureHosts(ctx context.Context, options HostOptions) docker.RegistryHos
if options.Credentials != nil {
authOpts = append(authOpts, docker.WithAuthCreds(options.Credentials))
}
authOpts = append(authOpts, options.AuthorizerOpts...)
authorizer := docker.NewDockerAuthorizer(authOpts...)

rhosts := make([]docker.RegistryHost, len(hosts))
Expand Down
Loading

0 comments on commit 3ccd43c

Please sign in to comment.