From ecaa643cb24288523eb2108f00c54fb7db7cfe7e Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Mon, 11 May 2015 11:31:22 -0700 Subject: [PATCH] Create authentication handler Refactory authorizer to take a set of authentication handlers for different authentication schemes returned by an unauthorized HTTP requst. Signed-off-by: Derek McGowan (github: dmcgowan) --- docs/client/authchallenge.go | 6 +- docs/client/authchallenge_test.go | 21 +-- docs/client/errors.go | 35 ----- docs/client/layer.go | 2 +- docs/client/session.go | 205 +++++++++++++++++++----------- 5 files changed, 143 insertions(+), 126 deletions(-) diff --git a/docs/client/authchallenge.go b/docs/client/authchallenge.go index a9cce3cce..49cf270e5 100644 --- a/docs/client/authchallenge.go +++ b/docs/client/authchallenge.go @@ -54,12 +54,12 @@ func init() { } } -func parseAuthHeader(header http.Header) []authorizationChallenge { - var challenges []authorizationChallenge +func parseAuthHeader(header http.Header) map[string]authorizationChallenge { + challenges := map[string]authorizationChallenge{} for _, h := range header[http.CanonicalHeaderKey("WWW-Authenticate")] { v, p := parseValueAndParams(h) if v != "" { - challenges = append(challenges, authorizationChallenge{Scheme: v, Parameters: p}) + challenges[v] = authorizationChallenge{Scheme: v, Parameters: p} } } return challenges diff --git a/docs/client/authchallenge_test.go b/docs/client/authchallenge_test.go index bb3016ee3..802c94f30 100644 --- a/docs/client/authchallenge_test.go +++ b/docs/client/authchallenge_test.go @@ -13,25 +13,26 @@ func TestAuthChallengeParse(t *testing.T) { if len(challenges) != 1 { t.Fatalf("Unexpected number of auth challenges: %d, expected 1", len(challenges)) } + challenge := challenges["bearer"] - if expected := "bearer"; challenges[0].Scheme != expected { - t.Fatalf("Unexpected scheme: %s, expected: %s", challenges[0].Scheme, expected) + if expected := "bearer"; challenge.Scheme != expected { + t.Fatalf("Unexpected scheme: %s, expected: %s", challenge.Scheme, expected) } - if expected := "https://auth.example.com/token"; challenges[0].Parameters["realm"] != expected { - t.Fatalf("Unexpected param: %s, expected: %s", challenges[0].Parameters["realm"], expected) + if expected := "https://auth.example.com/token"; challenge.Parameters["realm"] != expected { + t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["realm"], expected) } - if expected := "registry.example.com"; challenges[0].Parameters["service"] != expected { - t.Fatalf("Unexpected param: %s, expected: %s", challenges[0].Parameters["service"], expected) + if expected := "registry.example.com"; challenge.Parameters["service"] != expected { + t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["service"], expected) } - if expected := "fun"; challenges[0].Parameters["other"] != expected { - t.Fatalf("Unexpected param: %s, expected: %s", challenges[0].Parameters["other"], expected) + if expected := "fun"; challenge.Parameters["other"] != expected { + t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["other"], expected) } - if expected := "he\"llo"; challenges[0].Parameters["slashed"] != expected { - t.Fatalf("Unexpected param: %s, expected: %s", challenges[0].Parameters["slashed"], expected) + if expected := "he\"llo"; challenge.Parameters["slashed"] != expected { + t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["slashed"], expected) } } diff --git a/docs/client/errors.go b/docs/client/errors.go index adb909d13..2bb64a449 100644 --- a/docs/client/errors.go +++ b/docs/client/errors.go @@ -6,44 +6,9 @@ import ( "io/ioutil" "net/http" - "github.com/docker/distribution/digest" "github.com/docker/distribution/registry/api/v2" ) -// RepositoryNotFoundError is returned when making an operation against a -// repository that does not exist in the registry. -type RepositoryNotFoundError struct { - Name string -} - -func (e *RepositoryNotFoundError) Error() string { - return fmt.Sprintf("No repository found with Name: %s", e.Name) -} - -// ImageManifestNotFoundError is returned when making an operation against a -// given image manifest that does not exist in the registry. -type ImageManifestNotFoundError struct { - Name string - Tag string -} - -func (e *ImageManifestNotFoundError) Error() string { - return fmt.Sprintf("No manifest found with Name: %s, Tag: %s", - e.Name, e.Tag) -} - -// BlobNotFoundError is returned when making an operation against a given image -// layer that does not exist in the registry. -type BlobNotFoundError struct { - Name string - Digest digest.Digest -} - -func (e *BlobNotFoundError) Error() string { - return fmt.Sprintf("No blob found with Name: %s, Digest: %s", - e.Name, e.Digest) -} - // BlobUploadNotFoundError is returned when making a blob upload operation against an // invalid blob upload location url. // This may be the result of using a cancelled, completed, or stale upload diff --git a/docs/client/layer.go b/docs/client/layer.go index f61a9034e..b6e1697d1 100644 --- a/docs/client/layer.go +++ b/docs/client/layer.go @@ -48,7 +48,7 @@ func (hl *httpLayer) Read(p []byte) (n int, err error) { n, err = rd.Read(p) hl.offset += int64(n) - // Simulate io.EOR error if we reach filesize. + // Simulate io.EOF error if we reach filesize. if err == nil && hl.offset >= hl.size { err = io.EOF } diff --git a/docs/client/session.go b/docs/client/session.go index bd8abe0f7..97e932ff9 100644 --- a/docs/client/session.go +++ b/docs/client/session.go @@ -17,6 +17,13 @@ type Authorizer interface { Authorize(req *http.Request) error } +// AuthenticationHandler is an interface for authorizing a request from +// params from a "WWW-Authenicate" header for a single scheme. +type AuthenticationHandler interface { + Scheme() string + AuthorizeRequest(req *http.Request, params map[string]string) error +} + // CredentialStore is an interface for getting credentials for // a given URL type CredentialStore interface { @@ -48,18 +55,6 @@ func (rc *RepositoryConfig) HTTPClient() (*http.Client, error) { return client, nil } -// TokenScope represents the scope at which a token will be requested. -// This represents a specific action on a registry resource. -type TokenScope struct { - Resource string - Scope string - Actions []string -} - -func (ts TokenScope) String() string { - return fmt.Sprintf("%s:%s:%s", ts.Resource, ts.Scope, strings.Join(ts.Actions, ",")) -} - // NewTokenAuthorizer returns an authorizer which is capable of getting a token // from a token server. The expected authorization method will be discovered // by the authorizer, getting the token server endpoint from the URL being @@ -69,24 +64,37 @@ func (ts TokenScope) String() string { func NewTokenAuthorizer(creds CredentialStore, header http.Header, scope TokenScope) Authorizer { return &tokenAuthorizer{ header: header, - creds: creds, - scope: scope, - challenges: map[string][]authorizationChallenge{}, + challenges: map[string]map[string]authorizationChallenge{}, + handlers: []AuthenticationHandler{ + NewTokenHandler(creds, scope, header), + NewBasicHandler(creds), + }, + } +} + +// NewAuthorizer creates an authorizer which can handle multiple authentication +// schemes. The handlers are tried in order, the higher priority authentication +// methods should be first. +func NewAuthorizer(header http.Header, handlers ...AuthenticationHandler) Authorizer { + return &tokenAuthorizer{ + header: header, + challenges: map[string]map[string]authorizationChallenge{}, + handlers: handlers, } } type tokenAuthorizer struct { header http.Header - challenges map[string][]authorizationChallenge - creds CredentialStore - scope TokenScope - - tokenLock sync.Mutex - tokenCache string - tokenExpiration time.Time + challenges map[string]map[string]authorizationChallenge + handlers []AuthenticationHandler } -func (ta *tokenAuthorizer) ping(endpoint string) ([]authorizationChallenge, error) { +func (ta *tokenAuthorizer) client() *http.Client { + // TODO(dmcgowan): Use same transport which has properly configured TLS + return &http.Client{Transport: &Transport{ExtraHeader: ta.header}} +} + +func (ta *tokenAuthorizer) ping(endpoint string) (map[string]authorizationChallenge, error) { req, err := http.NewRequest("GET", endpoint, nil) if err != nil { return nil, err @@ -98,6 +106,7 @@ func (ta *tokenAuthorizer) ping(endpoint string) ([]authorizationChallenge, erro } defer resp.Body.Close() + // TODO(dmcgowan): Add version string which would allow skipping this section var supportsV2 bool HeaderLoop: for _, supportedVersions := range resp.Header[http.CanonicalHeaderKey("Docker-Distribution-API-Version")] { @@ -148,59 +157,80 @@ func (ta *tokenAuthorizer) Authorize(req *http.Request) error { ta.challenges[pingEndpoint] = challenges } - return ta.setAuth(challenges, req) -} - -func (ta *tokenAuthorizer) client() *http.Client { - // TODO(dmcgowan): Use same transport which has properly configured TLS - return &http.Client{Transport: &Transport{ExtraHeader: ta.header}} -} - -func (ta *tokenAuthorizer) setAuth(challenges []authorizationChallenge, req *http.Request) error { - var useBasic bool - for _, challenge := range challenges { - switch strings.ToLower(challenge.Scheme) { - case "basic": - useBasic = true - case "bearer": - if err := ta.refreshToken(challenge); err != nil { + for _, handler := range ta.handlers { + challenge, ok := challenges[handler.Scheme()] + if ok { + if err := handler.AuthorizeRequest(req, challenge.Parameters); err != nil { return err } - - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ta.tokenCache)) - - return nil - default: - //log.Infof("Unsupported auth scheme: %q", challenge.Scheme) } } - // Only use basic when no token auth challenges found - if useBasic { - if ta.creds != nil { - username, password := ta.creds.Basic(req.URL) - if username != "" && password != "" { - req.SetBasicAuth(username, password) - return nil - } - } - return errors.New("no basic auth credentials") - } - return nil } -func (ta *tokenAuthorizer) refreshToken(challenge authorizationChallenge) error { - ta.tokenLock.Lock() - defer ta.tokenLock.Unlock() +type tokenHandler struct { + header http.Header + creds CredentialStore + scope TokenScope + + tokenLock sync.Mutex + tokenCache string + tokenExpiration time.Time +} + +// TokenScope represents the scope at which a token will be requested. +// This represents a specific action on a registry resource. +type TokenScope struct { + Resource string + Scope string + Actions []string +} + +// NewTokenHandler creates a new AuthenicationHandler which supports +// fetching tokens from a remote token server. +func NewTokenHandler(creds CredentialStore, scope TokenScope, header http.Header) AuthenticationHandler { + return &tokenHandler{ + header: header, + creds: creds, + scope: scope, + } +} + +func (ts TokenScope) String() string { + return fmt.Sprintf("%s:%s:%s", ts.Resource, ts.Scope, strings.Join(ts.Actions, ",")) +} + +func (ts *tokenHandler) client() *http.Client { + // TODO(dmcgowan): Use same transport which has properly configured TLS + return &http.Client{Transport: &Transport{ExtraHeader: ts.header}} +} + +func (ts *tokenHandler) Scheme() string { + return "bearer" +} + +func (ts *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error { + if err := ts.refreshToken(params); err != nil { + return err + } + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.tokenCache)) + + return nil +} + +func (ts *tokenHandler) refreshToken(params map[string]string) error { + ts.tokenLock.Lock() + defer ts.tokenLock.Unlock() now := time.Now() - if now.After(ta.tokenExpiration) { - token, err := ta.fetchToken(challenge) + if now.After(ts.tokenExpiration) { + token, err := ts.fetchToken(params) if err != nil { return err } - ta.tokenCache = token - ta.tokenExpiration = now.Add(time.Minute) + ts.tokenCache = token + ts.tokenExpiration = now.Add(time.Minute) } return nil @@ -210,26 +240,20 @@ type tokenResponse struct { Token string `json:"token"` } -func (ta *tokenAuthorizer) fetchToken(challenge authorizationChallenge) (token string, err error) { +func (ts *tokenHandler) fetchToken(params map[string]string) (token string, err error) { //log.Debugf("Getting bearer token with %s for %s", challenge.Parameters, ta.auth.Username) - params := map[string]string{} - for k, v := range challenge.Parameters { - params[k] = v - } - params["scope"] = ta.scope.String() - realm, ok := params["realm"] if !ok { return "", errors.New("no realm specified for token auth challenge") } + // TODO(dmcgowan): Handle empty scheme + realmURL, err := url.Parse(realm) if err != nil { return "", fmt.Errorf("invalid token auth challenge realm: %s", err) } - // TODO(dmcgowan): Handle empty scheme - req, err := http.NewRequest("GET", realmURL.String(), nil) if err != nil { return "", err @@ -237,7 +261,7 @@ func (ta *tokenAuthorizer) fetchToken(challenge authorizationChallenge) (token s reqParams := req.URL.Query() service := params["service"] - scope := params["scope"] + scope := ts.scope.String() if service != "" { reqParams.Add("service", service) @@ -247,8 +271,8 @@ func (ta *tokenAuthorizer) fetchToken(challenge authorizationChallenge) (token s reqParams.Add("scope", scopeField) } - if ta.creds != nil { - username, password := ta.creds.Basic(realmURL) + if ts.creds != nil { + username, password := ts.creds.Basic(realmURL) if username != "" && password != "" { reqParams.Add("account", username) req.SetBasicAuth(username, password) @@ -257,7 +281,7 @@ func (ta *tokenAuthorizer) fetchToken(challenge authorizationChallenge) (token s req.URL.RawQuery = reqParams.Encode() - resp, err := ta.client().Do(req) + resp, err := ts.client().Do(req) if err != nil { return "", err } @@ -280,3 +304,30 @@ func (ta *tokenAuthorizer) fetchToken(challenge authorizationChallenge) (token s return tr.Token, nil } + +type basicHandler struct { + creds CredentialStore +} + +// NewBasicHandler creaters a new authentiation handler which adds +// basic authentication credentials to a request. +func NewBasicHandler(creds CredentialStore) AuthenticationHandler { + return &basicHandler{ + creds: creds, + } +} + +func (*basicHandler) Scheme() string { + return "basic" +} + +func (bh *basicHandler) AuthorizeRequest(req *http.Request, params map[string]string) error { + if bh.creds != nil { + username, password := bh.creds.Basic(req.URL) + if username != "" && password != "" { + req.SetBasicAuth(username, password) + return nil + } + } + return errors.New("no basic auth credentials") +}