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 <derek@mcgstyle.net> (github: dmcgowan)
This commit is contained in:
Derek McGowan 2015-05-11 11:31:22 -07:00
parent 7d630192dd
commit ecaa643cb2
5 changed files with 143 additions and 126 deletions

View file

@ -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

View file

@ -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)
}
}

View file

@ -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

View file

@ -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
}

View file

@ -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")
}