Added support for bcrypt, plaintext; extension points for other htpasswd hash methods.
Signed-off-by: Dave Trombley <dave.trombley@gmail.com>master
							parent
							
								
									c4849bb99a
								
							
						
					
					
						commit
						c50dfb7dae
					
				|  | @ -9,11 +9,9 @@ | |||
| package basic | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/base64" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	ctxu "github.com/docker/distribution/context" | ||||
| 	"github.com/docker/distribution/registry/auth" | ||||
|  | @ -58,8 +56,7 @@ func (ac *accessController) Authorized(ctx context.Context, accessRecords ...aut | |||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	authHeader := req.Header.Get("Authorization") | ||||
| 
 | ||||
| 	authHeader := req.Header.Get("Authorization")  | ||||
| 	if authHeader == "" { | ||||
| 		challenge := challenge{ | ||||
| 			realm: ac.realm, | ||||
|  | @ -67,35 +64,20 @@ func (ac *accessController) Authorized(ctx context.Context, accessRecords ...aut | |||
| 		return nil, &challenge | ||||
| 	} | ||||
| 
 | ||||
| 	parts := strings.Split(req.Header.Get("Authorization"), " ") | ||||
| 
 | ||||
| 	challenge := challenge{ | ||||
| 		realm: ac.realm, | ||||
| 	user, pass, ok := req.BasicAuth() | ||||
| 	if !ok { | ||||
| 		return nil, errors.New("Invalid Authorization header") | ||||
| 	} | ||||
| 
 | ||||
| 	if len(parts) != 2 || strings.ToLower(parts[0]) != "basic" { | ||||
| 		challenge.err = ErrPasswordRequired | ||||
| 		return nil, &challenge | ||||
| 	} | ||||
| 
 | ||||
| 	text, err := base64.StdEncoding.DecodeString(parts[1]) | ||||
| 	if err != nil { | ||||
| 		 | ||||
| 	if res, _ := ac.htpasswd.AuthenticateUser(user, pass); !res { | ||||
| 		challenge := challenge{ | ||||
| 			realm: ac.realm, | ||||
| 		} | ||||
| 		challenge.err = ErrInvalidCredential | ||||
| 		return nil, &challenge | ||||
| 	} | ||||
| 
 | ||||
| 	credential := strings.Split(string(text), ":") | ||||
| 	if len(credential) != 2 { | ||||
| 		challenge.err = ErrInvalidCredential | ||||
| 		return nil, &challenge | ||||
| 	} | ||||
| 
 | ||||
| 	if res, _ := ac.htpasswd.AuthenticateUser(credential[0], credential[1]); !res { | ||||
| 		challenge.err = ErrInvalidCredential | ||||
| 		return nil, &challenge | ||||
| 	} | ||||
| 
 | ||||
| 	return auth.WithUser(ctx, auth.UserInfo{Name: credential[0]}), nil | ||||
| 	return auth.WithUser(ctx, auth.UserInfo{Name: user}), nil | ||||
| } | ||||
| 
 | ||||
| func (ch *challenge) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
|  |  | |||
|  | @ -14,8 +14,13 @@ import ( | |||
| func TestBasicAccessController(t *testing.T) { | ||||
| 
 | ||||
| 	testRealm := "The-Shire" | ||||
| 	testUser := "bilbo" | ||||
| 	testHtpasswdContent := "bilbo:{SHA}5siv5c0SHx681xU6GiSx9ZQryqs=" | ||||
| 	testUsers := []string{"bilbo","frodo","MiShil","DeokMan"} | ||||
| 	testPasswords := []string{"baggins","baggins","새주","공주님"} | ||||
| 	testHtpasswdContent := `bilbo:{SHA}5siv5c0SHx681xU6GiSx9ZQryqs= | ||||
| 							frodo:$2y$05$926C3y10Quzn/LnqQH86VOEVh/18T6RnLaS.khre96jLNL/7e.K5W | ||||
| 							MiShil:$2y$05$0oHgwMehvoe8iAWS8I.7l.KoECXrwVaC16RPfaSCU5eVTFrATuMI2 | ||||
| 							DeokMan:공주님` | ||||
| 	 | ||||
| 
 | ||||
| 	tempFile, err := ioutil.TempFile("", "htpasswd-test") | ||||
| 	if err != nil { | ||||
|  | @ -36,7 +41,9 @@ func TestBasicAccessController(t *testing.T) { | |||
| 	} | ||||
| 
 | ||||
| 	tempFile.Close() | ||||
| 
 | ||||
| 	 | ||||
| 	var userNumber = 0 | ||||
| 	 | ||||
| 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		ctx := context.WithValue(nil, "http.request", r) | ||||
| 		authCtx, err := accessController.Authorized(ctx) | ||||
|  | @ -55,8 +62,8 @@ func TestBasicAccessController(t *testing.T) { | |||
| 			t.Fatal("basic accessController did not set auth.user context") | ||||
| 		} | ||||
| 
 | ||||
| 		if userInfo.Name != testUser { | ||||
| 			t.Fatalf("expected user name %q, got %q", testUser, userInfo.Name) | ||||
| 		if userInfo.Name != testUsers[userNumber] { | ||||
| 			t.Fatalf("expected user name %q, got %q", testUsers[userNumber], userInfo.Name) | ||||
| 		} | ||||
| 
 | ||||
| 		w.WriteHeader(http.StatusNoContent) | ||||
|  | @ -79,22 +86,25 @@ func TestBasicAccessController(t *testing.T) { | |||
| 		t.Fatalf("unexpected non-fail response status: %v != %v", resp.StatusCode, http.StatusUnauthorized) | ||||
| 	} | ||||
| 
 | ||||
| 	req, _ = http.NewRequest("GET", server.URL, nil) | ||||
| 	for i := 0; i < len(testUsers); i++ { | ||||
| 		userNumber = i | ||||
| 		req, _ = http.NewRequest("GET", server.URL, nil) | ||||
| 		sekrit := testUsers[i]+":"+testPasswords[i]  | ||||
| 		credential := "Basic " + base64.StdEncoding.EncodeToString([]byte(sekrit)) | ||||
| 
 | ||||
| 	sekrit := "bilbo:baggins" | ||||
| 	credential := "Basic " + base64.StdEncoding.EncodeToString([]byte(sekrit)) | ||||
| 		req.Header.Set("Authorization", credential) | ||||
| 		resp, err = client.Do(req) | ||||
| 		 | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("unexpected error during GET: %v", err) | ||||
| 		} | ||||
| 		defer resp.Body.Close() | ||||
| 
 | ||||
| 	req.Header.Set("Authorization", credential) | ||||
| 	resp, err = client.Do(req) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("unexpected error during GET: %v", err) | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	// Request should be authorized
 | ||||
| 	if resp.StatusCode != http.StatusNoContent { | ||||
| 		t.Fatalf("unexpected non-success response status: %v != %v", resp.StatusCode, http.StatusNoContent) | ||||
| 		// Request should be authorized
 | ||||
| 		if resp.StatusCode != http.StatusNoContent { | ||||
| 			t.Fatalf("unexpected non-success response status: %v != %v for %s %s %s", resp.StatusCode, http.StatusNoContent, testUsers[i], testPasswords[i], credential) | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -6,11 +6,14 @@ import ( | |||
| 	"encoding/csv" | ||||
| 	"errors" | ||||
| 	"os" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	 | ||||
| 	"golang.org/x/crypto/bcrypt" | ||||
| ) | ||||
| 
 | ||||
| // ErrSHARequired - returned in error field of challenge when the htpasswd was not made using SHA1 algorithm.
 | ||||
| // 	            (SHA1 is considered obsolete but the alternative for htpasswd is MD5, or system crypt...)
 | ||||
| var ErrSHARequired = errors.New("htpasswd file must use SHA (htpasswd -s)") | ||||
| // AuthenticationFailureErr - a generic error message for authentication failure to be presented to agent.
 | ||||
| var AuthenticationFailureErr = errors.New("Bad username or password") | ||||
| 
 | ||||
| // HTPasswd - holds a path to a system .htpasswd file and the machinery to parse it.
 | ||||
| type HTPasswd struct { | ||||
|  | @ -18,18 +21,57 @@ type HTPasswd struct { | |||
| 	reader *csv.Reader | ||||
| } | ||||
| 
 | ||||
| // AuthType represents a particular hash function used in the htpasswd file.
 | ||||
| type AuthType int  | ||||
| const ( | ||||
| 	PlainText 	AuthType 	= iota | ||||
| 	SHA1 | ||||
| 	ApacheMD5 | ||||
| 	BCrypt | ||||
| 	Crypt | ||||
| ) | ||||
| 
 | ||||
| // String returns a text representation of the AuthType
 | ||||
| func (at AuthType) String() string { | ||||
| 	switch(at) { | ||||
| 		case PlainText: return "plaintext" | ||||
| 		case SHA1: return "sha1" | ||||
| 		case ApacheMD5: return "md5" | ||||
| 		case BCrypt: return "bcrypt" | ||||
| 		case Crypt: return "system crypt" | ||||
| 	} | ||||
| 	return "unknown" | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| // NewHTPasswd - Create a new HTPasswd with the given path to .htpasswd file.
 | ||||
| func NewHTPasswd(htpath string) *HTPasswd { | ||||
| 	return &HTPasswd{path: htpath} | ||||
| } | ||||
| 
 | ||||
| var bcryptPrefixRegexp *regexp.Regexp = regexp.MustCompile(`^\$2[ab]?y\$`) | ||||
| 
 | ||||
| // GetAuthCredentialType - Inspect an htpasswd file credential and guess the encryption algorithm used.
 | ||||
| func GetAuthCredentialType(cred string) AuthType { | ||||
| 	if strings.HasPrefix(cred, "{SHA}") { | ||||
| 		return SHA1 | ||||
| 	} | ||||
| 	if strings.HasPrefix(cred, "$apr1$") { | ||||
| 		return ApacheMD5 | ||||
| 	} | ||||
| 	if bcryptPrefixRegexp.MatchString(cred) { | ||||
| 		return BCrypt | ||||
| 	} | ||||
| 	// There's just not a great way to distinguish between these next two...
 | ||||
| 	if len(cred) == 13 { | ||||
| 		return Crypt | ||||
| 	} | ||||
| 	return PlainText | ||||
| } | ||||
| 
 | ||||
| // AuthenticateUser - Check a given user:password credential against the receiving HTPasswd's file.
 | ||||
| func (htpasswd *HTPasswd) AuthenticateUser(user string, pwd string) (bool, error) { | ||||
| 
 | ||||
| 	// Hash the credential.
 | ||||
| 	sha := sha1.New() | ||||
| 	sha.Write([]byte(pwd)) | ||||
| 	hash := base64.StdEncoding.EncodeToString(sha.Sum(nil)) | ||||
| 
 | ||||
| 	// Open the file.
 | ||||
| 	in, err := os.Open(htpasswd.path) | ||||
|  | @ -43,12 +85,43 @@ func (htpasswd *HTPasswd) AuthenticateUser(user string, pwd string) (bool, error | |||
| 	reader.Comment = '#' | ||||
| 	reader.TrimLeadingSpace = true | ||||
| 	for entry, readerr := reader.Read(); entry != nil || readerr != nil; entry, readerr = reader.Read() { | ||||
| 		if readerr != nil { | ||||
| 			return false, readerr | ||||
| 		} | ||||
| 		if len(entry) == 0 { | ||||
| 			continue | ||||
| 		} | ||||
| 		if entry[0] == user { | ||||
| 			if len(entry[1]) < 6 || entry[1][0:5] != "{SHA}" { | ||||
| 				return false, ErrSHARequired | ||||
| 			credential := entry[1] | ||||
| 			credType := GetAuthCredentialType(credential) | ||||
| 			switch(credType) { | ||||
| 				case SHA1: { | ||||
| 					sha := sha1.New() | ||||
| 					sha.Write([]byte(pwd)) | ||||
| 					hash := base64.StdEncoding.EncodeToString(sha.Sum(nil)) | ||||
| 					return entry[1][5:] == hash, nil | ||||
| 				} | ||||
| 				case ApacheMD5: { | ||||
| 					return false, errors.New(ApacheMD5.String()+" htpasswd hash function not yet supported") | ||||
| 				} | ||||
| 				case BCrypt: { | ||||
| 					err := bcrypt.CompareHashAndPassword([]byte(credential),[]byte(pwd)) | ||||
| 					if err != nil { | ||||
| 						return false, err | ||||
| 					} | ||||
| 					return true, nil | ||||
| 				} | ||||
| 				case Crypt: { | ||||
| 					return false, errors.New(Crypt.String()+" htpasswd hash function not yet supported") | ||||
| 				} | ||||
| 				case PlainText: { | ||||
| 					if pwd == credential { | ||||
| 						return true, nil | ||||
| 					}  | ||||
| 					return false, AuthenticationFailureErr | ||||
| 				} | ||||
| 			} | ||||
| 			return entry[1][5:] == hash, nil | ||||
| 		} | ||||
| 	} | ||||
| 	return false, nil | ||||
| 	return false, AuthenticationFailureErr | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue