Skip to content

Commit c184361

Browse files
committed
Add path exclusion support to BasicAuth authentication
Signed-off-by: Kacper Rzetelski <[email protected]>
1 parent ad41e17 commit c184361

17 files changed

+992
-229
lines changed

docs/web-configuration.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ http_server_config:
125125
# required. Passwords are hashed with bcrypt.
126126
basic_auth_users:
127127
[ <string>: <secret> ... ]
128+
129+
# A list of HTTP paths to be excepted from authentication.
130+
auth_excluded_paths:
131+
[ - <string> ]
128132
```
129133

130134
[A sample configuration file](web-config.yml) is provided.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2023 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package authentication
15+
16+
import (
17+
"log/slog"
18+
"net/http"
19+
)
20+
21+
type Authenticator interface {
22+
Authenticate(*http.Request) (bool, string, error)
23+
}
24+
25+
type AuthenticatorFunc func(r *http.Request) (bool, string, error)
26+
27+
func (f AuthenticatorFunc) Authenticate(r *http.Request) (bool, string, error) {
28+
return f(r)
29+
}
30+
31+
func WithAuthentication(handler http.Handler, authenticator Authenticator, logger *slog.Logger) http.Handler {
32+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
33+
ok, reason, err := authenticator.Authenticate(r)
34+
if err != nil {
35+
logger.Error("Unable to authenticate", "URI", r.RequestURI, "err", err.Error())
36+
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
37+
return
38+
}
39+
40+
if ok {
41+
handler.ServeHTTP(w, r)
42+
return
43+
}
44+
45+
logger.Warn("Unauthenticated request", "URI", r.RequestURI, "reason", reason)
46+
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
47+
})
48+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright 2023 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package authentication
15+
16+
import (
17+
"errors"
18+
"net/http"
19+
"net/http/httptest"
20+
"testing"
21+
22+
"github.com/prometheus/exporter-toolkit/web/authentication/testhelpers"
23+
)
24+
25+
func TestWithAuthentication(t *testing.T) {
26+
t.Parallel()
27+
28+
logger := testhelpers.NewNoOpLogger()
29+
30+
tt := []struct {
31+
Name string
32+
Authenticator Authenticator
33+
ExpectedStatusCode int
34+
}{
35+
{
36+
Name: "Accepting authenticator",
37+
Authenticator: AuthenticatorFunc(func(_ *http.Request) (bool, string, error) {
38+
return true, "", nil
39+
}),
40+
ExpectedStatusCode: http.StatusOK,
41+
},
42+
{
43+
Name: "Denying authenticator",
44+
Authenticator: AuthenticatorFunc(func(_ *http.Request) (bool, string, error) {
45+
return false, "", nil
46+
}),
47+
ExpectedStatusCode: http.StatusUnauthorized,
48+
},
49+
{
50+
Name: "Erroring authenticator",
51+
Authenticator: AuthenticatorFunc(func(_ *http.Request) (bool, string, error) {
52+
return false, "", errors.New("error authenticating")
53+
}),
54+
ExpectedStatusCode: http.StatusInternalServerError,
55+
},
56+
}
57+
58+
for _, tc := range tt {
59+
t.Run(tc.Name, func(t *testing.T) {
60+
t.Parallel()
61+
62+
req := testhelpers.MakeDefaultRequest(t)
63+
64+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
65+
w.WriteHeader(http.StatusOK)
66+
})
67+
68+
rr := httptest.NewRecorder()
69+
authHandler := WithAuthentication(handler, tc.Authenticator, logger)
70+
authHandler.ServeHTTP(rr, req)
71+
got := rr.Result()
72+
73+
if tc.ExpectedStatusCode != got.StatusCode {
74+
t.Errorf("Expected status code %q, got %q", tc.ExpectedStatusCode, got.Status)
75+
}
76+
})
77+
}
78+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright 2023 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package basicauth
15+
16+
import (
17+
"encoding/hex"
18+
"net/http"
19+
"strings"
20+
"sync"
21+
22+
"github.com/prometheus/common/config"
23+
"github.com/prometheus/exporter-toolkit/web/authentication"
24+
"golang.org/x/crypto/bcrypt"
25+
)
26+
27+
type BasicAuthAuthenticator struct {
28+
users map[string]config.Secret
29+
30+
cache *cache
31+
// bcryptMtx is there to ensure that bcrypt.CompareHashAndPassword is run
32+
// only once in parallel as this is CPU intensive.
33+
bcryptMtx sync.Mutex
34+
}
35+
36+
func (b *BasicAuthAuthenticator) Authenticate(r *http.Request) (bool, string, error) {
37+
user, pass, auth := r.BasicAuth()
38+
39+
if !auth {
40+
return false, "No credentials in request", nil
41+
}
42+
43+
hashedPassword, validUser := b.users[user]
44+
45+
if !validUser {
46+
// The user is not found. Use a fixed password hash to
47+
// prevent user enumeration by timing requests.
48+
// This is a bcrypt-hashed version of "fakepassword".
49+
hashedPassword = "$2y$10$QOauhQNbBCuQDKes6eFzPeMqBSjb7Mr5DUmpZ/VcEd00UAV/LDeSi"
50+
}
51+
52+
cacheKey := strings.Join(
53+
[]string{
54+
hex.EncodeToString([]byte(user)),
55+
hex.EncodeToString([]byte(hashedPassword)),
56+
hex.EncodeToString([]byte(pass)),
57+
}, ":")
58+
authOk, ok := b.cache.get(cacheKey)
59+
60+
if !ok {
61+
// This user, hashedPassword, password is not cached.
62+
b.bcryptMtx.Lock()
63+
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(pass))
64+
b.bcryptMtx.Unlock()
65+
66+
authOk = validUser && err == nil
67+
b.cache.set(cacheKey, authOk)
68+
}
69+
70+
if authOk && validUser {
71+
return true, "", nil
72+
}
73+
74+
return false, "Invalid credentials", nil
75+
}
76+
77+
func NewBasicAuthAuthenticator(users map[string]config.Secret) authentication.Authenticator {
78+
return &BasicAuthAuthenticator{
79+
cache: newCache(),
80+
users: users,
81+
}
82+
}
83+
84+
var _ authentication.Authenticator = &BasicAuthAuthenticator{}

0 commit comments

Comments
 (0)