@marcel91200 / caddy Mirror · read-only / commits / 9a7756c6
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 14, 05:15 UTC

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.

Matt Holt committed Jun 1, 2020 at 23:56 UTC 9a7756c6e4b4ddb945bede3ddb2dfbf241208915
2 files changed +102 -3
modules/caddyhttp/caddyauth/basicauth.go
+101 -3
@@ -16,15 +16,21 @@ package caddyauth
16
17 import (
18 "encoding/base64"
19 + "encoding/hex"
20 "encoding/json"
21 "fmt"
22 + weakrand "math/rand"
23 "net/http"
24 + "sync"
25 + "time"
26
27 "github.com/caddyserver/caddy/v2"
28 )
29
30 func init() {
31 caddy.RegisterModule(HTTPBasicAuth{})
32 +
33 + weakrand.Seed(time.Now().UnixNano())
34 }
35
36 // HTTPBasicAuth facilitates HTTP basic authentication.
@@ -38,6 +44,17 @@ type HTTPBasicAuth struct {
44 // The name of the realm. Default: restricted
45 Realm string `json:"realm,omitempty"`
46
47 + // If non-nil, a mapping of plaintext passwords to their
48 + // hashes will be cached in memory (with random eviction).
49 + // This can greatly improve the performance of traffic-heavy
50 + // servers that use secure password hashing algorithms, with
51 + // the downside that plaintext passwords will be stored in
52 + // memory for a longer time (this should not be a problem
53 + // as long as your machine is not compromised, at which point
54 + // all bets are off, since basicauth necessitates plaintext
55 + // passwords being received over the wire anyway).
56 + HashCache *Cache `json:"hash_cache,omitempty"`
57 +
58 Accounts map[string]Account `json:"-"`
59 Hash Comparer `json:"-"`
60 }
@@ -99,6 +116,11 @@ func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error {
116 }
117 hba.AccountList = nil // allow GC to deallocate
118
119 + if hba.HashCache != nil {
120 + hba.HashCache.cache = make(map[string]bool)
121 + hba.HashCache.mu = new(sync.Mutex)
122 + }
123 +
124 return nil
125 }
126
@@ -109,13 +131,11 @@ func (hba HTTPBasicAuth) Authenticate(w http.ResponseWriter, req *http.Request)
131 return hba.promptForCredentials(w, nil)
132 }
133
112 - plaintextPassword := []byte(plaintextPasswordStr)
113 -
134 account, accountExists := hba.Accounts[username]
135 // don't return early if account does not exist; we want
136 // to try to avoid side-channels that leak existence
137
118 - same, err := hba.Hash.Compare(account.password, plaintextPassword, account.salt)
138 + same, err := hba.correctPassword(account, []byte(plaintextPasswordStr))
139 if err != nil {
140 return hba.promptForCredentials(w, err)
141 }
@@ -126,6 +146,43 @@ func (hba HTTPBasicAuth) Authenticate(w http.ResponseWriter, req *http.Request)
146 return User{ID: username}, true, nil
147 }
148
149 +func (hba HTTPBasicAuth) correctPassword(account Account, plaintextPassword []byte) (bool, error) {
150 + compare := func() (bool, error) {
151 + return hba.Hash.Compare(account.password, plaintextPassword, account.salt)
152 + }
153 +
154 + // if no caching is enabled, simply return the result of hashing + comparing
155 + if hba.HashCache == nil {
156 + return compare()
157 + }
158 +
159 + // compute a cache key that is unique for these input parameters
160 + cacheKey := hex.EncodeToString(append(append(account.password, account.salt...), plaintextPassword...))
161 +
162 + // fast track: if the result of the input is already cached, use it
163 + hba.HashCache.mu.Lock()
164 + same, ok := hba.HashCache.cache[cacheKey]
165 + if ok {
166 + hba.HashCache.mu.Unlock()
167 + return same, nil
168 + }
169 + hba.HashCache.mu.Unlock()
170 +
171 + // slow track: do the expensive op, then add it to the cache
172 + same, err := compare()
173 + if err != nil {
174 + return false, err
175 + }
176 + hba.HashCache.mu.Lock()
177 + if len(hba.HashCache.cache) >= 1000 {
178 + hba.HashCache.makeRoom() // keep cache size under control
179 + }
180 + hba.HashCache.cache[cacheKey] = same
181 + hba.HashCache.mu.Unlock()
182 +
183 + return same, nil
184 +}
185 +
186 func (hba HTTPBasicAuth) promptForCredentials(w http.ResponseWriter, err error) (User, bool, error) {
187 // browsers show a message that says something like:
188 // "The website says: <realm>"
@@ -138,6 +195,47 @@ func (hba HTTPBasicAuth) promptForCredentials(w http.ResponseWriter, err error)
195 return User{}, false, err
196 }
197
198 +// Cache enables caching of basic auth results. This is especially
199 +// helpful for secure password hashes which can be expensive to
200 +// compute on every HTTP request.
201 +type Cache struct {
202 + mu *sync.Mutex
203 +
204 + // map of concatenated hashed password + plaintext password + salt, to result
205 + cache map[string]bool
206 +}
207 +
208 +// makeRoom deletes about 1/10 of the items in the cache
209 +// in order to keep its size under control. It must not be
210 +// called without a lock on c.mu.
211 +func (c *Cache) makeRoom() {
212 + // we delete more than just 1 entry so that we don't have
213 + // to do this on every request; assuming the capacity of
214 + // the cache is on a long tail, we can save a lot of CPU
215 + // time by doing a whole bunch of deletions now and then
216 + // we won't have to do them again for a while
217 + numToDelete := len(c.cache) / 10
218 + if numToDelete < 1 {
219 + numToDelete = 1
220 + }
221 + for deleted := 0; deleted <= numToDelete; deleted++ {
222 + // Go maps are "nondeterministic" not actually random,
223 + // so although we could just chop off the "front" of the
224 + // map with less code, this is a heavily skewed eviction
225 + // strategy; generating random numbers is cheap and
226 + // ensures a much better distribution.
227 + rnd := weakrand.Intn(len(c.cache))
228 + i := 0
229 + for key := range c.cache {
230 + if i == rnd {
231 + delete(c.cache, key)
232 + break
233 + }
234 + i++
235 + }
236 + }
237 +}
238 +
239 // Comparer is a type that can securely compare
240 // a plaintext password with a hashed password
241 // in constant-time. Comparers should hash the
modules/caddyhttp/caddyauth/caddyfile.go
+1
@@ -35,6 +35,7 @@ func init() {
35 // If no hash algorithm is supplied, bcrypt will be assumed.
36 func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
37 var ba HTTPBasicAuth
38 + ba.HashCache = new(Cache)
39
40 for h.Next() {
41 var cmp Comparer