New file |
| | |
| | | // Package auth is an implementation of HTTP Basic and HTTP Digest authentication. |
| | | package auth |
| | | |
| | | import ( |
| | | "context" |
| | | "net/http" |
| | | ) |
| | | |
| | | // AuthenticatedRequest is passed to AuthenticatedHandlerFunc instead |
| | | // of *http.Request. |
| | | type AuthenticatedRequest struct { |
| | | http.Request |
| | | // Username is the authenticated user name. Current API implies that |
| | | // Username is never empty, which means that authentication is |
| | | // always done before calling the request handler. |
| | | Username string |
| | | } |
| | | |
| | | // AuthenticatedHandlerFunc is like http.HandlerFunc, but takes |
| | | // AuthenticatedRequest instead of http.Request |
| | | type AuthenticatedHandlerFunc func(http.ResponseWriter, *AuthenticatedRequest) |
| | | |
| | | // Authenticator wraps an AuthenticatedHandlerFunc with |
| | | // authentication-checking code. |
| | | // |
| | | // Typical Authenticator usage is something like: |
| | | // |
| | | // authenticator := SomeAuthenticator(...) |
| | | // http.HandleFunc("/", authenticator(my_handler)) |
| | | // |
| | | // Authenticator wrapper checks the user authentication and calls the |
| | | // wrapped function only after authentication has succeeded. Otherwise, |
| | | // it returns a handler which initiates the authentication procedure. |
| | | type Authenticator func(AuthenticatedHandlerFunc) http.HandlerFunc |
| | | |
| | | // Info contains authentication information for the request. |
| | | type Info struct { |
| | | // Authenticated is set to true when request was authenticated |
| | | // successfully, i.e. username and password passed in request did |
| | | // pass the check. |
| | | Authenticated bool |
| | | |
| | | // Username contains a user name passed in the request when |
| | | // Authenticated is true. It's value is undefined if Authenticated |
| | | // is false. |
| | | Username string |
| | | |
| | | // ResponseHeaders contains extra headers that must be set by server |
| | | // when sending back HTTP response. |
| | | ResponseHeaders http.Header |
| | | } |
| | | |
| | | // UpdateHeaders updates headers with this Info's ResponseHeaders. It is |
| | | // safe to call this function on nil Info. |
| | | func (i *Info) UpdateHeaders(headers http.Header) { |
| | | if i == nil { |
| | | return |
| | | } |
| | | for k, values := range i.ResponseHeaders { |
| | | for _, v := range values { |
| | | headers.Add(k, v) |
| | | } |
| | | } |
| | | } |
| | | |
| | | type key int // used for context keys |
| | | |
| | | var infoKey key |
| | | |
| | | // AuthenticatorInterface is the interface implemented by BasicAuth |
| | | // and DigestAuth authenticators. |
| | | // |
| | | // Deprecated: this interface is not coherent. New code should define |
| | | // and use your own interfaces with a required subset of authenticator |
| | | // methods. |
| | | type AuthenticatorInterface interface { |
| | | // NewContext returns a new context carrying authentication |
| | | // information extracted from the request. |
| | | NewContext(ctx context.Context, r *http.Request) context.Context |
| | | |
| | | // Wrap returns an http.HandlerFunc which wraps |
| | | // AuthenticatedHandlerFunc with this authenticator's |
| | | // authentication checks. |
| | | Wrap(AuthenticatedHandlerFunc) http.HandlerFunc |
| | | } |
| | | |
| | | // FromContext returns authentication information from the context or |
| | | // nil if no such information present. |
| | | func FromContext(ctx context.Context) *Info { |
| | | info, ok := ctx.Value(infoKey).(*Info) |
| | | if !ok { |
| | | return nil |
| | | } |
| | | return info |
| | | } |
| | | |
| | | // AuthUsernameHeader is the header set by JustCheck functions. It |
| | | // contains an authenticated username (if authentication was |
| | | // successful). |
| | | const AuthUsernameHeader = "X-Authenticated-Username" |
| | | |
| | | // JustCheck returns a new http.HandlerFunc, which requires |
| | | // authenticator to successfully authenticate a user before calling |
| | | // wrapped http.HandlerFunc. |
| | | func JustCheck(auth AuthenticatorInterface, wrapped http.HandlerFunc) http.HandlerFunc { |
| | | return auth.Wrap(func(w http.ResponseWriter, ar *AuthenticatedRequest) { |
| | | ar.Header.Set(AuthUsernameHeader, ar.Username) |
| | | wrapped(w, &ar.Request) |
| | | }) |
| | | } |
New file |
| | |
| | | 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} |
| | | } |
New file |
| | |
| | | package auth |
| | | |
| | | import ( |
| | | "context" |
| | | "crypto/subtle" |
| | | "fmt" |
| | | "net/http" |
| | | "net/url" |
| | | "sort" |
| | | "strconv" |
| | | "strings" |
| | | "sync" |
| | | "time" |
| | | ) |
| | | |
| | | type digestClient struct { |
| | | nc uint64 |
| | | lastSeen int64 |
| | | } |
| | | |
| | | // DigestAuth is an authenticator implementation for 'Digest' HTTP Authentication scheme (RFC 7616). |
| | | // |
| | | // Note: this implementation was written following now deprecated RFC |
| | | // 2617, and supports only MD5 algorithm. |
| | | // |
| | | // TODO: Add support for SHA-256 and SHA-512/256 algorithms. |
| | | type DigestAuth struct { |
| | | Realm string |
| | | Opaque string |
| | | Secrets SecretProvider |
| | | PlainTextSecrets bool |
| | | IgnoreNonceCount bool |
| | | // Headers used by authenticator. Set to ProxyHeaders to use with |
| | | // proxy server. When nil, NormalHeaders are used. |
| | | Headers *Headers |
| | | |
| | | /* |
| | | Approximate size of Client's Cache. When actual number of |
| | | tracked client nonces exceeds |
| | | ClientCacheSize+ClientCacheTolerance, ClientCacheTolerance*2 |
| | | older entries are purged. |
| | | */ |
| | | ClientCacheSize int |
| | | ClientCacheTolerance int |
| | | |
| | | clients map[string]*digestClient |
| | | mutex sync.RWMutex |
| | | } |
| | | |
| | | // check that DigestAuth implements AuthenticatorInterface |
| | | var _ = (AuthenticatorInterface)((*DigestAuth)(nil)) |
| | | |
| | | type digestCacheEntry struct { |
| | | nonce string |
| | | lastSeen int64 |
| | | } |
| | | |
| | | type digestCache []digestCacheEntry |
| | | |
| | | func (c digestCache) Less(i, j int) bool { |
| | | return c[i].lastSeen < c[j].lastSeen |
| | | } |
| | | |
| | | func (c digestCache) Len() int { |
| | | return len(c) |
| | | } |
| | | |
| | | func (c digestCache) Swap(i, j int) { |
| | | c[i], c[j] = c[j], c[i] |
| | | } |
| | | |
| | | // Purge removes count oldest entries from DigestAuth.clients |
| | | func (da *DigestAuth) Purge(count int) { |
| | | da.mutex.Lock() |
| | | da.purgeLocked(count) |
| | | da.mutex.Unlock() |
| | | } |
| | | |
| | | func (da *DigestAuth) purgeLocked(count int) { |
| | | entries := make([]digestCacheEntry, 0, len(da.clients)) |
| | | for nonce, client := range da.clients { |
| | | entries = append(entries, digestCacheEntry{nonce, client.lastSeen}) |
| | | } |
| | | cache := digestCache(entries) |
| | | sort.Sort(cache) |
| | | for _, client := range cache[:count] { |
| | | delete(da.clients, client.nonce) |
| | | } |
| | | } |
| | | |
| | | // RequireAuth is an http.HandlerFunc which initiates the |
| | | // authentication process (or requires reauthentication). |
| | | func (da *DigestAuth) RequireAuth(w http.ResponseWriter, r *http.Request) { |
| | | da.mutex.RLock() |
| | | clientsLen := len(da.clients) |
| | | da.mutex.RUnlock() |
| | | |
| | | if clientsLen > da.ClientCacheSize+da.ClientCacheTolerance { |
| | | da.Purge(da.ClientCacheTolerance * 2) |
| | | } |
| | | nonce := RandomKey() |
| | | |
| | | da.mutex.Lock() |
| | | da.clients[nonce] = &digestClient{nc: 0, lastSeen: time.Now().UnixNano()} |
| | | da.mutex.Unlock() |
| | | |
| | | da.mutex.RLock() |
| | | w.Header().Set(contentType, da.Headers.V().UnauthContentType) |
| | | w.Header().Set(da.Headers.V().Authenticate, |
| | | fmt.Sprintf(`Digest realm="%s", nonce="%s", opaque="%s", algorithm=MD5, qop="auth"`, |
| | | da.Realm, nonce, da.Opaque)) |
| | | w.WriteHeader(da.Headers.V().UnauthCode) |
| | | w.Write([]byte(da.Headers.V().UnauthResponse)) |
| | | da.mutex.RUnlock() |
| | | } |
| | | |
| | | // DigestAuthParams parses Authorization header from the |
| | | // http.Request. Returns a map of auth parameters or nil if the header |
| | | // is not a valid parsable Digest auth header. |
| | | func DigestAuthParams(authorization string) map[string]string { |
| | | s := strings.SplitN(authorization, " ", 2) |
| | | if len(s) != 2 || s[0] != "Digest" { |
| | | return nil |
| | | } |
| | | return ParsePairs(s[1]) |
| | | } |
| | | |
| | | // CheckAuth checks whether the request contains valid authentication |
| | | // data. Returns a pair of username, authinfo, where username is the |
| | | // name of the authenticated user or an empty string and authinfo is |
| | | // the contents for the optional Authentication-Info response header. |
| | | func (da *DigestAuth) CheckAuth(r *http.Request) (username string, authinfo *string) { |
| | | da.mutex.RLock() |
| | | defer da.mutex.RUnlock() |
| | | username = "" |
| | | authinfo = nil |
| | | |
| | | auth := DigestAuthParams(r.Header.Get(da.Headers.V().Authorization)) |
| | | if auth == nil { |
| | | return "", nil |
| | | } |
| | | // RFC2617 Section 3.2.1 specifies that unset value of algorithm in |
| | | // WWW-Authenticate Response header should be treated as |
| | | // "MD5". According to section 3.2.2 the "algorithm" value in |
| | | // subsequent Request Authorization header must be set to whatever |
| | | // was supplied in the WWW-Authenticate Response header. This |
| | | // implementation always returns an algorithm in WWW-Authenticate |
| | | // header, however there seems to be broken clients in the wild |
| | | // which do not set the algorithm. Assume the unset algorithm in |
| | | // Authorization header to be equal to MD5. |
| | | if _, ok := auth["algorithm"]; !ok { |
| | | auth["algorithm"] = "MD5" |
| | | } |
| | | if da.Opaque != auth["opaque"] || auth["algorithm"] != "MD5" || auth["qop"] != "auth" { |
| | | return "", nil |
| | | } |
| | | |
| | | // Check if the requested URI matches auth header |
| | | if r.RequestURI != auth["uri"] { |
| | | // We allow auth["uri"] to be a full path prefix of request-uri |
| | | // for some reason lost in history, which is probably wrong, but |
| | | // used to be like that for quite some time |
| | | // (https://tools.ietf.org/html/rfc2617#section-3.2.2 explicitly |
| | | // says that auth["uri"] is the request-uri). |
| | | // |
| | | // TODO: make an option to allow only strict checking. |
| | | switch u, err := url.Parse(auth["uri"]); { |
| | | case err != nil: |
| | | return "", nil |
| | | case r.URL == nil: |
| | | return "", nil |
| | | case len(u.Path) > len(r.URL.Path): |
| | | return "", nil |
| | | case !strings.HasPrefix(r.URL.Path, u.Path): |
| | | return "", nil |
| | | } |
| | | } |
| | | |
| | | HA1 := da.Secrets(auth["username"], da.Realm) |
| | | |
| | | if da.PlainTextSecrets { |
| | | HA1 = H(auth["username"] + ":" + da.Realm + ":" + HA1) |
| | | } |
| | | HA2 := H(r.Method + ":" + auth["uri"]) |
| | | KD := H(strings.Join([]string{HA1, auth["nonce"], auth["nc"], auth["cnonce"], auth["qop"], HA2}, ":")) |
| | | |
| | | if subtle.ConstantTimeCompare([]byte(KD), []byte(auth["response"])) != 1 { |
| | | return "", nil |
| | | } |
| | | |
| | | // At this point crypto checks are completed and validated. |
| | | // Now check if the session is valid. |
| | | |
| | | nc, err := strconv.ParseUint(auth["nc"], 16, 64) |
| | | if err != nil { |
| | | return "", nil |
| | | } |
| | | |
| | | client, ok := da.clients[auth["nonce"]] |
| | | if !ok { |
| | | return "", nil |
| | | } |
| | | if client.nc != 0 && client.nc >= nc && !da.IgnoreNonceCount { |
| | | return "", nil |
| | | } |
| | | client.nc = nc |
| | | client.lastSeen = time.Now().UnixNano() |
| | | |
| | | respHA2 := H(":" + auth["uri"]) |
| | | rspauth := H(strings.Join([]string{HA1, auth["nonce"], auth["nc"], auth["cnonce"], auth["qop"], respHA2}, ":")) |
| | | |
| | | info := fmt.Sprintf(`qop="auth", rspauth="%s", cnonce="%s", nc="%s"`, rspauth, auth["cnonce"], auth["nc"]) |
| | | return auth["username"], &info |
| | | } |
| | | |
| | | // Default values for ClientCacheSize and ClientCacheTolerance for DigestAuth |
| | | const ( |
| | | DefaultClientCacheSize = 1000 |
| | | DefaultClientCacheTolerance = 100 |
| | | ) |
| | | |
| | | // Wrap returns an http.HandlerFunc wraps AuthenticatedHandlerFunc |
| | | // with this DigestAuth authentication checks. Once the request |
| | | // contains valid credentials, it calls wrapped |
| | | // AuthenticatedHandlerFunc. |
| | | // |
| | | // Deprecated: new code should use NewContext instead. |
| | | func (da *DigestAuth) Wrap(wrapped AuthenticatedHandlerFunc) http.HandlerFunc { |
| | | return func(w http.ResponseWriter, r *http.Request) { |
| | | if username, authinfo := da.CheckAuth(r); username == "" { |
| | | da.RequireAuth(w, r) |
| | | } else { |
| | | ar := &AuthenticatedRequest{Request: *r, Username: username} |
| | | if authinfo != nil { |
| | | w.Header().Set(da.Headers.V().AuthInfo, *authinfo) |
| | | } |
| | | wrapped(w, ar) |
| | | } |
| | | } |
| | | } |
| | | |
| | | // JustCheck returns a new http.HandlerFunc, which requires |
| | | // DigestAuth to successfully authenticate a user before calling |
| | | // wrapped http.HandlerFunc. |
| | | // |
| | | // Authenticated Username is passed as an extra |
| | | // X-Authenticated-Username header to the wrapped HandlerFunc. |
| | | func (da *DigestAuth) JustCheck(wrapped http.HandlerFunc) http.HandlerFunc { |
| | | return da.Wrap(func(w http.ResponseWriter, ar *AuthenticatedRequest) { |
| | | ar.Header.Set(AuthUsernameHeader, ar.Username) |
| | | wrapped(w, &ar.Request) |
| | | }) |
| | | } |
| | | |
| | | // NewContext returns a context carrying authentication information for the request. |
| | | func (da *DigestAuth) NewContext(ctx context.Context, r *http.Request) context.Context { |
| | | username, authinfo := da.CheckAuth(r) |
| | | da.mutex.Lock() |
| | | defer da.mutex.Unlock() |
| | | info := &Info{Username: username, ResponseHeaders: make(http.Header)} |
| | | if username != "" { |
| | | info.Authenticated = true |
| | | info.ResponseHeaders.Set(da.Headers.V().AuthInfo, *authinfo) |
| | | } else { |
| | | // return back digest WWW-Authenticate header |
| | | if len(da.clients) > da.ClientCacheSize+da.ClientCacheTolerance { |
| | | da.purgeLocked(da.ClientCacheTolerance * 2) |
| | | } |
| | | nonce := RandomKey() |
| | | da.clients[nonce] = &digestClient{nc: 0, lastSeen: time.Now().UnixNano()} |
| | | info.ResponseHeaders.Set(da.Headers.V().Authenticate, |
| | | fmt.Sprintf(`Digest realm="%s", nonce="%s", opaque="%s", algorithm=MD5, qop="auth"`, |
| | | da.Realm, nonce, da.Opaque)) |
| | | } |
| | | return context.WithValue(ctx, infoKey, info) |
| | | } |
| | | |
| | | // NewDigestAuthenticator generates a new DigestAuth object |
| | | func NewDigestAuthenticator(realm string, secrets SecretProvider) *DigestAuth { |
| | | da := &DigestAuth{ |
| | | Opaque: RandomKey(), |
| | | Realm: realm, |
| | | Secrets: secrets, |
| | | PlainTextSecrets: true, |
| | | ClientCacheSize: DefaultClientCacheSize, |
| | | ClientCacheTolerance: DefaultClientCacheTolerance, |
| | | clients: map[string]*digestClient{}} |
| | | return da |
| | | } |
New file |
| | |
| | | package auth |
| | | |
| | | import "crypto/md5" |
| | | |
| | | const itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" |
| | | |
| | | var md5CryptSwaps = [16]int{12, 6, 0, 13, 7, 1, 14, 8, 2, 15, 9, 3, 5, 10, 4, 11} |
| | | |
| | | // MD5Crypt is the MD5 password crypt implementation. |
| | | func MD5Crypt(password, salt, magic []byte) []byte { |
| | | d := md5.New() |
| | | |
| | | d.Write(password) |
| | | d.Write(magic) |
| | | d.Write(salt) |
| | | |
| | | d2 := md5.New() |
| | | d2.Write(password) |
| | | d2.Write(salt) |
| | | d2.Write(password) |
| | | |
| | | for i, mixin := 0, d2.Sum(nil); i < len(password); i++ { |
| | | d.Write([]byte{mixin[i%16]}) |
| | | } |
| | | |
| | | for i := len(password); i != 0; i >>= 1 { |
| | | if i&1 == 0 { |
| | | d.Write([]byte{password[0]}) |
| | | } else { |
| | | d.Write([]byte{0}) |
| | | } |
| | | } |
| | | |
| | | final := d.Sum(nil) |
| | | |
| | | for i := 0; i < 1000; i++ { |
| | | d2 := md5.New() |
| | | if i&1 == 0 { |
| | | d2.Write(final) |
| | | } else { |
| | | d2.Write(password) |
| | | } |
| | | |
| | | if i%3 != 0 { |
| | | d2.Write(salt) |
| | | } |
| | | |
| | | if i%7 != 0 { |
| | | d2.Write(password) |
| | | } |
| | | |
| | | if i&1 == 0 { |
| | | d2.Write(password) |
| | | } else { |
| | | d2.Write(final) |
| | | } |
| | | final = d2.Sum(nil) |
| | | } |
| | | |
| | | result := make([]byte, 0, 22) |
| | | v := uint(0) |
| | | bits := uint(0) |
| | | for _, i := range md5CryptSwaps { |
| | | v |= (uint(final[i]) << bits) |
| | | for bits = bits + 8; bits > 6; bits -= 6 { |
| | | result = append(result, itoa64[v&0x3f]) |
| | | v >>= 6 |
| | | } |
| | | } |
| | | result = append(result, itoa64[v&0x3f]) |
| | | |
| | | return append(append(append(magic, salt...), '$'), result...) |
| | | } |
New file |
| | |
| | | package auth |
| | | |
| | | import ( |
| | | "bytes" |
| | | "crypto/md5" |
| | | "crypto/rand" |
| | | "encoding/base64" |
| | | "fmt" |
| | | "net/http" |
| | | "strings" |
| | | ) |
| | | |
| | | // RandomKey returns a random 16-byte base64 alphabet string |
| | | func RandomKey() string { |
| | | k := make([]byte, 12) |
| | | for bytes := 0; bytes < len(k); { |
| | | n, err := rand.Read(k[bytes:]) |
| | | if err != nil { |
| | | panic("rand.Read() failed") |
| | | } |
| | | bytes += n |
| | | } |
| | | return base64.StdEncoding.EncodeToString(k) |
| | | } |
| | | |
| | | // H function for MD5 algorithm (returns a lower-case hex MD5 digest) |
| | | func H(data string) string { |
| | | digest := md5.New() |
| | | digest.Write([]byte(data)) |
| | | return fmt.Sprintf("%x", digest.Sum(nil)) |
| | | } |
| | | |
| | | // ParseList parses a comma-separated list of values as described by |
| | | // RFC 2068 and returns list elements. |
| | | // |
| | | // Lifted from https://code.google.com/p/gorilla/source/browse/http/parser/parser.go |
| | | // which was ported from urllib2.parse_http_list, from the Python |
| | | // standard library. |
| | | func ParseList(value string) []string { |
| | | var list []string |
| | | var escape, quote bool |
| | | b := new(bytes.Buffer) |
| | | for _, r := range value { |
| | | switch { |
| | | case escape: |
| | | b.WriteRune(r) |
| | | escape = false |
| | | case quote: |
| | | if r == '\\' { |
| | | escape = true |
| | | } else { |
| | | if r == '"' { |
| | | quote = false |
| | | } |
| | | b.WriteRune(r) |
| | | } |
| | | case r == ',': |
| | | list = append(list, strings.TrimSpace(b.String())) |
| | | b.Reset() |
| | | case r == '"': |
| | | quote = true |
| | | b.WriteRune(r) |
| | | default: |
| | | b.WriteRune(r) |
| | | } |
| | | } |
| | | // Append last part. |
| | | if s := b.String(); s != "" { |
| | | list = append(list, strings.TrimSpace(s)) |
| | | } |
| | | return list |
| | | } |
| | | |
| | | // ParsePairs extracts key/value pairs from a comma-separated list of |
| | | // values as described by RFC 2068 and returns a map[key]value. The |
| | | // resulting values are unquoted. If a list element doesn't contain a |
| | | // "=", the key is the element itself and the value is an empty |
| | | // string. |
| | | // |
| | | // Lifted from https://code.google.com/p/gorilla/source/browse/http/parser/parser.go |
| | | func ParsePairs(value string) map[string]string { |
| | | m := make(map[string]string) |
| | | for _, pair := range ParseList(strings.TrimSpace(value)) { |
| | | switch i := strings.Index(pair, "="); { |
| | | case i < 0: |
| | | // No '=' in pair, treat whole string as a 'key'. |
| | | m[pair] = "" |
| | | case i == len(pair)-1: |
| | | // Malformed pair ('key=' with no value), keep key with empty value. |
| | | m[pair[:i]] = "" |
| | | default: |
| | | v := pair[i+1:] |
| | | if v[0] == '"' && v[len(v)-1] == '"' { |
| | | // Unquote it. |
| | | v = v[1 : len(v)-1] |
| | | } |
| | | m[pair[:i]] = v |
| | | } |
| | | } |
| | | return m |
| | | } |
| | | |
| | | // Headers contains header and error codes used by authenticator. |
| | | type Headers struct { |
| | | Authenticate string // WWW-Authenticate |
| | | Authorization string // Authorization |
| | | AuthInfo string // Authentication-Info |
| | | UnauthCode int // 401 |
| | | UnauthContentType string // text/plain |
| | | UnauthResponse string // Unauthorized. |
| | | } |
| | | |
| | | // V returns NormalHeaders when h is nil, or h otherwise. Allows to |
| | | // use uninitialized *Headers values in structs. |
| | | func (h *Headers) V() *Headers { |
| | | if h == nil { |
| | | return NormalHeaders |
| | | } |
| | | return h |
| | | } |
| | | |
| | | var ( |
| | | // NormalHeaders are the regular Headers used by an HTTP Server for |
| | | // request authentication. |
| | | NormalHeaders = &Headers{ |
| | | Authenticate: "WWW-Authenticate", |
| | | Authorization: "Authorization", |
| | | AuthInfo: "Authentication-Info", |
| | | UnauthCode: http.StatusUnauthorized, |
| | | UnauthContentType: "text/plain", |
| | | UnauthResponse: fmt.Sprintf("%d %s\n", http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized)), |
| | | } |
| | | |
| | | // ProxyHeaders are Headers used by an HTTP Proxy server for proxy |
| | | // access authentication. |
| | | ProxyHeaders = &Headers{ |
| | | Authenticate: "Proxy-Authenticate", |
| | | Authorization: "Proxy-Authorization", |
| | | AuthInfo: "Proxy-Authentication-Info", |
| | | UnauthCode: http.StatusProxyAuthRequired, |
| | | UnauthContentType: "text/plain", |
| | | UnauthResponse: fmt.Sprintf("%d %s\n", http.StatusProxyAuthRequired, http.StatusText(http.StatusProxyAuthRequired)), |
| | | } |
| | | ) |
| | | |
| | | const contentType = "Content-Type" |
New file |
| | |
| | | package auth |
| | | |
| | | import ( |
| | | "encoding/csv" |
| | | "os" |
| | | "sync" |
| | | ) |
| | | |
| | | // SecretProvider is used by authenticators. Takes user name and realm |
| | | // as an argument, returns secret required for authentication (HA1 for |
| | | // digest authentication, properly encrypted password for basic). |
| | | // |
| | | // Returning an empty string means failing the authentication. |
| | | type SecretProvider func(user, realm string) string |
| | | |
| | | // File handles automatic file reloading on changes. |
| | | type File struct { |
| | | Path string |
| | | Info os.FileInfo |
| | | /* must be set in inherited types during initialization */ |
| | | Reload func() |
| | | mu sync.Mutex |
| | | } |
| | | |
| | | // ReloadIfNeeded checks file Stat and calls Reload() if any changes |
| | | // were detected. File mutex is Locked for the duration of Reload() |
| | | // call. |
| | | // |
| | | // This function will panic() if Stat fails. |
| | | func (f *File) ReloadIfNeeded() { |
| | | info, err := os.Stat(f.Path) |
| | | if err != nil { |
| | | panic(err) |
| | | } |
| | | f.mu.Lock() |
| | | defer f.mu.Unlock() |
| | | if f.Info == nil || f.Info.ModTime() != info.ModTime() { |
| | | f.Info = info |
| | | f.Reload() |
| | | } |
| | | } |
| | | |
| | | // HtdigestFile is a File holding htdigest authentication data. |
| | | type HtdigestFile struct { |
| | | // File is used for automatic reloading of the authentication data. |
| | | File |
| | | // Users is a map of realms to users to HA1 digests. |
| | | Users map[string]map[string]string |
| | | mu sync.RWMutex |
| | | } |
| | | |
| | | func reloadHTDigest(hf *HtdigestFile) { |
| | | r, err := os.Open(hf.Path) |
| | | if err != nil { |
| | | panic(err) |
| | | } |
| | | reader := csv.NewReader(r) |
| | | reader.Comma = ':' |
| | | reader.Comment = '#' |
| | | reader.TrimLeadingSpace = true |
| | | |
| | | records, err := reader.ReadAll() |
| | | if err != nil { |
| | | panic(err) |
| | | } |
| | | |
| | | hf.mu.Lock() |
| | | defer hf.mu.Unlock() |
| | | hf.Users = make(map[string]map[string]string) |
| | | for _, record := range records { |
| | | _, exists := hf.Users[record[1]] |
| | | if !exists { |
| | | hf.Users[record[1]] = make(map[string]string) |
| | | } |
| | | hf.Users[record[1]][record[0]] = record[2] |
| | | } |
| | | } |
| | | |
| | | // HtdigestFileProvider is a SecretProvider implementation based on |
| | | // htdigest-formated files. It will automatically reload htdigest file |
| | | // on changes. It panics on syntax errors in htdigest files. |
| | | func HtdigestFileProvider(filename string) SecretProvider { |
| | | hf := &HtdigestFile{File: File{Path: filename}} |
| | | hf.Reload = func() { reloadHTDigest(hf) } |
| | | return func(user, realm string) string { |
| | | hf.ReloadIfNeeded() |
| | | hf.mu.RLock() |
| | | defer hf.mu.RUnlock() |
| | | _, exists := hf.Users[realm] |
| | | if !exists { |
| | | return "" |
| | | } |
| | | digest, exists := hf.Users[realm][user] |
| | | if !exists { |
| | | return "" |
| | | } |
| | | return digest |
| | | } |
| | | } |
| | | |
| | | // HtpasswdFile is a File holding basic authentication data. |
| | | type HtpasswdFile struct { |
| | | // File is used for automatic reloading of the authentication data. |
| | | File |
| | | // Users is a map of users to their secrets (salted encrypted |
| | | // passwords). |
| | | Users map[string]string |
| | | mu sync.RWMutex |
| | | } |
| | | |
| | | func reloadHTPasswd(h *HtpasswdFile) { |
| | | r, err := os.Open(h.Path) |
| | | if err != nil { |
| | | panic(err) |
| | | } |
| | | reader := csv.NewReader(r) |
| | | reader.Comma = ':' |
| | | reader.Comment = '#' |
| | | reader.TrimLeadingSpace = true |
| | | |
| | | records, err := reader.ReadAll() |
| | | if err != nil { |
| | | panic(err) |
| | | } |
| | | |
| | | h.mu.Lock() |
| | | defer h.mu.Unlock() |
| | | h.Users = make(map[string]string) |
| | | for _, record := range records { |
| | | h.Users[record[0]] = record[1] |
| | | } |
| | | } |
| | | |
| | | // HtpasswdFileProvider is a SecretProvider implementation based on |
| | | // htpasswd-formated files. It will automatically reload htpasswd file |
| | | // on changes. It panics on syntax errors in htpasswd files. Realm |
| | | // argument of the SecretProvider is ignored. |
| | | func HtpasswdFileProvider(filename string) SecretProvider { |
| | | h := &HtpasswdFile{File: File{Path: filename}} |
| | | h.Reload = func() { reloadHTPasswd(h) } |
| | | return func(user, realm string) string { |
| | | h.ReloadIfNeeded() |
| | | h.mu.RLock() |
| | | password, exists := h.Users[user] |
| | | h.mu.RUnlock() |
| | | if !exists { |
| | | return "" |
| | | } |
| | | return password |
| | | } |
| | | } |