package auth import ( "bytes" "context" "crypto/sha1" "crypto/subtle" "encoding/base64" "errors" "net/http" "strings" "golang.org/x/crypto/bcrypt" ) type compareFunc func(hashedPassword, password []byte) error var ( errMismatchedHashAndPassword = errors.New("mismatched hash and password") compareFuncs = []struct { prefix string compare compareFunc }{ {"", compareMD5HashAndPassword}, // default compareFunc {"{SHA}", compareShaHashAndPassword}, // Bcrypt is complicated. According to crypt(3) from // crypt_blowfish version 1.3 (fetched from // http://www.openwall.com/crypt/crypt_blowfish-1.3.tar.gz), there // are three different has prefixes: "$2a$", used by versions up // to 1.0.4, and "$2x$" and "$2y$", used in all later // versions. "$2a$" has a known bug, "$2x$" was added as a // migration path for systems with "$2a$" prefix and still has a // bug, and only "$2y$" should be used by modern systems. The bug // has something to do with handling of 8-bit characters. Since // both "$2a$" and "$2x$" are deprecated, we are handling them the // same way as "$2y$", which will yield correct results for 7-bit // character passwords, but is wrong for 8-bit character // passwords. You have to upgrade to "$2y$" if you want sant 8-bit // character password support with bcrypt. To add to the mess, // OpenBSD 5.5. introduced "$2b$" prefix, which behaves exactly // like "$2y$" according to the same source. {"$2a$", bcrypt.CompareHashAndPassword}, {"$2b$", bcrypt.CompareHashAndPassword}, {"$2x$", bcrypt.CompareHashAndPassword}, {"$2y$", bcrypt.CompareHashAndPassword}, } ) // BasicAuth is an authenticator implementation for 'Basic' HTTP // Authentication scheme (RFC 7617). type BasicAuth struct { Realm string Secrets SecretProvider // Headers used by authenticator. Set to ProxyHeaders to use with // proxy server. When nil, NormalHeaders are used. Headers *Headers } // check that BasicAuth implements AuthenticatorInterface var _ = (AuthenticatorInterface)((*BasicAuth)(nil)) // CheckAuth checks the username/password combination from the // request. Returns either an empty string (authentication failed) or // the name of the authenticated user. func (a *BasicAuth) CheckAuth(r *http.Request) string { user, password, ok := r.BasicAuth() if !ok { return "" } secret := a.Secrets(user, a.Realm) if secret == "" { return "" } if !CheckSecret(password, secret) { return "" } return user } // CheckSecret returns true if the password matches the encrypted // secret. func CheckSecret(password, secret string) bool { compare := compareFuncs[0].compare for _, cmp := range compareFuncs[1:] { if strings.HasPrefix(secret, cmp.prefix) { compare = cmp.compare break } } return compare([]byte(secret), []byte(password)) == nil } func compareShaHashAndPassword(hashedPassword, password []byte) error { d := sha1.New() d.Write(password) if subtle.ConstantTimeCompare(hashedPassword[5:], []byte(base64.StdEncoding.EncodeToString(d.Sum(nil)))) != 1 { return errMismatchedHashAndPassword } return nil } func compareMD5HashAndPassword(hashedPassword, password []byte) error { parts := bytes.SplitN(hashedPassword, []byte("$"), 4) if len(parts) != 4 { return errMismatchedHashAndPassword } magic := []byte("$" + string(parts[1]) + "$") salt := parts[2] if subtle.ConstantTimeCompare(hashedPassword, MD5Crypt(password, salt, magic)) != 1 { return errMismatchedHashAndPassword } return nil } // RequireAuth is an http.HandlerFunc for BasicAuth which initiates // the authentication process (or requires reauthentication). func (a *BasicAuth) RequireAuth(w http.ResponseWriter, r *http.Request) { w.Header().Set(contentType, a.Headers.V().UnauthContentType) w.Header().Set(a.Headers.V().Authenticate, `Basic realm="`+a.Realm+`"`) w.WriteHeader(a.Headers.V().UnauthCode) w.Write([]byte(a.Headers.V().UnauthResponse)) } // Wrap returns an http.HandlerFunc, which wraps // AuthenticatedHandlerFunc with this BasicAuth authenticator's // authentication checks. Once the request contains valid credentials, // it calls wrapped AuthenticatedHandlerFunc. // // Deprecated: new code should use NewContext instead. func (a *BasicAuth) Wrap(wrapped AuthenticatedHandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if username := a.CheckAuth(r); username == "" { a.RequireAuth(w, r) } else { ar := &AuthenticatedRequest{Request: *r, Username: username} wrapped(w, ar) } } } // NewContext returns a context carrying authentication information for the request. func (a *BasicAuth) NewContext(ctx context.Context, r *http.Request) context.Context { info := &Info{Username: a.CheckAuth(r), ResponseHeaders: make(http.Header)} info.Authenticated = (info.Username != "") if !info.Authenticated { info.ResponseHeaders.Set(a.Headers.V().Authenticate, `Basic realm="`+a.Realm+`"`) } return context.WithValue(ctx, infoKey, info) } // NewBasicAuthenticator returns a BasicAuth initialized with provided // realm and secrets. // // Deprecated: new code should construct BasicAuth values directly. func NewBasicAuthenticator(realm string, secrets SecretProvider) *BasicAuth { return &BasicAuth{Realm: realm, Secrets: secrets} }