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