1// Copyright 2020 The Prometheus Authors
2// This code is partly borrowed from Caddy:
3//    Copyright 2015 Matthew Holt and The Caddy Authors
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16package web
17
18import (
19	"encoding/hex"
20	"net/http"
21	"sync"
22
23	"github.com/go-kit/log"
24	"golang.org/x/crypto/bcrypt"
25)
26
27func validateUsers(configPath string) error {
28	c, err := getConfig(configPath)
29	if err != nil {
30		return err
31	}
32
33	for _, p := range c.Users {
34		_, err = bcrypt.Cost([]byte(p))
35		if err != nil {
36			return err
37		}
38	}
39
40	return nil
41}
42
43type userAuthRoundtrip struct {
44	tlsConfigPath string
45	handler       http.Handler
46	logger        log.Logger
47	cache         *cache
48	// bcryptMtx is there to ensure that bcrypt.CompareHashAndPassword is run
49	// only once in parallel as this is CPU intensive.
50	bcryptMtx sync.Mutex
51}
52
53func (u *userAuthRoundtrip) ServeHTTP(w http.ResponseWriter, r *http.Request) {
54	c, err := getConfig(u.tlsConfigPath)
55	if err != nil {
56		u.logger.Log("msg", "Unable to parse configuration", "err", err)
57		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
58		return
59	}
60
61	if len(c.Users) == 0 {
62		u.handler.ServeHTTP(w, r)
63		return
64	}
65
66	user, pass, auth := r.BasicAuth()
67	if auth {
68		hashedPassword, validUser := c.Users[user]
69
70		if !validUser {
71			// The user is not found. Use a fixed password hash to
72			// prevent user enumeration by timing requests.
73			// This is a bcrypt-hashed version of "fakepassword".
74			hashedPassword = "$2y$10$QOauhQNbBCuQDKes6eFzPeMqBSjb7Mr5DUmpZ/VcEd00UAV/LDeSi"
75		}
76
77		cacheKey := hex.EncodeToString(append(append([]byte(user), []byte(hashedPassword)...), []byte(pass)...))
78		authOk, ok := u.cache.get(cacheKey)
79
80		if !ok {
81			// This user, hashedPassword, password is not cached.
82			u.bcryptMtx.Lock()
83			err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(pass))
84			u.bcryptMtx.Unlock()
85
86			authOk = err == nil
87			u.cache.set(cacheKey, authOk)
88		}
89
90		if authOk && validUser {
91			u.handler.ServeHTTP(w, r)
92			return
93		}
94	}
95
96	w.Header().Set("WWW-Authenticate", "Basic")
97	http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
98}
99