Skip to content

Commit

Permalink
feat: HTTP Bearer Authorization for simple use cases
Browse files Browse the repository at this point in the history
Add support for HTTP Bearer Authorization for simple use cases, where
HTTP Basic might not fit workflows.

Signed-off-by: Robin H. Johnson <robbat2@gentoo.org>
  • Loading branch information
robbat2 committed Jan 7, 2024
1 parent d6919da commit 13f6b87
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 4 deletions.
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`,

Check failure on line 781 in web/tls_config_test.go

View workflow job for this annotation

GitHub Actions / lint

File is not `goimports`-ed (goimports)
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)
}
}

0 comments on commit 13f6b87

Please sign in to comment.