...

Source file src/github.com/google/go-containerregistry/pkg/v1/remote/transport/bearer.go

Documentation: github.com/google/go-containerregistry/pkg/v1/remote/transport

     1  // Copyright 2018 Google LLC All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //    http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package transport
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"errors"
    21  	"fmt"
    22  	"io"
    23  	"net"
    24  	"net/http"
    25  	"net/url"
    26  	"strings"
    27  
    28  	authchallenge "github.com/docker/distribution/registry/client/auth/challenge"
    29  	"github.com/google/go-containerregistry/internal/redact"
    30  	"github.com/google/go-containerregistry/pkg/authn"
    31  	"github.com/google/go-containerregistry/pkg/logs"
    32  	"github.com/google/go-containerregistry/pkg/name"
    33  )
    34  
    35  type Token struct {
    36  	Token        string `json:"token"`
    37  	AccessToken  string `json:"access_token,omitempty"`
    38  	RefreshToken string `json:"refresh_token"`
    39  	ExpiresIn    int    `json:"expires_in"`
    40  }
    41  
    42  // Exchange requests a registry Token with the given scopes.
    43  func Exchange(ctx context.Context, reg name.Registry, auth authn.Authenticator, t http.RoundTripper, scopes []string, pr *Challenge) (*Token, error) {
    44  	if strings.ToLower(pr.Scheme) != "bearer" {
    45  		// TODO: Pretend token for basic?
    46  		return nil, fmt.Errorf("challenge scheme %q is not bearer", pr.Scheme)
    47  	}
    48  	bt, err := fromChallenge(reg, auth, t, pr, scopes...)
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  	authcfg, err := auth.Authorization()
    53  	if err != nil {
    54  		return nil, err
    55  	}
    56  	tok, err := bt.Refresh(ctx, authcfg)
    57  	if err != nil {
    58  		return nil, err
    59  	}
    60  	return tok, nil
    61  }
    62  
    63  // FromToken returns a transport given a Challenge + Token.
    64  func FromToken(reg name.Registry, auth authn.Authenticator, t http.RoundTripper, pr *Challenge, tok *Token) (http.RoundTripper, error) {
    65  	if strings.ToLower(pr.Scheme) != "bearer" {
    66  		return &Wrapper{&basicTransport{inner: t, auth: auth, target: reg.RegistryStr()}}, nil
    67  	}
    68  	bt, err := fromChallenge(reg, auth, t, pr)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  	if tok.Token != "" {
    73  		bt.bearer.RegistryToken = tok.Token
    74  	}
    75  	return &Wrapper{bt}, nil
    76  }
    77  
    78  func fromChallenge(reg name.Registry, auth authn.Authenticator, t http.RoundTripper, pr *Challenge, scopes ...string) (*bearerTransport, error) {
    79  	// We require the realm, which tells us where to send our Basic auth to turn it into Bearer auth.
    80  	realm, ok := pr.Parameters["realm"]
    81  	if !ok {
    82  		return nil, fmt.Errorf("malformed www-authenticate, missing realm: %v", pr.Parameters)
    83  	}
    84  	service := pr.Parameters["service"]
    85  	scheme := "https"
    86  	if pr.Insecure {
    87  		scheme = "http"
    88  	}
    89  	return &bearerTransport{
    90  		inner:    t,
    91  		basic:    auth,
    92  		realm:    realm,
    93  		registry: reg,
    94  		service:  service,
    95  		scopes:   scopes,
    96  		scheme:   scheme,
    97  	}, nil
    98  }
    99  
   100  type bearerTransport struct {
   101  	// Wrapped by bearerTransport.
   102  	inner http.RoundTripper
   103  	// Basic credentials that we exchange for bearer tokens.
   104  	basic authn.Authenticator
   105  	// Holds the bearer response from the token service.
   106  	bearer authn.AuthConfig
   107  	// Registry to which we send bearer tokens.
   108  	registry name.Registry
   109  	// See https://tools.ietf.org/html/rfc6750#section-3
   110  	realm string
   111  	// See https://docs.docker.com/registry/spec/auth/token/
   112  	service string
   113  	scopes  []string
   114  	// Scheme we should use, determined by ping response.
   115  	scheme string
   116  }
   117  
   118  var _ http.RoundTripper = (*bearerTransport)(nil)
   119  
   120  var portMap = map[string]string{
   121  	"http":  "80",
   122  	"https": "443",
   123  }
   124  
   125  func stringSet(ss []string) map[string]struct{} {
   126  	set := make(map[string]struct{})
   127  	for _, s := range ss {
   128  		set[s] = struct{}{}
   129  	}
   130  	return set
   131  }
   132  
   133  // RoundTrip implements http.RoundTripper
   134  func (bt *bearerTransport) RoundTrip(in *http.Request) (*http.Response, error) {
   135  	sendRequest := func() (*http.Response, error) {
   136  		// http.Client handles redirects at a layer above the http.RoundTripper
   137  		// abstraction, so to avoid forwarding Authorization headers to places
   138  		// we are redirected, only set it when the authorization header matches
   139  		// the registry with which we are interacting.
   140  		// In case of redirect http.Client can use an empty Host, check URL too.
   141  		if matchesHost(bt.registry.RegistryStr(), in, bt.scheme) {
   142  			hdr := fmt.Sprintf("Bearer %s", bt.bearer.RegistryToken)
   143  			in.Header.Set("Authorization", hdr)
   144  		}
   145  		return bt.inner.RoundTrip(in)
   146  	}
   147  
   148  	res, err := sendRequest()
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  
   153  	// If we hit a WWW-Authenticate challenge, it might be due to expired tokens or insufficient scope.
   154  	if challenges := authchallenge.ResponseChallenges(res); len(challenges) != 0 {
   155  		// close out old response, since we will not return it.
   156  		res.Body.Close()
   157  
   158  		newScopes := []string{}
   159  		for _, wac := range challenges {
   160  			// TODO(jonjohnsonjr): Should we also update "realm" or "service"?
   161  			if want, ok := wac.Parameters["scope"]; ok {
   162  				// Add any scopes that we don't already request.
   163  				got := stringSet(bt.scopes)
   164  				if _, ok := got[want]; !ok {
   165  					newScopes = append(newScopes, want)
   166  				}
   167  			}
   168  		}
   169  
   170  		// Some registries seem to only look at the first scope parameter during a token exchange.
   171  		// If a request fails because it's missing a scope, we should put those at the beginning,
   172  		// otherwise the registry might just ignore it :/
   173  		newScopes = append(newScopes, bt.scopes...)
   174  		bt.scopes = newScopes
   175  
   176  		// TODO(jonjohnsonjr): Teach transport.Error about "error" and "error_description" from challenge.
   177  
   178  		// Retry the request to attempt to get a valid token.
   179  		if err = bt.refresh(in.Context()); err != nil {
   180  			return nil, err
   181  		}
   182  		return sendRequest()
   183  	}
   184  
   185  	return res, err
   186  }
   187  
   188  // It's unclear which authentication flow to use based purely on the protocol,
   189  // so we rely on heuristics and fallbacks to support as many registries as possible.
   190  // The basic token exchange is attempted first, falling back to the oauth flow.
   191  // If the IdentityToken is set, this indicates that we should start with the oauth flow.
   192  func (bt *bearerTransport) refresh(ctx context.Context) error {
   193  	auth, err := bt.basic.Authorization()
   194  	if err != nil {
   195  		return err
   196  	}
   197  
   198  	if auth.RegistryToken != "" {
   199  		bt.bearer.RegistryToken = auth.RegistryToken
   200  		return nil
   201  	}
   202  
   203  	response, err := bt.Refresh(ctx, auth)
   204  	if err != nil {
   205  		return err
   206  	}
   207  
   208  	// Some registries set access_token instead of token. See #54.
   209  	if response.AccessToken != "" {
   210  		response.Token = response.AccessToken
   211  	}
   212  
   213  	// Find a token to turn into a Bearer authenticator
   214  	if response.Token != "" {
   215  		bt.bearer.RegistryToken = response.Token
   216  	}
   217  
   218  	// If we obtained a refresh token from the oauth flow, use that for refresh() now.
   219  	if response.RefreshToken != "" {
   220  		bt.basic = authn.FromConfig(authn.AuthConfig{
   221  			IdentityToken: response.RefreshToken,
   222  		})
   223  	}
   224  
   225  	return nil
   226  }
   227  
   228  func (bt *bearerTransport) Refresh(ctx context.Context, auth *authn.AuthConfig) (*Token, error) {
   229  	var (
   230  		content []byte
   231  		err     error
   232  	)
   233  	if auth.IdentityToken != "" {
   234  		// If the secret being stored is an identity token,
   235  		// the Username should be set to <token>, which indicates
   236  		// we are using an oauth flow.
   237  		content, err = bt.refreshOauth(ctx)
   238  		var terr *Error
   239  		if errors.As(err, &terr) && terr.StatusCode == http.StatusNotFound {
   240  			// Note: Not all token servers implement oauth2.
   241  			// If the request to the endpoint returns 404 using the HTTP POST method,
   242  			// refer to Token Documentation for using the HTTP GET method supported by all token servers.
   243  			content, err = bt.refreshBasic(ctx)
   244  		}
   245  	} else {
   246  		content, err = bt.refreshBasic(ctx)
   247  	}
   248  	if err != nil {
   249  		return nil, err
   250  	}
   251  
   252  	var response Token
   253  	if err := json.Unmarshal(content, &response); err != nil {
   254  		return nil, err
   255  	}
   256  
   257  	if response.Token == "" && response.AccessToken == "" {
   258  		return &response, fmt.Errorf("no token in bearer response:\n%s", content)
   259  	}
   260  
   261  	return &response, nil
   262  }
   263  
   264  func matchesHost(host string, in *http.Request, scheme string) bool {
   265  	canonicalHeaderHost := canonicalAddress(in.Host, scheme)
   266  	canonicalURLHost := canonicalAddress(in.URL.Host, scheme)
   267  	canonicalRegistryHost := canonicalAddress(host, scheme)
   268  	return canonicalHeaderHost == canonicalRegistryHost || canonicalURLHost == canonicalRegistryHost
   269  }
   270  
   271  func canonicalAddress(host, scheme string) (address string) {
   272  	// The host may be any one of:
   273  	// - hostname
   274  	// - hostname:port
   275  	// - ipv4
   276  	// - ipv4:port
   277  	// - ipv6
   278  	// - [ipv6]:port
   279  	// As net.SplitHostPort returns an error if the host does not contain a port, we should only attempt
   280  	// to call it when we know that the address contains a port
   281  	if strings.Count(host, ":") == 1 || (strings.Count(host, ":") >= 2 && strings.Contains(host, "]:")) {
   282  		hostname, port, err := net.SplitHostPort(host)
   283  		if err != nil {
   284  			return host
   285  		}
   286  		if port == "" {
   287  			port = portMap[scheme]
   288  		}
   289  
   290  		return net.JoinHostPort(hostname, port)
   291  	}
   292  
   293  	return net.JoinHostPort(host, portMap[scheme])
   294  }
   295  
   296  // https://docs.docker.com/registry/spec/auth/oauth/
   297  func (bt *bearerTransport) refreshOauth(ctx context.Context) ([]byte, error) {
   298  	auth, err := bt.basic.Authorization()
   299  	if err != nil {
   300  		return nil, err
   301  	}
   302  
   303  	u, err := url.Parse(bt.realm)
   304  	if err != nil {
   305  		return nil, err
   306  	}
   307  
   308  	v := url.Values{}
   309  	v.Set("scope", strings.Join(bt.scopes, " "))
   310  	if bt.service != "" {
   311  		v.Set("service", bt.service)
   312  	}
   313  	v.Set("client_id", defaultUserAgent)
   314  	if auth.IdentityToken != "" {
   315  		v.Set("grant_type", "refresh_token")
   316  		v.Set("refresh_token", auth.IdentityToken)
   317  	} else if auth.Username != "" && auth.Password != "" {
   318  		// TODO(#629): This is unreachable.
   319  		v.Set("grant_type", "password")
   320  		v.Set("username", auth.Username)
   321  		v.Set("password", auth.Password)
   322  		v.Set("access_type", "offline")
   323  	}
   324  
   325  	client := http.Client{Transport: bt.inner}
   326  	req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(v.Encode()))
   327  	if err != nil {
   328  		return nil, err
   329  	}
   330  	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
   331  
   332  	// We don't want to log credentials.
   333  	ctx = redact.NewContext(ctx, "oauth token response contains credentials")
   334  
   335  	resp, err := client.Do(req.WithContext(ctx))
   336  	if err != nil {
   337  		return nil, err
   338  	}
   339  	defer resp.Body.Close()
   340  
   341  	if err := CheckError(resp, http.StatusOK); err != nil {
   342  		if bt.basic == authn.Anonymous {
   343  			logs.Warn.Printf("No matching credentials were found for %q", bt.registry)
   344  		}
   345  		return nil, err
   346  	}
   347  
   348  	return io.ReadAll(resp.Body)
   349  }
   350  
   351  // https://docs.docker.com/registry/spec/auth/token/
   352  func (bt *bearerTransport) refreshBasic(ctx context.Context) ([]byte, error) {
   353  	u, err := url.Parse(bt.realm)
   354  	if err != nil {
   355  		return nil, err
   356  	}
   357  	b := &basicTransport{
   358  		inner:  bt.inner,
   359  		auth:   bt.basic,
   360  		target: u.Host,
   361  	}
   362  	client := http.Client{Transport: b}
   363  
   364  	v := u.Query()
   365  	v["scope"] = bt.scopes
   366  	v.Set("service", bt.service)
   367  	u.RawQuery = v.Encode()
   368  
   369  	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
   370  	if err != nil {
   371  		return nil, err
   372  	}
   373  
   374  	// We don't want to log credentials.
   375  	ctx = redact.NewContext(ctx, "basic token response contains credentials")
   376  
   377  	resp, err := client.Do(req.WithContext(ctx))
   378  	if err != nil {
   379  		return nil, err
   380  	}
   381  	defer resp.Body.Close()
   382  
   383  	if err := CheckError(resp, http.StatusOK); err != nil {
   384  		if bt.basic == authn.Anonymous {
   385  			logs.Warn.Printf("No matching credentials were found for %q", bt.registry)
   386  		}
   387  		return nil, err
   388  	}
   389  
   390  	return io.ReadAll(resp.Body)
   391  }
   392  

View as plain text