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

feat: HTTP Bearer Authorization for simple use cases #193

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/web-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,9 @@ tls_server_config:
basic_auth_users:
alice: $2y$10$mDwo.lAisC94iLAyP81MCesa29IzH37oigHC/42V2pdJlUprsJPze
bob: $2y$10$hLqFl9jSjoAAy95Z/zw8Ye8wkdMBM8c5Bn1ptYqP/AXyV0.oy0S8m

# Tokens that have full access to the web server via Bearer authentication.
# Multiple tokens are accepted, to support gradual credential rollover.
# If empty, no Bearer authentication is required.
bearer_auth_tokens:
- ExampleBearerToken
10 changes: 8 additions & 2 deletions docs/web-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ http_server_config:
# required. Passwords are hashed with bcrypt.
basic_auth_users:
[ <string>: <secret> ... ]

# Tokens that have full access to the web server via Bearer authentication.
# Multiple tokens are accepted, to support gradual credential rollover.
# If empty, no Bearer authentication is required.
bearer_auth_tokens:
[- <token>]
```

[A sample configuration file](web-config.yml) is provided.
Expand All @@ -148,6 +154,6 @@ authenticated HTTP request and then cached.

## Performance

Basic authentication is meant for simple use cases, with a few users. If you
need to authenticate a lot of users, it is recommended to use TLS client
Basic & Bearer authentication are meant for simple use cases, with a few users.
If you need to authenticate a lot of users, it is recommended to use TLS client
certificates, or to use a proper reverse proxy to handle the authentication.
42 changes: 40 additions & 2 deletions web/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (

"github.com/go-kit/log"
"golang.org/x/crypto/bcrypt"

config_util "github.com/prometheus/common/config"
)

// extraHTTPHeaders is a map of HTTP headers that can be added to HTTP
Expand Down Expand Up @@ -53,6 +55,18 @@ func validateUsers(configPath string) error {
return nil
}

func validateTokens(configPath string) error {
//c, err := getConfig(configPath)
//if err != nil {
// return err
//}

//for _, p := range c.Bearer {
//}

return nil
}

// validateHeaderConfig checks that the provided header configuration is correct.
// It does not check the validity of all the values, only the ones which are
// well-defined enumerations.
Expand Down Expand Up @@ -98,11 +112,19 @@ func (u *webHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set(k, v)
}

if len(c.Users) == 0 {
u.handler.ServeHTTP(w, r)
if len(c.Users) > 0 {
u.ServeHTTPAuthBasic(w, r, c)
return
}
if len(c.Tokens) > 0 {
u.ServeHTTPAuthBearer(w, r, c)
return
}

u.handler.ServeHTTP(w, r)
}

func (u *webHandler) ServeHTTPAuthBasic(w http.ResponseWriter, r *http.Request, c *Config) {
user, pass, auth := r.BasicAuth()
if auth {
hashedPassword, validUser := c.Users[user]
Expand Down Expand Up @@ -141,3 +163,19 @@ func (u *webHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("WWW-Authenticate", "Basic")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
}

func (u *webHandler) ServeHTTPAuthBearer(w http.ResponseWriter, r *http.Request, c *Config) {
rawHttpAuthorization := r.Header.Get("Authorization")
prefix := "Bearer "
if strings.HasPrefix(rawHttpAuthorization, prefix) {
token := config_util.Secret(strings.TrimPrefix(rawHttpAuthorization, prefix))
_, tokenExists := c.tokenMap[token]
if tokenExists {
u.handler.ServeHTTP(w, r)
return
}
}

w.Header().Set("WWW-Authenticate", "Bearer")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
}
5 changes: 5 additions & 0 deletions web/testdata/web_config_tokens.good.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
tls_server_config:
cert_file: "server.crt"
key_file: "server.key"
bearer_auth_tokens:
- TokenTest12345
2 changes: 2 additions & 0 deletions web/testdata/web_config_tokens_noTLS.good.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bearer_auth_tokens:
- TokenTest12345
17 changes: 17 additions & 0 deletions web/tls_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ type Config struct {
TLSConfig TLSConfig `yaml:"tls_server_config"`
HTTPConfig HTTPConfig `yaml:"http_server_config"`
Users map[string]config_util.Secret `yaml:"basic_auth_users"`
Tokens []config_util.Secret `yaml:"bearer_auth_tokens"`

tokenMap map[config_util.Secret]bool // Fast lookup for Bearer Tokens
}

type TLSConfig struct {
Expand Down Expand Up @@ -123,6 +126,14 @@ func getConfig(configPath string) (*Config, error) {
if err == nil {
err = validateHeaderConfig(c.HTTPConfig.Header)
}
// Convert tokens for fast lookup
if len(c.Tokens) > 0 {
c.tokenMap = make(map[config_util.Secret]bool, len(c.Tokens))
for _, t := range c.Tokens {
c.tokenMap[t] = true
}
}

c.TLSConfig.SetDirectory(filepath.Dir(configPath))
return c, err
}
Expand Down Expand Up @@ -320,6 +331,9 @@ func Serve(l net.Listener, server *http.Server, flags *FlagConfig, logger log.Lo
if err := validateUsers(tlsConfigPath); err != nil {
return err
}
if err := validateTokens(tlsConfigPath); err != nil {
return err
}

// Setup basic authentication.
var handler http.Handler = http.DefaultServeMux
Expand Down Expand Up @@ -379,6 +393,9 @@ func Validate(tlsConfigPath string) error {
if err := validateUsers(tlsConfigPath); err != nil {
return err
}
if err := validateTokens(tlsConfigPath); err != nil {
return err
}
c, err := getConfig(tlsConfigPath)
if err != nil {
return err
Expand Down
98 changes: 98 additions & 0 deletions web/tls_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ type TestInputs struct {
CurvePreferences []tls.CurveID
Username string
Password string
Token string
Authorization string // Raw authorization
ClientCertificate string
}

Expand Down Expand Up @@ -516,6 +518,12 @@ func (test *TestInputs) Test(t *testing.T) {
if test.Username != "" {
req.SetBasicAuth(test.Username, test.Password)
}
if test.Token != "" {
req.Header.Set("Authorization", "Bearer "+test.Token)
}
if test.Authorization != "" {
req.Header.Set("Authorization", test.Authorization)
}
return client.Do(req)
}
go func() {
Expand Down Expand Up @@ -698,3 +706,93 @@ func TestUsers(t *testing.T) {
t.Run(testInputs.Name, testInputs.Test)
}
}

func TestTokens(t *testing.T) {
testTables := []*TestInputs{
{
Name: `with correct token`,
YAMLConfigPath: "testdata/web_config_tokens_noTLS.good.yml",
Token: "TokenTest12345",
ExpectedError: nil,
},
{
Name: `with incorrect token`,
YAMLConfigPath: "testdata/web_config_tokens_noTLS.good.yml",
Token: "TokenTest12345",
ExpectedError: nil,
},
{
Name: `without token and TLS`,
YAMLConfigPath: "testdata/web_config_tokens.good.yml",
UseTLSClient: true,
ExpectedError: ErrorMap["Unauthorized"],
},
{
Name: `with correct token and TLS`,
YAMLConfigPath: "testdata/web_config_tokens.good.yml",
UseTLSClient: true,
Token: "TokenTest12345",
ExpectedError: nil,
},
{
Name: `with incorrect token and TLS`,
YAMLConfigPath: "testdata/web_config_tokens.good.yml",
UseTLSClient: true,
Token: "nonexistent",
ExpectedError: ErrorMap["Unauthorized"],
},
}
for _, testInputs := range testTables {
t.Run(testInputs.Name, testInputs.Test)
}
}

func TestRawAuthorization(t *testing.T) {
testTables := []*TestInputs{
{
Name: `with raw authorization vs expected user`,
YAMLConfigPath: "testdata/web_config_users_noTLS.good.yml",
Authorization: "FakeAuth FakeAuthMagic12345",
UseTLSClient: false,
ExpectedError: ErrorMap["Unauthorized"],
},
{
Name: `with raw authorization and TLS vs expected user`,
YAMLConfigPath: "testdata/web_config_users.good.yml",
Authorization: "FakeAuth FakeAuthMagic12345",
UseTLSClient: true,
ExpectedError: ErrorMap["Unauthorized"],
},
{
Name: `with raw authorization vs expected token`,
YAMLConfigPath: "testdata/web_config_tokens_noTLS.good.yml",
Authorization: "FakeAuth FakeAuthMagic12345",
UseTLSClient: false,
ExpectedError: ErrorMap["Unauthorized"],
},
{
Name: `with raw authorization and TLS vs expected token`,
YAMLConfigPath: "testdata/web_config_tokens.good.yml",
Authorization: "FakeAuth FakeAuthMagic12345",
UseTLSClient: true,
ExpectedError: ErrorMap["Unauthorized"],
},
{
Name: `with raw authorization vs no auth expected`,
YAMLConfigPath: "testdata/web_config_noAuth.good.yml",
Authorization: "FakeAuth FakeAuthMagic12345",
UseTLSClient: true,
ExpectedError: nil,
},
{
Name: `with raw authorization, no TLS, vs no auth expected`,
YAMLConfigPath: "testdata/web_config_empty.yml",
Authorization: "FakeAuth FakeAuthMagic12345",
UseTLSClient: false,
ExpectedError: nil,
},
}
for _, testInputs := range testTables {
t.Run(testInputs.Name, testInputs.Test)
}
}