diff --git a/docs/web-config.yml b/docs/web-config.yml index 942d9812..def09a2c 100644 --- a/docs/web-config.yml +++ b/docs/web-config.yml @@ -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 diff --git a/docs/web-configuration.md b/docs/web-configuration.md index a7823349..940d2d0a 100644 --- a/docs/web-configuration.md +++ b/docs/web-configuration.md @@ -125,6 +125,12 @@ http_server_config: # required. Passwords are hashed with bcrypt. basic_auth_users: [ : ... ] + +# 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: + [- ] ``` [A sample configuration file](web-config.yml) is provided. @@ -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. diff --git a/web/handler.go b/web/handler.go index c607a163..40cf8d49 100644 --- a/web/handler.go +++ b/web/handler.go @@ -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 @@ -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. @@ -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] @@ -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) +} diff --git a/web/testdata/web_config_tokens.good.yml b/web/testdata/web_config_tokens.good.yml new file mode 100644 index 00000000..df5afb1a --- /dev/null +++ b/web/testdata/web_config_tokens.good.yml @@ -0,0 +1,5 @@ +tls_server_config: + cert_file: "server.crt" + key_file: "server.key" +bearer_auth_tokens: + - TokenTest12345 diff --git a/web/testdata/web_config_tokens_noTLS.good.yml b/web/testdata/web_config_tokens_noTLS.good.yml new file mode 100644 index 00000000..3d39f4da --- /dev/null +++ b/web/testdata/web_config_tokens_noTLS.good.yml @@ -0,0 +1,2 @@ +bearer_auth_tokens: + - TokenTest12345 diff --git a/web/tls_config.go b/web/tls_config.go index 61383bce..8e796fe0 100644 --- a/web/tls_config.go +++ b/web/tls_config.go @@ -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 { @@ -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 } @@ -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 @@ -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 diff --git a/web/tls_config_test.go b/web/tls_config_test.go index 9a4926b6..d0481c82 100644 --- a/web/tls_config_test.go +++ b/web/tls_config_test.go @@ -102,6 +102,8 @@ type TestInputs struct { CurvePreferences []tls.CurveID Username string Password string + Token string + Authorization string // Raw authorization ClientCertificate string } @@ -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() { @@ -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) + } +}