Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add path exclusion to mTLS and BasicAuth authentication #1

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/web-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ http_server_config:
# required. Passwords are hashed with bcrypt.
basic_auth_users:
[ <string>: <secret> ... ]

# A list of HTTP paths to be excepted from authentication.
auth_excluded_paths:
[ - <string> ]
```

[A sample configuration file](web-config.yml) is provided.
Expand Down
50 changes: 50 additions & 0 deletions web/authentication/authenticator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2023 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package authentication

import (
"net/http"

"github.com/go-kit/log"
"github.com/go-kit/log/level"
)

type Authenticator interface {
Authenticate(*http.Request) (bool, string, error)
}

type AuthenticatorFunc func(r *http.Request) (bool, string, error)

func (f AuthenticatorFunc) Authenticate(r *http.Request) (bool, string, error) {
return f(r)
}

func WithAuthentication(handler http.Handler, authenticator Authenticator, logger log.Logger) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ok, reason, err := authenticator.Authenticate(r)
if err != nil {
level.Error(logger).Log("msg", "Error authenticating", "URI", r.RequestURI, "err", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}

if ok {
handler.ServeHTTP(w, r)
return
}

level.Warn(logger).Log("msg", "Unauthenticated request", "URI", r.RequestURI, "reason", reason)
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
})
}
92 changes: 92 additions & 0 deletions web/authentication/authenticator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright 2023 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package authentication

import (
"errors"
"net/http"
"net/http/httptest"
"testing"

"github.com/go-kit/log"
)

func TestWithAuthentication(t *testing.T) {
logger := &noOpLogger{}

ts := []struct {
Name string
Authenticator Authenticator
ExpectedStatusCode int
}{
{
Name: "Accepting authenticator",
Authenticator: AuthenticatorFunc(func(_ *http.Request) (bool, string, error) {
return true, "", nil
}),
ExpectedStatusCode: http.StatusOK,
},
{
Name: "Denying authenticator",
Authenticator: AuthenticatorFunc(func(_ *http.Request) (bool, string, error) {
return false, "", nil
}),
ExpectedStatusCode: http.StatusUnauthorized,
},
{
Name: "Erroring authenticator",
Authenticator: AuthenticatorFunc(func(_ *http.Request) (bool, string, error) {
return false, "", errors.New("error authenticating")
}),
ExpectedStatusCode: http.StatusInternalServerError,
},
}

for _, tt := range ts {
t.Run(tt.Name, func(t *testing.T) {
req := makeDefaultRequest(t)

handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})

rr := httptest.NewRecorder()
authHandler := WithAuthentication(handler, tt.Authenticator, logger)
authHandler.ServeHTTP(rr, req)
got := rr.Result()

if tt.ExpectedStatusCode != got.StatusCode {
t.Errorf("Expected status code %q, got %q", tt.ExpectedStatusCode, got.Status)
}
})
}
}

type noOpLogger struct{}

func (noOpLogger) Log(...interface{}) error {
return nil
}

var _ log.Logger = &noOpLogger{}

func makeDefaultRequest(t *testing.T) *http.Request {
t.Helper()

req, err := http.NewRequest(http.MethodGet, "/", nil)
if err != nil {
t.Fatalf("Error creating request: %v", err)
}
return req
}
84 changes: 84 additions & 0 deletions web/authentication/basicauth/basicauth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright 2023 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package basicauth

import (
"encoding/hex"
"net/http"
"strings"
"sync"

"github.com/prometheus/common/config"
"github.com/prometheus/exporter-toolkit/web/authentication"
"golang.org/x/crypto/bcrypt"
)

type BasicAuthAuthenticator struct {
users map[string]config.Secret

cache *cache
// bcryptMtx is there to ensure that bcrypt.CompareHashAndPassword is run
// only once in parallel as this is CPU intensive.
bcryptMtx sync.Mutex
}

func (b *BasicAuthAuthenticator) Authenticate(r *http.Request) (bool, string, error) {
user, pass, auth := r.BasicAuth()

if !auth {
return false, "No credentials in request", nil
}

hashedPassword, validUser := b.users[user]

if !validUser {
// The user is not found. Use a fixed password hash to
// prevent user enumeration by timing requests.
// This is a bcrypt-hashed version of "fakepassword".
hashedPassword = "$2y$10$QOauhQNbBCuQDKes6eFzPeMqBSjb7Mr5DUmpZ/VcEd00UAV/LDeSi"
}

cacheKey := strings.Join(
[]string{
hex.EncodeToString([]byte(user)),
hex.EncodeToString([]byte(hashedPassword)),
hex.EncodeToString([]byte(pass)),
}, ":")
authOk, ok := b.cache.get(cacheKey)

if !ok {
// This user, hashedPassword, password is not cached.
b.bcryptMtx.Lock()
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(pass))
b.bcryptMtx.Unlock()

authOk = validUser && err == nil
b.cache.set(cacheKey, authOk)
}

if authOk && validUser {
return true, "", nil
}

return false, "Invalid credentials", nil
}

func NewBasicAuthAuthenticator(users map[string]config.Secret) authentication.Authenticator {
return &BasicAuthAuthenticator{
cache: newCache(),
users: users,
}
}

var _ authentication.Authenticator = &BasicAuthAuthenticator{}
Loading