Added support for bcrypt, plaintext; extension points for other htpasswd hash methods.
Signed-off-by: Dave Trombley <dave.trombley@gmail.com>master
							parent
							
								
									d2b7988b7f
								
							
						
					
					
						commit
						ff67393b2b
					
				| 
						 | 
				
			
			@ -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