github.com/mika/distribution@v2.2.2-0.20160108133430-a75790e3d8e0+incompatible/registry/client/auth/session.go (about)

     1  package auth
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"net/url"
     9  	"strings"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/Sirupsen/logrus"
    14  	"github.com/docker/distribution/registry/client"
    15  	"github.com/docker/distribution/registry/client/transport"
    16  )
    17  
    18  // AuthenticationHandler is an interface for authorizing a request from
    19  // params from a "WWW-Authenicate" header for a single scheme.
    20  type AuthenticationHandler interface {
    21  	// Scheme returns the scheme as expected from the "WWW-Authenicate" header.
    22  	Scheme() string
    23  
    24  	// AuthorizeRequest adds the authorization header to a request (if needed)
    25  	// using the parameters from "WWW-Authenticate" method. The parameters
    26  	// values depend on the scheme.
    27  	AuthorizeRequest(req *http.Request, params map[string]string) error
    28  }
    29  
    30  // CredentialStore is an interface for getting credentials for
    31  // a given URL
    32  type CredentialStore interface {
    33  	// Basic returns basic auth for the given URL
    34  	Basic(*url.URL) (string, string)
    35  }
    36  
    37  // NewAuthorizer creates an authorizer which can handle multiple authentication
    38  // schemes. The handlers are tried in order, the higher priority authentication
    39  // methods should be first. The challengeMap holds a list of challenges for
    40  // a given root API endpoint (for example "https://registry-1.docker.io/v2/").
    41  func NewAuthorizer(manager ChallengeManager, handlers ...AuthenticationHandler) transport.RequestModifier {
    42  	return &endpointAuthorizer{
    43  		challenges: manager,
    44  		handlers:   handlers,
    45  	}
    46  }
    47  
    48  type endpointAuthorizer struct {
    49  	challenges ChallengeManager
    50  	handlers   []AuthenticationHandler
    51  	transport  http.RoundTripper
    52  }
    53  
    54  func (ea *endpointAuthorizer) ModifyRequest(req *http.Request) error {
    55  	v2Root := strings.Index(req.URL.Path, "/v2/")
    56  	if v2Root == -1 {
    57  		return nil
    58  	}
    59  
    60  	ping := url.URL{
    61  		Host:   req.URL.Host,
    62  		Scheme: req.URL.Scheme,
    63  		Path:   req.URL.Path[:v2Root+4],
    64  	}
    65  
    66  	pingEndpoint := ping.String()
    67  
    68  	challenges, err := ea.challenges.GetChallenges(pingEndpoint)
    69  	if err != nil {
    70  		return err
    71  	}
    72  
    73  	if len(challenges) > 0 {
    74  		for _, handler := range ea.handlers {
    75  			for _, challenge := range challenges {
    76  				if challenge.Scheme != handler.Scheme() {
    77  					continue
    78  				}
    79  				if err := handler.AuthorizeRequest(req, challenge.Parameters); err != nil {
    80  					return err
    81  				}
    82  			}
    83  		}
    84  	}
    85  
    86  	return nil
    87  }
    88  
    89  // This is the minimum duration a token can last (in seconds).
    90  // A token must not live less than 60 seconds because older versions
    91  // of the Docker client didn't read their expiration from the token
    92  // response and assumed 60 seconds.  So to remain compatible with
    93  // those implementations, a token must live at least this long.
    94  const minimumTokenLifetimeSeconds = 60
    95  
    96  // Private interface for time used by this package to enable tests to provide their own implementation.
    97  type clock interface {
    98  	Now() time.Time
    99  }
   100  
   101  type tokenHandler struct {
   102  	header    http.Header
   103  	creds     CredentialStore
   104  	scope     tokenScope
   105  	transport http.RoundTripper
   106  	clock     clock
   107  
   108  	tokenLock       sync.Mutex
   109  	tokenCache      string
   110  	tokenExpiration time.Time
   111  }
   112  
   113  // tokenScope represents the scope at which a token will be requested.
   114  // This represents a specific action on a registry resource.
   115  type tokenScope struct {
   116  	Resource string
   117  	Scope    string
   118  	Actions  []string
   119  }
   120  
   121  func (ts tokenScope) String() string {
   122  	return fmt.Sprintf("%s:%s:%s", ts.Resource, ts.Scope, strings.Join(ts.Actions, ","))
   123  }
   124  
   125  // An implementation of clock for providing real time data.
   126  type realClock struct{}
   127  
   128  // Now implements clock
   129  func (realClock) Now() time.Time { return time.Now() }
   130  
   131  // NewTokenHandler creates a new AuthenicationHandler which supports
   132  // fetching tokens from a remote token server.
   133  func NewTokenHandler(transport http.RoundTripper, creds CredentialStore, scope string, actions ...string) AuthenticationHandler {
   134  	return newTokenHandler(transport, creds, realClock{}, scope, actions...)
   135  }
   136  
   137  // newTokenHandler exposes the option to provide a clock to manipulate time in unit testing.
   138  func newTokenHandler(transport http.RoundTripper, creds CredentialStore, c clock, scope string, actions ...string) AuthenticationHandler {
   139  	return &tokenHandler{
   140  		transport: transport,
   141  		creds:     creds,
   142  		clock:     c,
   143  		scope: tokenScope{
   144  			Resource: "repository",
   145  			Scope:    scope,
   146  			Actions:  actions,
   147  		},
   148  	}
   149  }
   150  
   151  func (th *tokenHandler) client() *http.Client {
   152  	return &http.Client{
   153  		Transport: th.transport,
   154  		Timeout:   15 * time.Second,
   155  	}
   156  }
   157  
   158  func (th *tokenHandler) Scheme() string {
   159  	return "bearer"
   160  }
   161  
   162  func (th *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
   163  	if err := th.refreshToken(params); err != nil {
   164  		return err
   165  	}
   166  
   167  	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", th.tokenCache))
   168  
   169  	return nil
   170  }
   171  
   172  func (th *tokenHandler) refreshToken(params map[string]string) error {
   173  	th.tokenLock.Lock()
   174  	defer th.tokenLock.Unlock()
   175  	now := th.clock.Now()
   176  	if now.After(th.tokenExpiration) {
   177  		tr, err := th.fetchToken(params)
   178  		if err != nil {
   179  			return err
   180  		}
   181  		th.tokenCache = tr.Token
   182  		th.tokenExpiration = tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second)
   183  	}
   184  
   185  	return nil
   186  }
   187  
   188  type tokenResponse struct {
   189  	Token       string    `json:"token"`
   190  	AccessToken string    `json:"access_token"`
   191  	ExpiresIn   int       `json:"expires_in"`
   192  	IssuedAt    time.Time `json:"issued_at"`
   193  }
   194  
   195  func (th *tokenHandler) fetchToken(params map[string]string) (token *tokenResponse, err error) {
   196  	//log.Debugf("Getting bearer token with %s for %s", challenge.Parameters, ta.auth.Username)
   197  	realm, ok := params["realm"]
   198  	if !ok {
   199  		return nil, errors.New("no realm specified for token auth challenge")
   200  	}
   201  
   202  	// TODO(dmcgowan): Handle empty scheme
   203  
   204  	realmURL, err := url.Parse(realm)
   205  	if err != nil {
   206  		return nil, fmt.Errorf("invalid token auth challenge realm: %s", err)
   207  	}
   208  
   209  	req, err := http.NewRequest("GET", realmURL.String(), nil)
   210  	if err != nil {
   211  		return nil, err
   212  	}
   213  
   214  	reqParams := req.URL.Query()
   215  	service := params["service"]
   216  	scope := th.scope.String()
   217  
   218  	if service != "" {
   219  		reqParams.Add("service", service)
   220  	}
   221  
   222  	for _, scopeField := range strings.Fields(scope) {
   223  		reqParams.Add("scope", scopeField)
   224  	}
   225  
   226  	if th.creds != nil {
   227  		username, password := th.creds.Basic(realmURL)
   228  		if username != "" && password != "" {
   229  			reqParams.Add("account", username)
   230  			req.SetBasicAuth(username, password)
   231  		}
   232  	}
   233  
   234  	req.URL.RawQuery = reqParams.Encode()
   235  
   236  	resp, err := th.client().Do(req)
   237  	if err != nil {
   238  		return nil, err
   239  	}
   240  	defer resp.Body.Close()
   241  
   242  	if !client.SuccessStatus(resp.StatusCode) {
   243  		err := client.HandleErrorResponse(resp)
   244  		return nil, err
   245  	}
   246  
   247  	decoder := json.NewDecoder(resp.Body)
   248  
   249  	tr := new(tokenResponse)
   250  	if err = decoder.Decode(tr); err != nil {
   251  		return nil, fmt.Errorf("unable to decode token response: %s", err)
   252  	}
   253  
   254  	// `access_token` is equivalent to `token` and if both are specified
   255  	// the choice is undefined.  Canonicalize `access_token` by sticking
   256  	// things in `token`.
   257  	if tr.AccessToken != "" {
   258  		tr.Token = tr.AccessToken
   259  	}
   260  
   261  	if tr.Token == "" {
   262  		return nil, errors.New("authorization server did not include a token in the response")
   263  	}
   264  
   265  	if tr.ExpiresIn < minimumTokenLifetimeSeconds {
   266  		logrus.Debugf("Increasing token expiration to: %d seconds", tr.ExpiresIn)
   267  		// The default/minimum lifetime.
   268  		tr.ExpiresIn = minimumTokenLifetimeSeconds
   269  	}
   270  
   271  	if tr.IssuedAt.IsZero() {
   272  		// issued_at is optional in the token response.
   273  		tr.IssuedAt = th.clock.Now()
   274  	}
   275  
   276  	return tr, nil
   277  }
   278  
   279  type basicHandler struct {
   280  	creds CredentialStore
   281  }
   282  
   283  // NewBasicHandler creaters a new authentiation handler which adds
   284  // basic authentication credentials to a request.
   285  func NewBasicHandler(creds CredentialStore) AuthenticationHandler {
   286  	return &basicHandler{
   287  		creds: creds,
   288  	}
   289  }
   290  
   291  func (*basicHandler) Scheme() string {
   292  	return "basic"
   293  }
   294  
   295  func (bh *basicHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
   296  	if bh.creds != nil {
   297  		username, password := bh.creds.Basic(req.URL)
   298  		if username != "" && password != "" {
   299  			req.SetBasicAuth(username, password)
   300  			return nil
   301  		}
   302  	}
   303  	return errors.New("no basic auth credentials")
   304  }