Skip to content

Commit afa4616

Browse files
committed
Add path exclusion support to BasicAuth authentication
1 parent a7dd218 commit afa4616

16 files changed

+961
-229
lines changed

docs/web-configuration.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ http_server_config:
107107
# required. Passwords are hashed with bcrypt.
108108
basic_auth_users:
109109
[ <string>: <secret> ... ]
110+
111+
# A list of HTTP paths to be excepted from authentication.
112+
auth_excluded_paths:
113+
[ - <string> ]
110114
```
111115

112116
[A sample configuration file](web-config.yml) is provided.

web/authentication/authenticator.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
"net/http"
18+
19+
"github.com/go-kit/log"
20+
"github.com/go-kit/log/level"
21+
)
22+
23+
type Authenticator interface {
24+
Authenticate(*http.Request) (bool, string, error)
25+
}
26+
27+
type AuthenticatorFunc func(r *http.Request) (bool, string, error)
28+
29+
func (f AuthenticatorFunc) Authenticate(r *http.Request) (bool, string, error) {
30+
return f(r)
31+
}
32+
33+
func WithAuthentication(handler http.Handler, authenticator Authenticator, logger log.Logger) http.Handler {
34+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
35+
ok, reason, err := authenticator.Authenticate(r)
36+
if err != nil {
37+
level.Error(logger).Log("msg", "Error authenticating", "URI", r.RequestURI, "err", err)
38+
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
39+
return
40+
}
41+
42+
if ok {
43+
handler.ServeHTTP(w, r)
44+
return
45+
}
46+
47+
level.Warn(logger).Log("msg", "Unauthenticated request", "URI", r.RequestURI, "reason", reason)
48+
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
49+
})
50+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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/go-kit/log"
23+
)
24+
25+
func TestWithAuthentication(t *testing.T) {
26+
logger := &noOpLogger{}
27+
28+
ts := []struct {
29+
Name string
30+
Authenticator Authenticator
31+
ExpectedStatusCode int
32+
}{
33+
{
34+
Name: "Accepting authenticator",
35+
Authenticator: AuthenticatorFunc(func(_ *http.Request) (bool, string, error) {
36+
return true, "", nil
37+
}),
38+
ExpectedStatusCode: http.StatusOK,
39+
},
40+
{
41+
Name: "Denying authenticator",
42+
Authenticator: AuthenticatorFunc(func(_ *http.Request) (bool, string, error) {
43+
return false, "", nil
44+
}),
45+
ExpectedStatusCode: http.StatusUnauthorized,
46+
},
47+
{
48+
Name: "Erroring authenticator",
49+
Authenticator: AuthenticatorFunc(func(_ *http.Request) (bool, string, error) {
50+
return false, "", errors.New("error authenticating")
51+
}),
52+
ExpectedStatusCode: http.StatusInternalServerError,
53+
},
54+
}
55+
56+
for _, tt := range ts {
57+
t.Run(tt.Name, func(t *testing.T) {
58+
req := makeDefaultRequest(t)
59+
60+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
61+
w.WriteHeader(http.StatusOK)
62+
})
63+
64+
rr := httptest.NewRecorder()
65+
authHandler := WithAuthentication(handler, tt.Authenticator, logger)
66+
authHandler.ServeHTTP(rr, req)
67+
got := rr.Result()
68+
69+
if tt.ExpectedStatusCode != got.StatusCode {
70+
t.Errorf("Expected status code %q, got %q", tt.ExpectedStatusCode, got.Status)
71+
}
72+
})
73+
}
74+
}
75+
76+
type noOpLogger struct{}
77+
78+
func (noOpLogger) Log(...interface{}) error {
79+
return nil
80+
}
81+
82+
var _ log.Logger = &noOpLogger{}
83+
84+
func makeDefaultRequest(t *testing.T) *http.Request {
85+
t.Helper()
86+
87+
req, err := http.NewRequest(http.MethodGet, "/", nil)
88+
if err != nil {
89+
t.Fatalf("Error creating request: %v", err)
90+
}
91+
return req
92+
}
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)