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 }