Skip to content

Commit 13f6b87

Browse files
committed
feat: HTTP Bearer Authorization for simple use cases
Add support for HTTP Bearer Authorization for simple use cases, where HTTP Basic might not fit workflows. Signed-off-by: Robin H. Johnson <[email protected]>
1 parent d6919da commit 13f6b87

File tree

7 files changed

+176
-4
lines changed

7 files changed

+176
-4
lines changed

docs/web-config.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,9 @@ tls_server_config:
1010
basic_auth_users:
1111
alice: $2y$10$mDwo.lAisC94iLAyP81MCesa29IzH37oigHC/42V2pdJlUprsJPze
1212
bob: $2y$10$hLqFl9jSjoAAy95Z/zw8Ye8wkdMBM8c5Bn1ptYqP/AXyV0.oy0S8m
13+
14+
# Tokens that have full access to the web server via Bearer authentication.
15+
# Multiple tokens are accepted, to support gradual credential rollover.
16+
# If empty, no Bearer authentication is required.
17+
bearer_auth_tokens:
18+
- ExampleBearerToken

docs/web-configuration.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ http_server_config:
125125
# required. Passwords are hashed with bcrypt.
126126
basic_auth_users:
127127
[ <string>: <secret> ... ]
128+
129+
# Tokens that have full access to the web server via Bearer authentication.
130+
# Multiple tokens are accepted, to support gradual credential rollover.
131+
# If empty, no Bearer authentication is required.
132+
bearer_auth_tokens:
133+
[- <token>]
128134
```
129135

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

149155
## Performance
150156

151-
Basic authentication is meant for simple use cases, with a few users. If you
152-
need to authenticate a lot of users, it is recommended to use TLS client
157+
Basic & Bearer authentication are meant for simple use cases, with a few users.
158+
If you need to authenticate a lot of users, it is recommended to use TLS client
153159
certificates, or to use a proper reverse proxy to handle the authentication.

web/handler.go

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import (
2424

2525
"github.com/go-kit/log"
2626
"golang.org/x/crypto/bcrypt"
27+
28+
config_util "github.com/prometheus/common/config"
2729
)
2830

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

58+
func validateTokens(configPath string) error {
59+
//c, err := getConfig(configPath)
60+
//if err != nil {
61+
// return err
62+
//}
63+
64+
//for _, p := range c.Bearer {
65+
//}
66+
67+
return nil
68+
}
69+
5670
// validateHeaderConfig checks that the provided header configuration is correct.
5771
// It does not check the validity of all the values, only the ones which are
5872
// well-defined enumerations.
@@ -98,11 +112,19 @@ func (u *webHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
98112
w.Header().Set(k, v)
99113
}
100114

101-
if len(c.Users) == 0 {
102-
u.handler.ServeHTTP(w, r)
115+
if len(c.Users) > 0 {
116+
u.ServeHTTPAuthBasic(w, r, c)
117+
return
118+
}
119+
if len(c.Tokens) > 0 {
120+
u.ServeHTTPAuthBearer(w, r, c)
103121
return
104122
}
105123

124+
u.handler.ServeHTTP(w, r)
125+
}
126+
127+
func (u *webHandler) ServeHTTPAuthBasic(w http.ResponseWriter, r *http.Request, c *Config) {
106128
user, pass, auth := r.BasicAuth()
107129
if auth {
108130
hashedPassword, validUser := c.Users[user]
@@ -141,3 +163,19 @@ func (u *webHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
141163
w.Header().Set("WWW-Authenticate", "Basic")
142164
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
143165
}
166+
167+
func (u *webHandler) ServeHTTPAuthBearer(w http.ResponseWriter, r *http.Request, c *Config) {
168+
rawHttpAuthorization := r.Header.Get("Authorization")
169+
prefix := "Bearer "
170+
if strings.HasPrefix(rawHttpAuthorization, prefix) {
171+
token := config_util.Secret(strings.TrimPrefix(rawHttpAuthorization, prefix))
172+
_, tokenExists := c.tokenMap[token]
173+
if tokenExists {
174+
u.handler.ServeHTTP(w, r)
175+
return
176+
}
177+
}
178+
179+
w.Header().Set("WWW-Authenticate", "Bearer")
180+
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
181+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
tls_server_config:
2+
cert_file: "server.crt"
3+
key_file: "server.key"
4+
bearer_auth_tokens:
5+
- TokenTest12345
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
bearer_auth_tokens:
2+
- TokenTest12345

web/tls_config.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ type Config struct {
4040
TLSConfig TLSConfig `yaml:"tls_server_config"`
4141
HTTPConfig HTTPConfig `yaml:"http_server_config"`
4242
Users map[string]config_util.Secret `yaml:"basic_auth_users"`
43+
Tokens []config_util.Secret `yaml:"bearer_auth_tokens"`
44+
45+
tokenMap map[config_util.Secret]bool // Fast lookup for Bearer Tokens
4346
}
4447

4548
type TLSConfig struct {
@@ -123,6 +126,14 @@ func getConfig(configPath string) (*Config, error) {
123126
if err == nil {
124127
err = validateHeaderConfig(c.HTTPConfig.Header)
125128
}
129+
// Convert tokens for fast lookup
130+
if len(c.Tokens) > 0 {
131+
c.tokenMap = make(map[config_util.Secret]bool, len(c.Tokens))
132+
for _, t := range c.Tokens {
133+
c.tokenMap[t] = true
134+
}
135+
}
136+
126137
c.TLSConfig.SetDirectory(filepath.Dir(configPath))
127138
return c, err
128139
}
@@ -320,6 +331,9 @@ func Serve(l net.Listener, server *http.Server, flags *FlagConfig, logger log.Lo
320331
if err := validateUsers(tlsConfigPath); err != nil {
321332
return err
322333
}
334+
if err := validateTokens(tlsConfigPath); err != nil {
335+
return err
336+
}
323337

324338
// Setup basic authentication.
325339
var handler http.Handler = http.DefaultServeMux
@@ -379,6 +393,9 @@ func Validate(tlsConfigPath string) error {
379393
if err := validateUsers(tlsConfigPath); err != nil {
380394
return err
381395
}
396+
if err := validateTokens(tlsConfigPath); err != nil {
397+
return err
398+
}
382399
c, err := getConfig(tlsConfigPath)
383400
if err != nil {
384401
return err

web/tls_config_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ type TestInputs struct {
102102
CurvePreferences []tls.CurveID
103103
Username string
104104
Password string
105+
Token string
106+
Authorization string // Raw authorization
105107
ClientCertificate string
106108
}
107109

@@ -516,6 +518,12 @@ func (test *TestInputs) Test(t *testing.T) {
516518
if test.Username != "" {
517519
req.SetBasicAuth(test.Username, test.Password)
518520
}
521+
if test.Token != "" {
522+
req.Header.Set("Authorization", "Bearer "+test.Token)
523+
}
524+
if test.Authorization != "" {
525+
req.Header.Set("Authorization", test.Authorization)
526+
}
519527
return client.Do(req)
520528
}
521529
go func() {
@@ -698,3 +706,93 @@ func TestUsers(t *testing.T) {
698706
t.Run(testInputs.Name, testInputs.Test)
699707
}
700708
}
709+
710+
func TestTokens(t *testing.T) {
711+
testTables := []*TestInputs{
712+
{
713+
Name: `with correct token`,
714+
YAMLConfigPath: "testdata/web_config_tokens_noTLS.good.yml",
715+
Token: "TokenTest12345",
716+
ExpectedError: nil,
717+
},
718+
{
719+
Name: `with incorrect token`,
720+
YAMLConfigPath: "testdata/web_config_tokens_noTLS.good.yml",
721+
Token: "TokenTest12345",
722+
ExpectedError: nil,
723+
},
724+
{
725+
Name: `without token and TLS`,
726+
YAMLConfigPath: "testdata/web_config_tokens.good.yml",
727+
UseTLSClient: true,
728+
ExpectedError: ErrorMap["Unauthorized"],
729+
},
730+
{
731+
Name: `with correct token and TLS`,
732+
YAMLConfigPath: "testdata/web_config_tokens.good.yml",
733+
UseTLSClient: true,
734+
Token: "TokenTest12345",
735+
ExpectedError: nil,
736+
},
737+
{
738+
Name: `with incorrect token and TLS`,
739+
YAMLConfigPath: "testdata/web_config_tokens.good.yml",
740+
UseTLSClient: true,
741+
Token: "nonexistent",
742+
ExpectedError: ErrorMap["Unauthorized"],
743+
},
744+
}
745+
for _, testInputs := range testTables {
746+
t.Run(testInputs.Name, testInputs.Test)
747+
}
748+
}
749+
750+
func TestRawAuthorization(t *testing.T) {
751+
testTables := []*TestInputs{
752+
{
753+
Name: `with raw authorization vs expected user`,
754+
YAMLConfigPath: "testdata/web_config_users_noTLS.good.yml",
755+
Authorization: "FakeAuth FakeAuthMagic12345",
756+
UseTLSClient: false,
757+
ExpectedError: ErrorMap["Unauthorized"],
758+
},
759+
{
760+
Name: `with raw authorization and TLS vs expected user`,
761+
YAMLConfigPath: "testdata/web_config_users.good.yml",
762+
Authorization: "FakeAuth FakeAuthMagic12345",
763+
UseTLSClient: true,
764+
ExpectedError: ErrorMap["Unauthorized"],
765+
},
766+
{
767+
Name: `with raw authorization vs expected token`,
768+
YAMLConfigPath: "testdata/web_config_tokens_noTLS.good.yml",
769+
Authorization: "FakeAuth FakeAuthMagic12345",
770+
UseTLSClient: false,
771+
ExpectedError: ErrorMap["Unauthorized"],
772+
},
773+
{
774+
Name: `with raw authorization and TLS vs expected token`,
775+
YAMLConfigPath: "testdata/web_config_tokens.good.yml",
776+
Authorization: "FakeAuth FakeAuthMagic12345",
777+
UseTLSClient: true,
778+
ExpectedError: ErrorMap["Unauthorized"],
779+
},
780+
{
781+
Name: `with raw authorization vs no auth expected`,
782+
YAMLConfigPath: "testdata/web_config_noAuth.good.yml",
783+
Authorization: "FakeAuth FakeAuthMagic12345",
784+
UseTLSClient: true,
785+
ExpectedError: nil,
786+
},
787+
{
788+
Name: `with raw authorization, no TLS, vs no auth expected`,
789+
YAMLConfigPath: "testdata/web_config_empty.yml",
790+
Authorization: "FakeAuth FakeAuthMagic12345",
791+
UseTLSClient: false,
792+
ExpectedError: nil,
793+
},
794+
}
795+
for _, testInputs := range testTables {
796+
t.Run(testInputs.Name, testInputs.Test)
797+
}
798+
}

0 commit comments

Comments
 (0)