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
| 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 | ) |