Merge pull request #1465 from dmcgowan/token-server-oauth
Integration token server supporting oauthmaster
						commit
						fb106e167a
					
				|  | @ -0,0 +1,38 @@ | ||||||
|  | package main | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  | 
 | ||||||
|  | 	"github.com/docker/distribution/registry/api/errcode" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	errGroup = "tokenserver" | ||||||
|  | 
 | ||||||
|  | 	// ErrorBadTokenOption is returned when a token parameter is invalid
 | ||||||
|  | 	ErrorBadTokenOption = errcode.Register(errGroup, errcode.ErrorDescriptor{ | ||||||
|  | 		Value:   "BAD_TOKEN_OPTION", | ||||||
|  | 		Message: "bad token option", | ||||||
|  | 		Description: `This error may be returned when a request for a | ||||||
|  | 		token contains an option which is not valid`, | ||||||
|  | 		HTTPStatusCode: http.StatusBadRequest, | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	// ErrorMissingRequiredField is returned when a required form field is missing
 | ||||||
|  | 	ErrorMissingRequiredField = errcode.Register(errGroup, errcode.ErrorDescriptor{ | ||||||
|  | 		Value:   "MISSING_REQUIRED_FIELD", | ||||||
|  | 		Message: "missing required field", | ||||||
|  | 		Description: `This error may be returned when a request for a | ||||||
|  | 		token does not contain a required form field`, | ||||||
|  | 		HTTPStatusCode: http.StatusBadRequest, | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	// ErrorUnsupportedValue is returned when a form field has an unsupported value
 | ||||||
|  | 	ErrorUnsupportedValue = errcode.Register(errGroup, errcode.ErrorDescriptor{ | ||||||
|  | 		Value:   "UNSUPPORTED_VALUE", | ||||||
|  | 		Message: "unsupported value", | ||||||
|  | 		Description: `This error may be returned when a request for a | ||||||
|  | 		token contains a form field with an unsupported value`, | ||||||
|  | 		HTTPStatusCode: http.StatusBadRequest, | ||||||
|  | 	}) | ||||||
|  | ) | ||||||
|  | @ -3,8 +3,11 @@ package main | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"flag" | 	"flag" | ||||||
|  | 	"math/rand" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/Sirupsen/logrus" | 	"github.com/Sirupsen/logrus" | ||||||
| 	"github.com/docker/distribution/context" | 	"github.com/docker/distribution/context" | ||||||
|  | @ -73,15 +76,20 @@ func main() { | ||||||
| 		logrus.Fatalf("Error initializing access controller: %v", err) | 		logrus.Fatalf("Error initializing access controller: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// TODO: Make configurable
 | ||||||
|  | 	issuer.Expiration = 15 * time.Minute | ||||||
|  | 
 | ||||||
| 	ctx := context.Background() | 	ctx := context.Background() | ||||||
| 
 | 
 | ||||||
| 	ts := &tokenServer{ | 	ts := &tokenServer{ | ||||||
| 		issuer:           issuer, | 		issuer:           issuer, | ||||||
| 		accessController: ac, | 		accessController: ac, | ||||||
|  | 		refreshCache:     map[string]refreshToken{}, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	router := mux.NewRouter() | 	router := mux.NewRouter() | ||||||
| 	router.Path("/token/").Methods("GET").Handler(handlerWithContext(ctx, ts.getToken)) | 	router.Path("/token/").Methods("GET").Handler(handlerWithContext(ctx, ts.getToken)) | ||||||
|  | 	router.Path("/token/").Methods("POST").Handler(handlerWithContext(ctx, ts.postToken)) | ||||||
| 
 | 
 | ||||||
| 	if cert == "" { | 	if cert == "" { | ||||||
| 		err = http.ListenAndServe(addr, router) | 		err = http.ListenAndServe(addr, router) | ||||||
|  | @ -120,9 +128,52 @@ func handleError(ctx context.Context, err error, w http.ResponseWriter) { | ||||||
| 	context.GetResponseLogger(ctx).Info("application error") | 	context.GetResponseLogger(ctx).Info("application error") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | var refreshCharacters = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") | ||||||
|  | 
 | ||||||
|  | const refreshTokenLength = 15 | ||||||
|  | 
 | ||||||
|  | func newRefreshToken() string { | ||||||
|  | 	s := make([]rune, refreshTokenLength) | ||||||
|  | 	for i := range s { | ||||||
|  | 		s[i] = refreshCharacters[rand.Intn(len(refreshCharacters))] | ||||||
|  | 	} | ||||||
|  | 	return string(s) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type refreshToken struct { | ||||||
|  | 	subject string | ||||||
|  | 	service string | ||||||
|  | } | ||||||
|  | 
 | ||||||
| type tokenServer struct { | type tokenServer struct { | ||||||
| 	issuer           *TokenIssuer | 	issuer           *TokenIssuer | ||||||
| 	accessController auth.AccessController | 	accessController auth.AccessController | ||||||
|  | 	refreshCache     map[string]refreshToken | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type tokenResponse struct { | ||||||
|  | 	Token        string `json:"access_token"` | ||||||
|  | 	RefreshToken string `json:"refresh_token,omitempty"` | ||||||
|  | 	ExpiresIn    int    `json:"expires_in,omitempty"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func filterAccessList(ctx context.Context, scope string, requestedAccessList []auth.Access) []auth.Access { | ||||||
|  | 	if !strings.HasSuffix(scope, "/") { | ||||||
|  | 		scope = scope + "/" | ||||||
|  | 	} | ||||||
|  | 	grantedAccessList := make([]auth.Access, 0, len(requestedAccessList)) | ||||||
|  | 	for _, access := range requestedAccessList { | ||||||
|  | 		if access.Type != "repository" { | ||||||
|  | 			context.GetLogger(ctx).Debugf("Skipping unsupported resource type: %s", access.Type) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if !strings.HasPrefix(access.Name, scope) { | ||||||
|  | 			context.GetLogger(ctx).Debugf("Resource scope not allowed: %s", access.Name) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		grantedAccessList = append(grantedAccessList, access) | ||||||
|  | 	} | ||||||
|  | 	return grantedAccessList | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // getToken handles authenticating the request and authorizing access to the
 | // getToken handles authenticating the request and authorizing access to the
 | ||||||
|  | @ -133,6 +184,15 @@ func (ts *tokenServer) getToken(ctx context.Context, w http.ResponseWriter, r *h | ||||||
| 	params := r.URL.Query() | 	params := r.URL.Query() | ||||||
| 	service := params.Get("service") | 	service := params.Get("service") | ||||||
| 	scopeSpecifiers := params["scope"] | 	scopeSpecifiers := params["scope"] | ||||||
|  | 	var offline bool | ||||||
|  | 	if offlineStr := params.Get("offline_token"); offlineStr != "" { | ||||||
|  | 		var err error | ||||||
|  | 		offline, err = strconv.ParseBool(offlineStr) | ||||||
|  | 		if err != nil { | ||||||
|  | 			handleError(ctx, ErrorBadTokenOption.WithDetail(err), w) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	requestedAccessList := ResolveScopeSpecifiers(ctx, scopeSpecifiers) | 	requestedAccessList := ResolveScopeSpecifiers(ctx, scopeSpecifiers) | ||||||
| 
 | 
 | ||||||
|  | @ -166,20 +226,7 @@ func (ts *tokenServer) getToken(ctx context.Context, w http.ResponseWriter, r *h | ||||||
| 	ctx = context.WithValue(ctx, "requestedAccess", requestedAccessList) | 	ctx = context.WithValue(ctx, "requestedAccess", requestedAccessList) | ||||||
| 	ctx = context.WithLogger(ctx, context.GetLogger(ctx, "requestedAccess")) | 	ctx = context.WithLogger(ctx, context.GetLogger(ctx, "requestedAccess")) | ||||||
| 
 | 
 | ||||||
| 	scopePrefix := username + "/" | 	grantedAccessList := filterAccessList(ctx, username, requestedAccessList) | ||||||
| 	grantedAccessList := make([]auth.Access, 0, len(requestedAccessList)) |  | ||||||
| 	for _, access := range requestedAccessList { |  | ||||||
| 		if access.Type != "repository" { |  | ||||||
| 			context.GetLogger(ctx).Debugf("Skipping unsupported resource type: %s", access.Type) |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
| 		if !strings.HasPrefix(access.Name, scopePrefix) { |  | ||||||
| 			context.GetLogger(ctx).Debugf("Resource scope not allowed: %s", access.Name) |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
| 		grantedAccessList = append(grantedAccessList, access) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	ctx = context.WithValue(ctx, "grantedAccess", grantedAccessList) | 	ctx = context.WithValue(ctx, "grantedAccess", grantedAccessList) | ||||||
| 	ctx = context.WithLogger(ctx, context.GetLogger(ctx, "grantedAccess")) | 	ctx = context.WithLogger(ctx, context.GetLogger(ctx, "grantedAccess")) | ||||||
| 
 | 
 | ||||||
|  | @ -191,11 +238,151 @@ func (ts *tokenServer) getToken(ctx context.Context, w http.ResponseWriter, r *h | ||||||
| 
 | 
 | ||||||
| 	context.GetLogger(ctx).Info("authorized client") | 	context.GetLogger(ctx).Info("authorized client") | ||||||
| 
 | 
 | ||||||
| 	// Get response context.
 | 	response := tokenResponse{ | ||||||
|  | 		Token:     token, | ||||||
|  | 		ExpiresIn: int(ts.issuer.Expiration.Seconds()), | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if offline { | ||||||
|  | 		response.RefreshToken = newRefreshToken() | ||||||
|  | 		ts.refreshCache[response.RefreshToken] = refreshToken{ | ||||||
|  | 			subject: username, | ||||||
|  | 			service: service, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	ctx, w = context.WithResponseWriter(ctx, w) | 	ctx, w = context.WithResponseWriter(ctx, w) | ||||||
| 
 | 
 | ||||||
| 	w.Header().Set("Content-Type", "application/json") | 	w.Header().Set("Content-Type", "application/json") | ||||||
| 	json.NewEncoder(w).Encode(map[string]string{"token": token}) | 	json.NewEncoder(w).Encode(response) | ||||||
| 
 | 
 | ||||||
| 	context.GetResponseLogger(ctx).Info("get token complete") | 	context.GetResponseLogger(ctx).Info("get token complete") | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | type postTokenResponse struct { | ||||||
|  | 	Token        string `json:"access_token"` | ||||||
|  | 	Scope        string `json:"scope,omitempty"` | ||||||
|  | 	ExpiresIn    int    `json:"expires_in,omitempty"` | ||||||
|  | 	IssuedAt     string `json:"issued_at,omitempty"` | ||||||
|  | 	RefreshToken string `json:"refresh_token,omitempty"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // postToken handles authenticating the request and authorizing access to the
 | ||||||
|  | // requested scopes.
 | ||||||
|  | func (ts *tokenServer) postToken(ctx context.Context, w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	grantType := r.PostFormValue("grant_type") | ||||||
|  | 	if grantType == "" { | ||||||
|  | 		handleError(ctx, ErrorMissingRequiredField.WithDetail("missing grant_type value"), w) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	service := r.PostFormValue("service") | ||||||
|  | 	if service == "" { | ||||||
|  | 		handleError(ctx, ErrorMissingRequiredField.WithDetail("missing service value"), w) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	clientID := r.PostFormValue("client_id") | ||||||
|  | 	if clientID == "" { | ||||||
|  | 		handleError(ctx, ErrorMissingRequiredField.WithDetail("missing client_id value"), w) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var offline bool | ||||||
|  | 	switch r.PostFormValue("access_type") { | ||||||
|  | 	case "", "online": | ||||||
|  | 	case "offline": | ||||||
|  | 		offline = true | ||||||
|  | 	default: | ||||||
|  | 		handleError(ctx, ErrorUnsupportedValue.WithDetail("unknown access_type value"), w) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	requestedAccessList := ResolveScopeList(ctx, r.PostFormValue("scope")) | ||||||
|  | 
 | ||||||
|  | 	var subject string | ||||||
|  | 	var rToken string | ||||||
|  | 	switch grantType { | ||||||
|  | 	case "refresh_token": | ||||||
|  | 		rToken = r.PostFormValue("refresh_token") | ||||||
|  | 		if rToken == "" { | ||||||
|  | 			handleError(ctx, ErrorUnsupportedValue.WithDetail("missing refresh_token value"), w) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		rt, ok := ts.refreshCache[rToken] | ||||||
|  | 		if !ok || rt.service != service { | ||||||
|  | 			handleError(ctx, errcode.ErrorCodeUnauthorized.WithDetail("invalid refresh token"), w) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		subject = rt.subject | ||||||
|  | 	case "password": | ||||||
|  | 		ca, ok := ts.accessController.(auth.CredentialAuthenticator) | ||||||
|  | 		if !ok { | ||||||
|  | 			handleError(ctx, ErrorUnsupportedValue.WithDetail("password grant type not supported"), w) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		subject = r.PostFormValue("username") | ||||||
|  | 		if subject == "" { | ||||||
|  | 			handleError(ctx, ErrorUnsupportedValue.WithDetail("missing username value"), w) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		password := r.PostFormValue("password") | ||||||
|  | 		if password == "" { | ||||||
|  | 			handleError(ctx, ErrorUnsupportedValue.WithDetail("missing password value"), w) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		if err := ca.AuthenticateUser(subject, password); err != nil { | ||||||
|  | 			handleError(ctx, errcode.ErrorCodeUnauthorized.WithDetail("invalid credentials"), w) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	default: | ||||||
|  | 		handleError(ctx, ErrorUnsupportedValue.WithDetail("unknown grant_type value"), w) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	ctx = context.WithValue(ctx, "acctSubject", subject) | ||||||
|  | 	ctx = context.WithLogger(ctx, context.GetLogger(ctx, "acctSubject")) | ||||||
|  | 
 | ||||||
|  | 	context.GetLogger(ctx).Info("authenticated client") | ||||||
|  | 
 | ||||||
|  | 	ctx = context.WithValue(ctx, "requestedAccess", requestedAccessList) | ||||||
|  | 	ctx = context.WithLogger(ctx, context.GetLogger(ctx, "requestedAccess")) | ||||||
|  | 
 | ||||||
|  | 	grantedAccessList := filterAccessList(ctx, subject, requestedAccessList) | ||||||
|  | 	ctx = context.WithValue(ctx, "grantedAccess", grantedAccessList) | ||||||
|  | 	ctx = context.WithLogger(ctx, context.GetLogger(ctx, "grantedAccess")) | ||||||
|  | 
 | ||||||
|  | 	token, err := ts.issuer.CreateJWT(subject, service, grantedAccessList) | ||||||
|  | 	if err != nil { | ||||||
|  | 		handleError(ctx, err, w) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	context.GetLogger(ctx).Info("authorized client") | ||||||
|  | 
 | ||||||
|  | 	response := postTokenResponse{ | ||||||
|  | 		Token:     token, | ||||||
|  | 		ExpiresIn: int(ts.issuer.Expiration.Seconds()), | ||||||
|  | 		IssuedAt:  time.Now().UTC().Format(time.RFC3339), | ||||||
|  | 		Scope:     ToScopeList(grantedAccessList), | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if offline { | ||||||
|  | 		rToken = newRefreshToken() | ||||||
|  | 		ts.refreshCache[rToken] = refreshToken{ | ||||||
|  | 			subject: subject, | ||||||
|  | 			service: service, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if rToken != "" { | ||||||
|  | 		response.RefreshToken = rToken | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	ctx, w = context.WithResponseWriter(ctx, w) | ||||||
|  | 
 | ||||||
|  | 	w.Header().Set("Content-Type", "application/json") | ||||||
|  | 	json.NewEncoder(w).Encode(response) | ||||||
|  | 
 | ||||||
|  | 	context.GetResponseLogger(ctx).Info("post token complete") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -55,6 +55,23 @@ func ResolveScopeSpecifiers(ctx context.Context, scopeSpecs []string) []auth.Acc | ||||||
| 	return requestedAccessList | 	return requestedAccessList | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // ResolveScopeList converts a scope list from a token request's
 | ||||||
|  | // `scope` parameter into a list of standard access objects.
 | ||||||
|  | func ResolveScopeList(ctx context.Context, scopeList string) []auth.Access { | ||||||
|  | 	scopes := strings.Split(scopeList, " ") | ||||||
|  | 	return ResolveScopeSpecifiers(ctx, scopes) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ToScopeList converts a list of access to a
 | ||||||
|  | // scope list string
 | ||||||
|  | func ToScopeList(access []auth.Access) string { | ||||||
|  | 	var s []string | ||||||
|  | 	for _, a := range access { | ||||||
|  | 		s = append(s, fmt.Sprintf("%s:%s:%s", a.Type, a.Name, a.Action)) | ||||||
|  | 	} | ||||||
|  | 	return strings.Join(s, ",") | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // TokenIssuer represents an issuer capable of generating JWT tokens
 | // TokenIssuer represents an issuer capable of generating JWT tokens
 | ||||||
| type TokenIssuer struct { | type TokenIssuer struct { | ||||||
| 	Issuer     string | 	Issuer     string | ||||||
|  |  | ||||||
|  | @ -33,6 +33,7 @@ | ||||||
| package auth | package auth | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
|  | @ -49,6 +50,14 @@ const ( | ||||||
| 	UserNameKey = "auth.user.name" | 	UserNameKey = "auth.user.name" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | var ( | ||||||
|  | 	// ErrInvalidCredential is returned when the auth token does not authenticate correctly.
 | ||||||
|  | 	ErrInvalidCredential = errors.New("invalid authorization credential") | ||||||
|  | 
 | ||||||
|  | 	// ErrAuthenticationFailure returned when authentication fails.
 | ||||||
|  | 	ErrAuthenticationFailure = errors.New("authentication failure") | ||||||
|  | ) | ||||||
|  | 
 | ||||||
| // UserInfo carries information about
 | // UserInfo carries information about
 | ||||||
| // an autenticated/authorized client.
 | // an autenticated/authorized client.
 | ||||||
| type UserInfo struct { | type UserInfo struct { | ||||||
|  | @ -97,6 +106,11 @@ type AccessController interface { | ||||||
| 	Authorized(ctx context.Context, access ...Access) (context.Context, error) | 	Authorized(ctx context.Context, access ...Access) (context.Context, error) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // CredentialAuthenticator is an object which is able to authenticate credentials
 | ||||||
|  | type CredentialAuthenticator interface { | ||||||
|  | 	AuthenticateUser(username, password string) error | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // WithUser returns a context with the authorized user info.
 | // WithUser returns a context with the authorized user info.
 | ||||||
| func WithUser(ctx context.Context, user UserInfo) context.Context { | func WithUser(ctx context.Context, user UserInfo) context.Context { | ||||||
| 	return userInfoContext{ | 	return userInfoContext{ | ||||||
|  |  | ||||||
|  | @ -6,7 +6,6 @@ | ||||||
| package htpasswd | package htpasswd | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"errors" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"os" | 	"os" | ||||||
|  | @ -15,14 +14,6 @@ import ( | ||||||
| 	"github.com/docker/distribution/registry/auth" | 	"github.com/docker/distribution/registry/auth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var ( |  | ||||||
| 	// ErrInvalidCredential is returned when the auth token does not authenticate correctly.
 |  | ||||||
| 	ErrInvalidCredential = errors.New("invalid authorization credential") |  | ||||||
| 
 |  | ||||||
| 	// ErrAuthenticationFailure returned when authentication failure to be presented to agent.
 |  | ||||||
| 	ErrAuthenticationFailure = errors.New("authentication failure") |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type accessController struct { | type accessController struct { | ||||||
| 	realm    string | 	realm    string | ||||||
| 	htpasswd *htpasswd | 	htpasswd *htpasswd | ||||||
|  | @ -65,21 +56,25 @@ func (ac *accessController) Authorized(ctx context.Context, accessRecords ...aut | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		return nil, &challenge{ | 		return nil, &challenge{ | ||||||
| 			realm: ac.realm, | 			realm: ac.realm, | ||||||
| 			err:   ErrInvalidCredential, | 			err:   auth.ErrInvalidCredential, | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := ac.htpasswd.authenticateUser(username, password); err != nil { | 	if err := ac.AuthenticateUser(username, password); err != nil { | ||||||
| 		context.GetLogger(ctx).Errorf("error authenticating user %q: %v", username, err) | 		context.GetLogger(ctx).Errorf("error authenticating user %q: %v", username, err) | ||||||
| 		return nil, &challenge{ | 		return nil, &challenge{ | ||||||
| 			realm: ac.realm, | 			realm: ac.realm, | ||||||
| 			err:   ErrAuthenticationFailure, | 			err:   auth.ErrAuthenticationFailure, | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return auth.WithUser(ctx, auth.UserInfo{Name: username}), nil | 	return auth.WithUser(ctx, auth.UserInfo{Name: username}), nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (ac *accessController) AuthenticateUser(username, password string) error { | ||||||
|  | 	return ac.htpasswd.authenticateUser(username, password) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // challenge implements the auth.Challenge interface.
 | // challenge implements the auth.Challenge interface.
 | ||||||
| type challenge struct { | type challenge struct { | ||||||
| 	realm string | 	realm string | ||||||
|  |  | ||||||
|  | @ -6,6 +6,8 @@ import ( | ||||||
| 	"io" | 	"io" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/docker/distribution/registry/auth" | ||||||
|  | 
 | ||||||
| 	"golang.org/x/crypto/bcrypt" | 	"golang.org/x/crypto/bcrypt" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -33,12 +35,12 @@ func (htpasswd *htpasswd) authenticateUser(username string, password string) err | ||||||
| 		// timing attack paranoia
 | 		// timing attack paranoia
 | ||||||
| 		bcrypt.CompareHashAndPassword([]byte{}, []byte(password)) | 		bcrypt.CompareHashAndPassword([]byte{}, []byte(password)) | ||||||
| 
 | 
 | ||||||
| 		return ErrAuthenticationFailure | 		return auth.ErrAuthenticationFailure | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	err := bcrypt.CompareHashAndPassword([]byte(credentials), []byte(password)) | 	err := bcrypt.CompareHashAndPassword([]byte(credentials), []byte(password)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return ErrAuthenticationFailure | 		return auth.ErrAuthenticationFailure | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return nil | 	return nil | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue