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

Support authorization_credentials #272

Merged
merged 1 commit into from
Feb 17, 2021
Merged
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
124 changes: 94 additions & 30 deletions config/http_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@ func (a *BasicAuth) SetDirectory(dir string) {
a.PasswordFile = JoinDir(dir, a.PasswordFile)
}

// Authorization contains HTTP authorization credentials.
type Authorization struct {
Type string `yaml:"type,omitempty"`
Credentials Secret `yaml:"credentials,omitempty"`
CredentialsFile string `yaml:"credentials_file,omitempty"`
}

// SetDirectory joins any relative file paths with dir.
func (a *Authorization) SetDirectory(dir string) {
if a == nil {
return
}
a.CredentialsFile = JoinDir(dir, a.CredentialsFile)
}

// URL is a custom URL type that allows validation at configuration load time.
type URL struct {
*url.URL
Expand Down Expand Up @@ -84,14 +99,21 @@ func (u URL) MarshalYAML() (interface{}, error) {
type HTTPClientConfig struct {
// The HTTP basic authentication credentials for the targets.
BasicAuth *BasicAuth `yaml:"basic_auth,omitempty"`
// The bearer token for the targets.
// The HTTP authorization credentials for the targets.
Authorization *Authorization `yaml:"authorization,omitempty"`
// The bearer token for the targets. Deprecated in favour of
// Authorization.Credentials.
BearerToken Secret `yaml:"bearer_token,omitempty"`
// The bearer token file for the targets.
// The bearer token file for the targets. Deprecated in favour of
// Authorization.CredentialsFile.
BearerTokenFile string `yaml:"bearer_token_file,omitempty"`
// HTTP proxy server to use to connect to the targets.
ProxyURL URL `yaml:"proxy_url,omitempty"`
// TLSConfig to use to connect to the targets.
TLSConfig TLSConfig `yaml:"tls_config,omitempty"`
// Used to make sure that the configuration is valid and that BearerToken to
// Authorization.Credentials change has been handled.
valid bool
}

// SetDirectory joins any relative file paths with dir.
Expand All @@ -101,12 +123,14 @@ func (c *HTTPClientConfig) SetDirectory(dir string) {
}
c.TLSConfig.SetDirectory(dir)
c.BasicAuth.SetDirectory(dir)
c.Authorization.SetDirectory(dir)
c.BearerTokenFile = JoinDir(dir, c.BearerTokenFile)
}

// Validate validates the HTTPClientConfig to check only one of BearerToken,
// BasicAuth and BearerTokenFile is configured.
func (c *HTTPClientConfig) Validate() error {
// Backwards compatibility with the bearer_token field.
if len(c.BearerToken) > 0 && len(c.BearerTokenFile) > 0 {
return fmt.Errorf("at most one of bearer_token & bearer_token_file must be configured")
}
Expand All @@ -116,6 +140,37 @@ func (c *HTTPClientConfig) Validate() error {
if c.BasicAuth != nil && (string(c.BasicAuth.Password) != "" && c.BasicAuth.PasswordFile != "") {
return fmt.Errorf("at most one of basic_auth password & password_file must be configured")
}
if c.Authorization != nil {
if len(c.BearerToken) > 0 || len(c.BearerTokenFile) > 0 {
return fmt.Errorf("authorization is not compatible with bearer_token & bearer_token_file")
}
if string(c.Authorization.Credentials) != "" && c.Authorization.CredentialsFile != "" {
return fmt.Errorf("at most one of authorization credentials & credentials_file must be configured")
}
c.Authorization.Type = strings.TrimSpace(c.Authorization.Type)
if len(c.Authorization.Type) == 0 {
c.Authorization.Type = "Bearer"
}
if strings.ToLower(c.Authorization.Type) == "basic" {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the benefit for preventing "basic" as the auth type here? Just thinking the two config mechanisms are not strictly equivalent - as with one the user must save the username and password in the config in plain text. In the other the user can save the credentials pre encoded as they are transmitted over the wire. It may be preferable to the user to use the second mechanism?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to reduce support requests. That prevents users to put clear text password here and expect it to work. We have a well defined way to do Basic auth, let's not add a second.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also note that some users might have the fake sense of security that this method would use an encrypted password while it's not the case.

return fmt.Errorf(`authorization type cannot be set to "basic", use "basic_auth" instead`)
}
if c.BasicAuth != nil {
return fmt.Errorf("at most one of basic_auth & authorization must be configured")
}
} else {
if len(c.BearerToken) > 0 {
c.Authorization = &Authorization{Credentials: c.BearerToken}
c.Authorization.Type = "Bearer"
c.BearerToken = ""
}
if len(c.BearerTokenFile) > 0 {
c.Authorization = &Authorization{CredentialsFile: c.BearerTokenFile}
c.Authorization.Type = "Bearer"
c.BearerTokenFile = ""
}
}

c.valid = true
return nil
}

Expand Down Expand Up @@ -152,6 +207,12 @@ func NewClientFromConfig(cfg HTTPClientConfig, name string, disableKeepAlives, e
// NewRoundTripperFromConfig returns a new HTTP RoundTripper configured for the
// given config.HTTPClientConfig. The name is used as go-conntrack metric label.
func NewRoundTripperFromConfig(cfg HTTPClientConfig, name string, disableKeepAlives, enableHTTP2 bool) (http.RoundTripper, error) {
// Make sure that the configuration is valid.
if !cfg.valid {
if err := cfg.Validate(); err != nil {
return nil, err
}
}
newRT := func(tlsConfig *tls.Config) (http.RoundTripper, error) {
// The only timeout we care about is the configured scrape timeout.
// It is applied on request. So we leave out any timings here.
Expand Down Expand Up @@ -186,12 +247,12 @@ func NewRoundTripperFromConfig(cfg HTTPClientConfig, name string, disableKeepAli
}
}

// If a bearer token is provided, create a round tripper that will set the
// If a authorization_credentials is provided, create a round tripper that will set the
// Authorization header correctly on each request.
if len(cfg.BearerToken) > 0 {
rt = NewBearerAuthRoundTripper(cfg.BearerToken, rt)
} else if len(cfg.BearerTokenFile) > 0 {
rt = NewBearerAuthFileRoundTripper(cfg.BearerTokenFile, rt)
if cfg.Authorization != nil && len(cfg.Authorization.Credentials) > 0 {
rt = NewAuthorizationCredentialsRoundTripper(cfg.Authorization.Type, cfg.Authorization.Credentials, rt)
} else if cfg.Authorization != nil && len(cfg.Authorization.CredentialsFile) > 0 {
rt = NewAuthorizationCredentialsFileRoundTripper(cfg.Authorization.Type, cfg.Authorization.CredentialsFile, rt)
}

if cfg.BasicAuth != nil {
Expand All @@ -214,58 +275,61 @@ func NewRoundTripperFromConfig(cfg HTTPClientConfig, name string, disableKeepAli
return newTLSRoundTripper(tlsConfig, cfg.TLSConfig.CAFile, newRT)
}

type bearerAuthRoundTripper struct {
bearerToken Secret
rt http.RoundTripper
type authorizationCredentialsRoundTripper struct {
authType string
authCredentials Secret
rt http.RoundTripper
}

// NewBearerAuthRoundTripper adds the provided bearer token to a request unless the authorization
// header has already been set.
func NewBearerAuthRoundTripper(token Secret, rt http.RoundTripper) http.RoundTripper {
return &bearerAuthRoundTripper{token, rt}
// NewAuthorizationCredentialsRoundTripper adds the provided credentials to a
// request unless the authorization header has already been set.
func NewAuthorizationCredentialsRoundTripper(authType string, authCredentials Secret, rt http.RoundTripper) http.RoundTripper {
return &authorizationCredentialsRoundTripper{authType, authCredentials, rt}
}

func (rt *bearerAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
func (rt *authorizationCredentialsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
if len(req.Header.Get("Authorization")) == 0 {
req = cloneRequest(req)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(rt.bearerToken)))
req.Header.Set("Authorization", fmt.Sprintf("%s %s", rt.authType, string(rt.authCredentials)))
}
return rt.rt.RoundTrip(req)
}

func (rt *bearerAuthRoundTripper) CloseIdleConnections() {
func (rt *authorizationCredentialsRoundTripper) CloseIdleConnections() {
if ci, ok := rt.rt.(closeIdler); ok {
ci.CloseIdleConnections()
}
}

type bearerAuthFileRoundTripper struct {
bearerFile string
rt http.RoundTripper
type authorizationCredentialsFileRoundTripper struct {
authType string
authCredentialsFile string
rt http.RoundTripper
}

// NewBearerAuthFileRoundTripper adds the bearer token read from the provided file to a request unless
// the authorization header has already been set. This file is read for every request.
func NewBearerAuthFileRoundTripper(bearerFile string, rt http.RoundTripper) http.RoundTripper {
return &bearerAuthFileRoundTripper{bearerFile, rt}
// NewAuthorizationCredentialsFileRoundTripper adds the authorization
// credentials read from the provided file to a request unless the authorization
// header has already been set. This file is read for every request.
func NewAuthorizationCredentialsFileRoundTripper(authType, authCredentialsFile string, rt http.RoundTripper) http.RoundTripper {
return &authorizationCredentialsFileRoundTripper{authType, authCredentialsFile, rt}
}

func (rt *bearerAuthFileRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
func (rt *authorizationCredentialsFileRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
if len(req.Header.Get("Authorization")) == 0 {
b, err := ioutil.ReadFile(rt.bearerFile)
b, err := ioutil.ReadFile(rt.authCredentialsFile)
if err != nil {
return nil, fmt.Errorf("unable to read bearer token file %s: %s", rt.bearerFile, err)
return nil, fmt.Errorf("unable to read authorization credentials file %s: %s", rt.authCredentialsFile, err)
}
bearerToken := strings.TrimSpace(string(b))
authCredentials := strings.TrimSpace(string(b))

req = cloneRequest(req)
req.Header.Set("Authorization", "Bearer "+bearerToken)
req.Header.Set("Authorization", fmt.Sprintf("%s %s", rt.authType, authCredentials))
}

return rt.rt.RoundTrip(req)
}

func (rt *bearerAuthFileRoundTripper) CloseIdleConnections() {
func (rt *authorizationCredentialsFileRoundTripper) CloseIdleConnections() {
if ci, ok := rt.rt.(closeIdler); ok {
ci.CloseIdleConnections()
}
Expand Down
125 changes: 113 additions & 12 deletions config/http_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,17 @@ const (
MissingCert = "missing/cert.crt"
MissingKey = "missing/secret.key"

ExpectedMessage = "I'm here to serve you!!!"
BearerToken = "theanswertothegreatquestionoflifetheuniverseandeverythingisfortytwo"
BearerTokenFile = "testdata/bearer.token"
MissingBearerTokenFile = "missing/bearer.token"
ExpectedBearer = "Bearer " + BearerToken
ExpectedUsername = "arthurdent"
ExpectedPassword = "42"
ExpectedMessage = "I'm here to serve you!!!"
AuthorizationCredentials = "theanswertothegreatquestionoflifetheuniverseandeverythingisfortytwo"
AuthorizationCredentialsFile = "testdata/bearer.token"
AuthorizationType = "APIKEY"
BearerToken = AuthorizationCredentials
BearerTokenFile = AuthorizationCredentialsFile
MissingBearerTokenFile = "missing/bearer.token"
ExpectedBearer = "Bearer " + BearerToken
ExpectedAuthenticationCredentials = AuthorizationType + " " + BearerToken
ExpectedUsername = "arthurdent"
ExpectedPassword = "42"
)

var invalidHTTPClientConfigs = []struct {
Expand All @@ -74,6 +78,22 @@ var invalidHTTPClientConfigs = []struct {
httpClientConfigFile: "testdata/http.conf.basic-auth.too-much.bad.yaml",
errMsg: "at most one of basic_auth password & password_file must be configured",
},
{
httpClientConfigFile: "testdata/http.conf.mix-bearer-and-creds.bad.yaml",
errMsg: "authorization is not compatible with bearer_token & bearer_token_file",
},
{
httpClientConfigFile: "testdata/http.conf.auth-creds-and-file-set.too-much.bad.yaml",
errMsg: "at most one of authorization credentials & credentials_file must be configured",
},
{
httpClientConfigFile: "testdata/http.conf.basic-auth-and-auth-creds.too-much.bad.yaml",
errMsg: "at most one of basic_auth & authorization must be configured",
},
{
httpClientConfigFile: "testdata/http.conf.auth-creds-no-basic.bad.yaml",
errMsg: `authorization type cannot be set to "basic", use "basic_auth" instead`,
},
}

func newTestServer(handler func(w http.ResponseWriter, r *http.Request)) (*httptest.Server, error) {
Expand Down Expand Up @@ -170,6 +190,87 @@ func TestNewClientFromConfig(t *testing.T) {
fmt.Fprint(w, ExpectedMessage)
}
},
}, {
clientConfig: HTTPClientConfig{
Authorization: &Authorization{Credentials: BearerToken},
TLSConfig: TLSConfig{
CAFile: TLSCAChainPath,
CertFile: ClientCertificatePath,
KeyFile: ClientKeyNoPassPath,
ServerName: "",
InsecureSkipVerify: false},
},
handler: func(w http.ResponseWriter, r *http.Request) {
bearer := r.Header.Get("Authorization")
if bearer != ExpectedBearer {
fmt.Fprintf(w, "The expected Bearer Authorization (%s) differs from the obtained Bearer Authorization (%s)",
ExpectedBearer, bearer)
} else {
fmt.Fprint(w, ExpectedMessage)
}
},
}, {
clientConfig: HTTPClientConfig{
Authorization: &Authorization{CredentialsFile: AuthorizationCredentialsFile, Type: AuthorizationType},
TLSConfig: TLSConfig{
CAFile: TLSCAChainPath,
CertFile: ClientCertificatePath,
KeyFile: ClientKeyNoPassPath,
ServerName: "",
InsecureSkipVerify: false},
},
handler: func(w http.ResponseWriter, r *http.Request) {
bearer := r.Header.Get("Authorization")
if bearer != ExpectedAuthenticationCredentials {
fmt.Fprintf(w, "The expected Bearer Authorization (%s) differs from the obtained Bearer Authorization (%s)",
ExpectedAuthenticationCredentials, bearer)
} else {
fmt.Fprint(w, ExpectedMessage)
}
},
}, {
clientConfig: HTTPClientConfig{
Authorization: &Authorization{
Credentials: AuthorizationCredentials,
Type: AuthorizationType,
},
TLSConfig: TLSConfig{
CAFile: TLSCAChainPath,
CertFile: ClientCertificatePath,
KeyFile: ClientKeyNoPassPath,
ServerName: "",
InsecureSkipVerify: false},
},
handler: func(w http.ResponseWriter, r *http.Request) {
bearer := r.Header.Get("Authorization")
if bearer != ExpectedAuthenticationCredentials {
fmt.Fprintf(w, "The expected Bearer Authorization (%s) differs from the obtained Bearer Authorization (%s)",
ExpectedAuthenticationCredentials, bearer)
} else {
fmt.Fprint(w, ExpectedMessage)
}
},
}, {
clientConfig: HTTPClientConfig{
Authorization: &Authorization{
CredentialsFile: BearerTokenFile,
},
TLSConfig: TLSConfig{
CAFile: TLSCAChainPath,
CertFile: ClientCertificatePath,
KeyFile: ClientKeyNoPassPath,
ServerName: "",
InsecureSkipVerify: false},
},
handler: func(w http.ResponseWriter, r *http.Request) {
bearer := r.Header.Get("Authorization")
if bearer != ExpectedBearer {
fmt.Fprintf(w, "The expected Bearer Authorization (%s) differs from the obtained Bearer Authorization (%s)",
ExpectedBearer, bearer)
} else {
fmt.Fprint(w, ExpectedMessage)
}
},
}, {
clientConfig: HTTPClientConfig{
BasicAuth: &BasicAuth{
Expand Down Expand Up @@ -304,7 +405,7 @@ func TestMissingBearerAuthFile(t *testing.T) {
t.Fatal("No error is returned here")
}

if !strings.Contains(err.Error(), "unable to read bearer token file missing/bearer.token: open missing/bearer.token: no such file or directory") {
if !strings.Contains(err.Error(), "unable to read authorization credentials file missing/bearer.token: open missing/bearer.token: no such file or directory") {
t.Fatal("wrong error message being returned")
}
}
Expand All @@ -323,7 +424,7 @@ func TestBearerAuthRoundTripper(t *testing.T) {
}, nil, nil)

// Normal flow.
bearerAuthRoundTripper := NewBearerAuthRoundTripper(BearerToken, fakeRoundTripper)
bearerAuthRoundTripper := NewAuthorizationCredentialsRoundTripper("Bearer", BearerToken, fakeRoundTripper)
request, _ := http.NewRequest("GET", "/hitchhiker", nil)
request.Header.Set("User-Agent", "Douglas Adams mind")
_, err := bearerAuthRoundTripper.RoundTrip(request)
Expand All @@ -332,7 +433,7 @@ func TestBearerAuthRoundTripper(t *testing.T) {
}

// Should honor already Authorization header set.
bearerAuthRoundTripperShouldNotModifyExistingAuthorization := NewBearerAuthRoundTripper(newBearerToken, fakeRoundTripper)
bearerAuthRoundTripperShouldNotModifyExistingAuthorization := NewAuthorizationCredentialsRoundTripper("Bearer", newBearerToken, fakeRoundTripper)
request, _ = http.NewRequest("GET", "/hitchhiker", nil)
request.Header.Set("Authorization", ExpectedBearer)
_, err = bearerAuthRoundTripperShouldNotModifyExistingAuthorization.RoundTrip(request)
Expand All @@ -351,7 +452,7 @@ func TestBearerAuthFileRoundTripper(t *testing.T) {
}, nil, nil)

// Normal flow.
bearerAuthRoundTripper := NewBearerAuthFileRoundTripper(BearerTokenFile, fakeRoundTripper)
bearerAuthRoundTripper := NewAuthorizationCredentialsFileRoundTripper("Bearer", BearerTokenFile, fakeRoundTripper)
request, _ := http.NewRequest("GET", "/hitchhiker", nil)
request.Header.Set("User-Agent", "Douglas Adams mind")
_, err := bearerAuthRoundTripper.RoundTrip(request)
Expand All @@ -360,7 +461,7 @@ func TestBearerAuthFileRoundTripper(t *testing.T) {
}

// Should honor already Authorization header set.
bearerAuthRoundTripperShouldNotModifyExistingAuthorization := NewBearerAuthFileRoundTripper(MissingBearerTokenFile, fakeRoundTripper)
bearerAuthRoundTripperShouldNotModifyExistingAuthorization := NewAuthorizationCredentialsFileRoundTripper("Bearer", MissingBearerTokenFile, fakeRoundTripper)
request, _ = http.NewRequest("GET", "/hitchhiker", nil)
request.Header.Set("Authorization", ExpectedBearer)
_, err = bearerAuthRoundTripperShouldNotModifyExistingAuthorization.RoundTrip(request)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
authorization:
credentials: bearertoken
credentials_file: key.txt