Skip to content

Commit

Permalink
Add path exclusion support to BasicAuth authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
rzetelskik committed May 11, 2023
1 parent 3df7372 commit d217ea9
Show file tree
Hide file tree
Showing 16 changed files with 961 additions and 229 deletions.
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

0 comments on commit d217ea9

Please sign in to comment.