163 lines
5.1 KiB
Go
163 lines
5.1 KiB
Go
package auth
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha1"
|
|
"crypto/subtle"
|
|
"encoding/base64"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
"golang.org/x/net/context"
|
|
)
|
|
|
|
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},
|
|
}
|
|
)
|
|
|
|
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))
|
|
|
|
/*
|
|
Checks the username/password combination from the request. Returns
|
|
either an empty string (authentication failed) or the name of the
|
|
authenticated user.
|
|
|
|
Supports MD5 and SHA1 password entries
|
|
*/
|
|
func (a *BasicAuth) CheckAuth(r *http.Request) string {
|
|
s := strings.SplitN(r.Header.Get(a.Headers.V().Authorization), " ", 2)
|
|
if len(s) != 2 || s[0] != "Basic" {
|
|
return ""
|
|
}
|
|
|
|
b, err := base64.StdEncoding.DecodeString(s[1])
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
pair := strings.SplitN(string(b), ":", 2)
|
|
if len(pair) != 2 {
|
|
return ""
|
|
}
|
|
user, password := pair[0], pair[1]
|
|
secret := a.Secrets(user, a.Realm)
|
|
if secret == "" {
|
|
return ""
|
|
}
|
|
compare := compareFuncs[0].compare
|
|
for _, cmp := range compareFuncs[1:] {
|
|
if strings.HasPrefix(secret, cmp.prefix) {
|
|
compare = cmp.compare
|
|
break
|
|
}
|
|
}
|
|
if compare([]byte(secret), []byte(password)) != nil {
|
|
return ""
|
|
}
|
|
return pair[0]
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
/*
|
|
http.Handler 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))
|
|
}
|
|
|
|
/*
|
|
BasicAuthenticator returns a function, which wraps an
|
|
AuthenticatedHandlerFunc converting it to http.HandlerFunc. This
|
|
wrapper function checks the authentication and either sends back
|
|
required authentication headers, or calls the wrapped function with
|
|
authenticated username in the AuthenticatedRequest.
|
|
*/
|
|
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)
|
|
}
|
|
|
|
func NewBasicAuthenticator(realm string, secrets SecretProvider) *BasicAuth {
|
|
return &BasicAuth{Realm: realm, Secrets: secrets}
|
|
}
|