summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatt Holt <mholt@users.noreply.github.com>2020-06-01 23:56:47 -0600
committerGitHub <noreply@github.com>2020-06-01 23:56:47 -0600
commit9a7756c6e4b4ddb945bede3ddb2dfbf241208915 (patch)
treec13b241c6caefff0fce9b08084ae63b013f345a0
parentfdf2a77feb53cb9dd6de772f811e96a5f6b2d2c4 (diff)
caddyauth: Cache basicauth results (fixes #3462) (#3465)
Cache capacity is currently hard-coded at 1000 with random eviction. It is enabled by default from Caddyfile configurations because I assume this is the most common preference.
-rw-r--r--modules/caddyhttp/caddyauth/basicauth.go104
-rw-r--r--modules/caddyhttp/caddyauth/caddyfile.go1
2 files changed, 102 insertions, 3 deletions
diff --git a/modules/caddyhttp/caddyauth/basicauth.go b/modules/caddyhttp/caddyauth/basicauth.go
index d709f94..92e1683 100644
--- a/modules/caddyhttp/caddyauth/basicauth.go
+++ b/modules/caddyhttp/caddyauth/basicauth.go
@@ -16,15 +16,21 @@ package caddyauth
import (
"encoding/base64"
+ "encoding/hex"
"encoding/json"
"fmt"
+ weakrand "math/rand"
"net/http"
+ "sync"
+ "time"
"github.com/caddyserver/caddy/v2"
)
func init() {
caddy.RegisterModule(HTTPBasicAuth{})
+
+ weakrand.Seed(time.Now().UnixNano())
}
// HTTPBasicAuth facilitates HTTP basic authentication.
@@ -38,6 +44,17 @@ type HTTPBasicAuth struct {
// The name of the realm. Default: restricted
Realm string `json:"realm,omitempty"`
+ // If non-nil, a mapping of plaintext passwords to their
+ // hashes will be cached in memory (with random eviction).
+ // This can greatly improve the performance of traffic-heavy
+ // servers that use secure password hashing algorithms, with
+ // the downside that plaintext passwords will be stored in
+ // memory for a longer time (this should not be a problem
+ // as long as your machine is not compromised, at which point
+ // all bets are off, since basicauth necessitates plaintext
+ // passwords being received over the wire anyway).
+ HashCache *Cache `json:"hash_cache,omitempty"`
+
Accounts map[string]Account `json:"-"`
Hash Comparer `json:"-"`
}
@@ -99,6 +116,11 @@ func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error {
}
hba.AccountList = nil // allow GC to deallocate
+ if hba.HashCache != nil {
+ hba.HashCache.cache = make(map[string]bool)
+ hba.HashCache.mu = new(sync.Mutex)
+ }
+
return nil
}
@@ -109,13 +131,11 @@ func (hba HTTPBasicAuth) Authenticate(w http.ResponseWriter, req *http.Request)
return hba.promptForCredentials(w, nil)
}
- plaintextPassword := []byte(plaintextPasswordStr)
-
account, accountExists := hba.Accounts[username]
// don't return early if account does not exist; we want
// to try to avoid side-channels that leak existence
- same, err := hba.Hash.Compare(account.password, plaintextPassword, account.salt)
+ same, err := hba.correctPassword(account, []byte(plaintextPasswordStr))
if err != nil {
return hba.promptForCredentials(w, err)
}
@@ -126,6 +146,43 @@ func (hba HTTPBasicAuth) Authenticate(w http.ResponseWriter, req *http.Request)
return User{ID: username}, true, nil
}
+func (hba HTTPBasicAuth) correctPassword(account Account, plaintextPassword []byte) (bool, error) {
+ compare := func() (bool, error) {
+ return hba.Hash.Compare(account.password, plaintextPassword, account.salt)
+ }
+
+ // if no caching is enabled, simply return the result of hashing + comparing
+ if hba.HashCache == nil {
+ return compare()
+ }
+
+ // compute a cache key that is unique for these input parameters
+ cacheKey := hex.EncodeToString(append(append(account.password, account.salt...), plaintextPassword...))
+
+ // fast track: if the result of the input is already cached, use it
+ hba.HashCache.mu.Lock()
+ same, ok := hba.HashCache.cache[cacheKey]
+ if ok {
+ hba.HashCache.mu.Unlock()
+ return same, nil
+ }
+ hba.HashCache.mu.Unlock()
+
+ // slow track: do the expensive op, then add it to the cache
+ same, err := compare()
+ if err != nil {
+ return false, err
+ }
+ hba.HashCache.mu.Lock()
+ if len(hba.HashCache.cache) >= 1000 {
+ hba.HashCache.makeRoom() // keep cache size under control
+ }
+ hba.HashCache.cache[cacheKey] = same
+ hba.HashCache.mu.Unlock()
+
+ return same, nil
+}
+
func (hba HTTPBasicAuth) promptForCredentials(w http.ResponseWriter, err error) (User, bool, error) {
// browsers show a message that says something like:
// "The website says: <realm>"
@@ -138,6 +195,47 @@ func (hba HTTPBasicAuth) promptForCredentials(w http.ResponseWriter, err error)
return User{}, false, err
}
+// Cache enables caching of basic auth results. This is especially
+// helpful for secure password hashes which can be expensive to
+// compute on every HTTP request.
+type Cache struct {
+ mu *sync.Mutex
+
+ // map of concatenated hashed password + plaintext password + salt, to result
+ cache map[string]bool
+}
+
+// makeRoom deletes about 1/10 of the items in the cache
+// in order to keep its size under control. It must not be
+// called without a lock on c.mu.
+func (c *Cache) makeRoom() {
+ // we delete more than just 1 entry so that we don't have
+ // to do this on every request; assuming the capacity of
+ // the cache is on a long tail, we can save a lot of CPU
+ // time by doing a whole bunch of deletions now and then
+ // we won't have to do them again for a while
+ numToDelete := len(c.cache) / 10
+ if numToDelete < 1 {
+ numToDelete = 1
+ }
+ for deleted := 0; deleted <= numToDelete; deleted++ {
+ // Go maps are "nondeterministic" not actually random,
+ // so although we could just chop off the "front" of the
+ // map with less code, this is a heavily skewed eviction
+ // strategy; generating random numbers is cheap and
+ // ensures a much better distribution.
+ rnd := weakrand.Intn(len(c.cache))
+ i := 0
+ for key := range c.cache {
+ if i == rnd {
+ delete(c.cache, key)
+ break
+ }
+ i++
+ }
+ }
+}
+
// Comparer is a type that can securely compare
// a plaintext password with a hashed password
// in constant-time. Comparers should hash the
diff --git a/modules/caddyhttp/caddyauth/caddyfile.go b/modules/caddyhttp/caddyauth/caddyfile.go
index 9fe8a80..13e78fc 100644
--- a/modules/caddyhttp/caddyauth/caddyfile.go
+++ b/modules/caddyhttp/caddyauth/caddyfile.go
@@ -35,6 +35,7 @@ func init() {
// If no hash algorithm is supplied, bcrypt will be assumed.
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
var ba HTTPBasicAuth
+ ba.HashCache = new(Cache)
for h.Next() {
var cmp Comparer