@marcel91200 / caddy Mirror · read-only
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 11, 01:00 UTC

master
go 321 lines 10.4 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package caddyauth
16
17 import (
18 "encoding/base64"
19 "encoding/hex"
20 "encoding/json"
21 "fmt"
22 weakrand "math/rand/v2"
23 "net/http"
24 "slices"
25 "strings"
26 "sync"
27
28 "golang.org/x/sync/singleflight"
29
30 "github.com/caddyserver/caddy/v2"
31 )
32
33 func init() {
34 caddy.RegisterModule(HTTPBasicAuth{})
35 }
36
37 // HTTPBasicAuth facilitates HTTP basic authentication.
38 type HTTPBasicAuth struct {
39 // The algorithm with which the passwords are hashed. Default: bcrypt
40 HashRaw json.RawMessage `json:"hash,omitempty" caddy:"namespace=http.authentication.hashes inline_key=algorithm"`
41
42 // The list of accounts to authenticate.
43 AccountList []Account `json:"accounts,omitempty"`
44
45 // The name of the realm. Default: restricted
46 Realm string `json:"realm,omitempty"`
47
48 // If non-nil, a mapping of plaintext passwords to their
49 // hashes will be cached in memory (with random eviction).
50 // This can greatly improve the performance of traffic-heavy
51 // servers that use secure password hashing algorithms, with
52 // the downside that plaintext passwords will be stored in
53 // memory for a longer time (this should not be a problem
54 // as long as your machine is not compromised, at which point
55 // all bets are off, since basicauth necessitates plaintext
56 // passwords being received over the wire anyway). Note that
57 // a cache hit does not mean it is a valid password.
58 HashCache *Cache `json:"hash_cache,omitempty"`
59
60 Accounts map[string]Account `json:"-"`
61 Hash Comparer `json:"-"`
62
63 // fakePassword is used when a given user is not found,
64 // so that timing side-channels can be mitigated: it gives
65 // us something to hash and compare even if the user does
66 // not exist, which should have similar timing as a user
67 // account that does exist.
68 fakePassword []byte
69 }
70
71 // CaddyModule returns the Caddy module information.
72 func (HTTPBasicAuth) CaddyModule() caddy.ModuleInfo {
73 return caddy.ModuleInfo{
74 ID: "http.authentication.providers.http_basic",
75 New: func() caddy.Module { return new(HTTPBasicAuth) },
76 }
77 }
78
79 // Provision provisions the HTTP basic auth provider.
80 func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error {
81 if hba.HashRaw == nil {
82 hba.HashRaw = json.RawMessage(`{"algorithm": "bcrypt"}`)
83 }
84
85 // load password hasher
86 hasherIface, err := ctx.LoadModule(hba, "HashRaw")
87 if err != nil {
88 return fmt.Errorf("loading password hasher module: %v", err)
89 }
90 hba.Hash = hasherIface.(Comparer)
91
92 if hba.Hash == nil {
93 return fmt.Errorf("hash is required")
94 }
95
96 // if supported, generate a fake password we can compare against if needed
97 if hasher, ok := hba.Hash.(Hasher); ok {
98 hba.fakePassword = hasher.FakeHash()
99 }
100
101 repl := caddy.NewReplacer()
102
103 // load account list
104 hba.Accounts = make(map[string]Account)
105 for i, acct := range hba.AccountList {
106 acct.Username = repl.ReplaceKnown(acct.Username, "")
107 acct.Password = repl.ReplaceKnown(acct.Password, "")
108
109 if _, ok := hba.Accounts[acct.Username]; ok {
110 return fmt.Errorf("account %d: username is not unique: %s", i, acct.Username)
111 }
112
113 if acct.Username == "" || acct.Password == "" {
114 return fmt.Errorf("account %d: username and password are required", i)
115 }
116
117 // TODO: Remove support for redundantly-encoded b64-encoded hashes
118 // Passwords starting with '$' are likely in Modular Crypt Format,
119 // so we don't need to base64 decode them. But historically, we
120 // required redundant base64, so we try to decode it otherwise.
121 if strings.HasPrefix(acct.Password, "$") {
122 acct.password = []byte(acct.Password)
123 } else {
124 acct.password, err = base64.StdEncoding.DecodeString(acct.Password)
125 if err != nil {
126 return fmt.Errorf("base64-decoding password: %v", err)
127 }
128 }
129
130 hba.Accounts[acct.Username] = acct
131 }
132 hba.AccountList = nil // allow GC to deallocate
133
134 if hba.HashCache != nil {
135 hba.HashCache.cache = make(map[string]bool)
136 hba.HashCache.mu = new(sync.RWMutex)
137 hba.HashCache.g = new(singleflight.Group)
138 }
139
140 return nil
141 }
142
143 // Authenticate validates the user credentials in req and returns the user, if valid.
144 func (hba HTTPBasicAuth) Authenticate(w http.ResponseWriter, req *http.Request) (User, bool, error) {
145 username, plaintextPasswordStr, ok := req.BasicAuth()
146 if !ok {
147 return hba.promptForCredentials(w, nil)
148 }
149
150 account, accountExists := hba.Accounts[username]
151 if !accountExists {
152 // don't return early if account does not exist; we want
153 // to try to avoid side-channels that leak existence, so
154 // we use a fake password to simulate realistic CPU cycles
155 account.password = hba.fakePassword
156 }
157
158 same, err := hba.correctPassword(account, []byte(plaintextPasswordStr))
159 if err != nil || !same || !accountExists {
160 return hba.promptForCredentials(w, err)
161 }
162
163 return User{ID: username}, true, nil
164 }
165
166 func (hba HTTPBasicAuth) correctPassword(account Account, plaintextPassword []byte) (bool, error) {
167 compare := func() (bool, error) {
168 return hba.Hash.Compare(account.password, plaintextPassword)
169 }
170
171 // if no caching is enabled, simply return the result of hashing + comparing
172 if hba.HashCache == nil {
173 return compare()
174 }
175
176 // compute a cache key that is unique for these input parameters
177 // (build it in a new buffer: appending to account.password could write
178 // into its spare capacity, which is shared by all concurrent requests)
179 cacheKey := hex.EncodeToString(slices.Concat(account.password, plaintextPassword))
180
181 // fast track: if the result of the input is already cached, use it
182 hba.HashCache.mu.RLock()
183 same, ok := hba.HashCache.cache[cacheKey]
184 hba.HashCache.mu.RUnlock()
185 if ok {
186 return same, nil
187 }
188 // slow track: do the expensive op, then add it to the cache
189 // but perform it in a singleflight group so that multiple
190 // parallel requests using the same password don't cause a
191 // thundering herd problem by all performing the same hashing
192 // operation before the first one finishes and caches it.
193 v, err, _ := hba.HashCache.g.Do(cacheKey, func() (any, error) {
194 return compare()
195 })
196 if err != nil {
197 return false, err
198 }
199 same = v.(bool)
200 hba.HashCache.mu.Lock()
201 if len(hba.HashCache.cache) >= 1000 {
202 hba.HashCache.makeRoom() // keep cache size under control
203 }
204 hba.HashCache.cache[cacheKey] = same
205 hba.HashCache.mu.Unlock()
206
207 return same, nil
208 }
209
210 func (hba HTTPBasicAuth) promptForCredentials(w http.ResponseWriter, err error) (User, bool, error) {
211 // browsers show a message that says something like:
212 // "The website says: <realm>"
213 // which is kinda dumb, but whatever.
214 realm := hba.Realm
215 if realm == "" {
216 realm = "restricted"
217 }
218 w.Header().Set("Www-Authenticate", fmt.Sprintf(`Basic realm="%s"`, realm))
219 return User{}, false, err
220 }
221
222 // Cache enables caching of basic auth results. This is especially
223 // helpful for secure password hashes which can be expensive to
224 // compute on every HTTP request.
225 type Cache struct {
226 mu *sync.RWMutex
227 g *singleflight.Group
228
229 // map of concatenated hashed password + plaintext password, to result
230 cache map[string]bool
231 }
232
233 // makeRoom deletes about 1/10 of the items in the cache
234 // in order to keep its size under control. It must not be
235 // called without a lock on c.mu.
236 func (c *Cache) makeRoom() {
237 // we delete more than just 1 entry so that we don't have
238 // to do this on every request; assuming the capacity of
239 // the cache is on a long tail, we can save a lot of CPU
240 // time by doing a whole bunch of deletions now and then
241 // we won't have to do them again for a while
242 numToDelete := max(len(c.cache)/10, 1)
243 for deleted := 0; deleted <= numToDelete; deleted++ {
244 // Go maps are "nondeterministic" not actually random,
245 // so although we could just chop off the "front" of the
246 // map with less code, this is a heavily skewed eviction
247 // strategy; generating random numbers is cheap and
248 // ensures a much better distribution.
249 //nolint:gosec
250 rnd := weakrand.IntN(len(c.cache))
251 i := 0
252 for key := range c.cache {
253 if i == rnd {
254 delete(c.cache, key)
255 break
256 }
257 i++
258 }
259 }
260 }
261
262 // Comparer is a type that can securely compare
263 // a plaintext password with a hashed password
264 // in constant-time. Comparers should hash the
265 // plaintext password and then use constant-time
266 // comparison.
267 type Comparer interface {
268 // Compare returns true if the result of hashing
269 // plaintextPassword is hashedPassword, false
270 // otherwise. An error is returned only if
271 // there is a technical/configuration error.
272 Compare(hashedPassword, plaintextPassword []byte) (bool, error)
273 }
274
275 // Hasher is a type that can generate a secure hash
276 // given a plaintext. Hashing modules which implement
277 // this interface can be used with the hash-password
278 // subcommand as well as benefitting from anti-timing
279 // features. A hasher also returns a fake hash which
280 // can be used for timing side-channel mitigation.
281 type Hasher interface {
282 Hash(plaintext []byte) ([]byte, error)
283 FakeHash() []byte
284 }
285
286 // hashesNamespace is the module namespace of password hashing modules.
287 const hashesNamespace = "http.authentication.hashes"
288
289 // hashModuleInfo returns the module info of the installed password
290 // hashing module with the given name (e.g. "bcrypt"). If no such module
291 // is installed, the returned error lists the available algorithms.
292 func hashModuleInfo(name string) (caddy.ModuleInfo, error) {
293 mods := caddy.GetModules(hashesNamespace)
294 names := make([]string, 0, len(mods))
295 for _, mod := range mods {
296 if mod.ID.Name() == name {
297 return mod, nil
298 }
299 names = append(names, mod.ID.Name())
300 }
301 return caddy.ModuleInfo{}, fmt.Errorf("unrecognized hash algorithm: %s (available: %s)",
302 name, strings.Join(names, ", "))
303 }
304
305 // Account contains a username and password.
306 type Account struct {
307 // A user's username.
308 Username string `json:"username"`
309
310 // The user's hashed password, in Modular Crypt Format (with `$` prefix)
311 // or base64-encoded.
312 Password string `json:"password"` //nolint:gosec // false positive, this is a hashed password
313
314 password []byte
315 }
316
317 // Interface guards
318 var (
319 _ caddy.Provisioner = (*HTTPBasicAuth)(nil)
320 _ Authenticator = (*HTTPBasicAuth)(nil)
321 )