Repository: VictoriaMetrics/VictoriaMetrics
Pull request #1316 (2021-05-22 01:35:09)

basic OAuth2 support for remoteWrite and scrape targets

codecov[bot]: # [Codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1316?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) Report
> Merging [#1316](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1316?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) (e81b9a4) into [master](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/commit/14ec2b9f26783aef2be1ee7983e98738782805ca?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) (14ec2b9) will **decrease** coverage by `0.11%`.
> The diff coverage is `51.92%`.

[![Impacted file tree graph](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1316/graphs/tree.svg?width=650&height=150&src=pr&token=YND8dYVcXn&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics)](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1316?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics)

```diff
@@            Coverage Diff             @@
##           master    #1316      +/-   ##
==========================================
- Coverage   60.73%   60.62%   -0.12%     
==========================================
  Files         217      219       +2     
  Lines       25866    26045     +179     
==========================================
+ Hits        15710    15789      +79     
- Misses       8706     8782      +76     
- Partials     1450     1474      +24     
```


| [Impacted Files](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1316?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) | Coverage Δ | |
|---|---|---|
| [lib/promscrape/discovery/consul/api.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1316/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics#diff-bGliL3Byb21zY3JhcGUvZGlzY292ZXJ5L2NvbnN1bC9hcGkuZ28=) | `0.00% <0.00%> (ø)` | |
| [lib/promscrape/discovery/kubernetes/api.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1316/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics#diff-bGliL3Byb21zY3JhcGUvZGlzY292ZXJ5L2t1YmVybmV0ZXMvYXBpLmdv) | `21.21% <0.00%> (ø)` | |
| [lib/promscrape/discovery/openstack/api.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1316/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics#diff-bGliL3Byb21zY3JhcGUvZGlzY292ZXJ5L29wZW5zdGFjay9hcGkuZ28=) | `0.00% <0.00%> (ø)` | |
| [lib/promauth/config.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1316/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics#diff-bGliL3Byb21hdXRoL2NvbmZpZy5nbw==) | `49.12% <55.10%> (ø)` | |
| [lib/mergeset/table.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1316/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics#diff-bGliL21lcmdlc2V0L3RhYmxlLmdv) | `68.82% <0.00%> (-1.03%)` | :arrow_down: |
| [lib/storage/partition.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1316/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics#diff-bGliL3N0b3JhZ2UvcGFydGl0aW9uLmdv) | `65.40% <0.00%> (-0.24%)` | :arrow_down: |
| [app/vmselect/promql/aggr\_incremental.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1316/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics#diff-YXBwL3Ztc2VsZWN0L3Byb21xbC9hZ2dyX2luY3JlbWVudGFsLmdv) | `86.43% <0.00%> (ø)` | |
| [lib/promauth/util.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1316/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics#diff-bGliL3Byb21hdXRoL3V0aWwuZ28=) | `50.00% <0.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1316?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1316?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics). Last update [d173a93...e81b9a4](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1316?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics).


Patch:
diff --git a/app/vmagent/remotewrite/client.go b/app/vmagent/remotewrite/client.go
index 103e8848b..6cb87769d 100644
--- a/app/vmagent/remotewrite/client.go
+++ b/app/vmagent/remotewrite/client.go
@@ -44,6 +44,17 @@ var (
 		"If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url")
 	bearerToken = flagutil.NewArray("remoteWrite.bearerToken", "Optional bearer auth token to use for -remoteWrite.url. "+
 		"If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url")
+
+	clientID = flagutil.NewArray("remoteWrite.oauth2.clientID", "Optional OAuth2 clientID to use for -remoteWrite.url."+
+		"If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url")
+	clientSecret = flagutil.NewArray("remoteWrite.oauth2.clientSecret", "Optional OAuth2 clientSecret to use for -remoteWrite.url."+
+		"If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url")
+	clientSecretFile = flagutil.NewArray("remoteWrite.oauth2.clientSecretFile", "Optional OAuth2 clientSecretFile to use for -remoteWrite.url."+
+		"If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url")
+	tokenURL = flagutil.NewArray("remoteWrite.oauth2.tokenUrl", "Optional OAuth2 token url to use for -remoteWrite.url."+
+		"If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url")
+	oAuth2Scopes = flagutil.NewArray("remoteWrite.oauth2.scopes", "Optional OAuth2 scopes to use for -remoteWrite.url."+
+		"If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url")
 )
 
 type client struct {
@@ -53,6 +64,8 @@ type client struct {
 	fq             *persistentqueue.FastQueue
 	hc             *http.Client
 
+	authCfg *promauth.Config
+
 	rl rateLimiter
 
 	bytesSent       *metrics.Counter
@@ -72,6 +85,7 @@ func newClient(argIdx int, remoteWriteURL, sanitizedURL string, fq *persistentqu
 	if err != nil {
 		logger.Panicf("FATAL: cannot initialize TLS config: %s", err)
 	}
+
 	tr := &http.Transport{
 		Dial:                statDial,
 		TLSClientConfig:     tlsCfg,
@@ -108,10 +122,18 @@ func newClient(argIdx int, remoteWriteURL, sanitizedURL string, fq *persistentqu
 		}
 		authHeader = "Bearer " + token
 	}
+	authCfg, err := getAuthConfig(argIdx)
+	if err != nil {
+		logger.Fatalf("FATAL: cannot create OAuth2 config for remoteWrite idx: %d, err: %s", argIdx, err)
+	}
+	if authCfg != nil && authHeader != "" {
+		logger.Fatalf("`-remoteWrite.bearerToken`=%q  or `-remoteWrite.basicAuth.* cannot be set when `-remoteWrite.oauth2.*` flags are set", token)
+	}
 	c := &client{
 		sanitizedURL:   sanitizedURL,
 		remoteWriteURL: remoteWriteURL,
 		authHeader:     authHeader,
+		authCfg:        authCfg,
 		fq:             fq,
 		hc: &http.Client{
 			Transport: tr,
@@ -160,7 +182,7 @@ func getTLSConfig(argIdx int) (*tls.Config, error) {
 	if c.CAFile == "" && c.CertFile == "" && c.KeyFile == "" && c.ServerName == "" && !c.InsecureSkipVerify {
 		return nil, nil
 	}
-	cfg, err := promauth.NewConfig(".", nil, nil, "", "", c)
+	cfg, err := promauth.NewConfig(".", nil, nil, "", "", nil, c)
 	if err != nil {
 		return nil, fmt.Errorf("cannot populate TLS config: %w", err)
 	}
@@ -168,6 +190,25 @@ func getTLSConfig(argIdx int) (*tls.Config, error) {
 	return tlsCfg, nil
 }
 
+func getAuthConfig(argIdx int) (*promauth.Config, error) {
+
+	oAuth2Cfg := &promauth.OAuth2Config{
+		ClientID:         clientID.GetOptionalArg(argIdx),
+		ClientSecret:     clientSecret.GetOptionalArg(argIdx),
+		ClientSecretFile: clientSecretFile.GetOptionalArg(argIdx),
+		TokenURL:         tokenURL.GetOptionalArg(argIdx),
+		Scopes:           strings.Split(oAuth2Scopes.GetOptionalArg(argIdx), ";"),
+	}
+	if oAuth2Cfg.ClientSecretFile == "" && oAuth2Cfg.ClientSecret == "" {
+		return nil, nil
+	}
+	authCfg, err := promauth.NewConfig("", nil, nil, "", "", oAuth2Cfg, nil)
+	if err != nil {
+		return nil, fmt.Errorf("cannot populate OAuth2 config for remoteWrite idx: %d, err: %w", argIdx, err)
+	}
+	return authCfg, nil
+}
+
 func (c *client) runWorker() {
 	var ok bool
 	var block []byte
@@ -229,6 +270,11 @@ again:
 	if c.authHeader != "" {
 		req.Header.Set("Authorization", c.authHeader)
 	}
+	// add oauth2 header on best effort.
+	// remote storage may return error with incorrect authorization.
+	if c.authCfg != nil {
+		req.Header.Set("Authorization", c.authCfg.GetAuthHeader())
+	}
 
 	startTime := time.Now()
 	resp, err := c.hc.Do(req)
diff --git a/go.mod b/go.mod
index b355ad4ea..814963091 100644
--- a/go.mod
+++ b/go.mod
@@ -31,7 +31,7 @@ require (
 	github.com/valyala/gozstd v1.10.0
 	github.com/valyala/histogram v1.1.2
 	github.com/valyala/quicktemplate v1.6.3
-	golang.org/x/net v0.0.0-20210510120150-4163338589ed
+	golang.org/x/net v0.0.0-20210520170846-37e1c6afe023
 	golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c
 	golang.org/x/sys v0.0.0-20210514084401-e8d321eab015
 	google.golang.org/api v0.47.0
diff --git a/go.sum b/go.sum
index 67344b945..9b20abbb8 100644
--- a/go.sum
+++ b/go.sum
@@ -533,7 +533,7 @@ github.com/influxdata/flux v0.113.0/go.mod h1:3TJtvbm/Kwuo5/PEo5P6HUzwVg4bXWkb2w
 github.com/influxdata/httprouter v1.3.1-0.20191122104820-ee83e2772f69/go.mod h1:pwymjR6SrP3gD3pRj9RJwdl1j5s3doEEV8gS4X9qSzA=
 github.com/influxdata/influxdb v1.8.0/go.mod h1:SIzcnsjaHRFpmlxpJ4S3NT64qtEKYweNTUMb/vh0OMQ=
 github.com/influxdata/influxdb v1.8.3/go.mod h1:JugdFhsvvI8gadxOI6noqNeeBHvWNTbfYGtiAn+2jhI=
-github.com/influxdata/influxdb v1.9.0 h1:9z/aRmTpWT1rIm4EN+qTJTZqgEdLGZ4xRMgvA276UEA=
+github.com/influxdata/influxdb v1.9.0 h1:KefL3i2JdNgsZwKRlHkkV+sYs4ejJ+aGF6glBUoKKio=
 github.com/influxdata/influxdb v1.9.0/go.mod h1:UEe3MeD9AaP5rlPIes102IhYua3FhIWZuOXNHxDjSrI=
 github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
 github.com/influxdata/influxql v1.1.0/go.mod h1:KpVI7okXjK6PRi3Z5B+mtKZli+R1DnZgb3N+tzevNgo=
@@ -1033,6 +1033,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
 golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I=
 golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 h1:ADo5wSpq2gqaCGQWzk7S5vd//0iyyLeAratkEoG5dLE=
+golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
diff --git a/lib/promauth/config.go b/lib/promauth/config.go
index b89693301..02812c405 100644
--- a/lib/promauth/config.go
+++ b/lib/promauth/config.go
@@ -2,6 +2,7 @@ package promauth
 
 import (
 	"bytes"
+	"context"
 	"crypto/tls"
 	"crypto/x509"
 	"encoding/base64"
@@ -11,6 +12,8 @@ import (
 
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
+	"golang.org/x/oauth2"
+	"golang.org/x/oauth2/clientcredentials"
 )
 
 // TLSConfig represents TLS config.
@@ -46,6 +49,7 @@ type HTTPClientConfig struct {
 	BasicAuth       *BasicAuthConfig `yaml:"basic_auth,omitempty"`
 	BearerToken     string           `yaml:"bearer_token,omitempty"`
 	BearerTokenFile string           `yaml:"bearer_token_file,omitempty"`
+	OAuth2          *OAuth2Config    `yaml:"oauth2,omitempty"`
 	TLSConfig       *TLSConfig       `yaml:"tls_config,omitempty"`
 }
 
@@ -58,6 +62,69 @@ type ProxyClientConfig struct {
 	TLSConfig       *TLSConfig       `yaml:"proxy_tls_config,omitempty"`
 }
 
+// OAuth2Config represent OAuth2 configuration
+type OAuth2Config struct {
+	ClientID         string   `yaml:"client_id"`
+	ClientSecretFile string   `yaml:"client_secret_file"`
+	Scopes           []string `yaml:"scopes"`
+	TokenURL         string   `yaml:"token_url"`
+	// mu guards tokenSource and client Secret
+	mu           sync.Mutex
+	ClientSecret string `yaml:"client_secret"`
+	tokenSource  oauth2.TokenSource
+}
+
+func (o *OAuth2Config) refreshTokenSourceLocked() {
+	cfg := clientcredentials.Config{
+		ClientID:     o.ClientID,
+		ClientSecret: o.ClientSecret,
+		TokenURL:     o.TokenURL,
+		Scopes:       o.Scopes,
+	}
+	o.tokenSource = cfg.TokenSource(context.Background())
+}
+
+// validate checks given configs.
+func (o *OAuth2Config) validate() error {
+	if o.TokenURL == "" {
+		return fmt.Errorf("token url cannot be empty")
+	}
+	if o.ClientSecret == "" && o.ClientSecretFile == "" {
+		return fmt.Errorf("ClientSecret or ClientSecretFile must be set")
+	}
+	if o.ClientSecret != "" && o.ClientSecretFile != "" {
+		return fmt.Errorf("only one option can be set ClientSecret or ClientSecretFile, provided both")
+	}
+	return nil
+}
+
+func (o *OAuth2Config) getAuthHeader() (string, error) {
+	var needUpdate bool
+	if o.ClientSecretFile != "" {
+		newSecret, err := readPasswordFromFile(o.ClientSecretFile)
+		if err != nil {
+			return "", fmt.Errorf("cannot read OAuth2 config file with path: %s, err: %w", o.ClientSecretFile, err)
+		}
+		o.mu.Lock()
+		if o.ClientSecret != newSecret {
+			o.ClientSecret = newSecret
+			needUpdate = true
+		}
+		o.mu.Unlock()
+	}
+	o.mu.Lock()
+	defer o.mu.Unlock()
+	if needUpdate {
+		o.refreshTokenSourceLocked()
+	}
+	t, err := o.tokenSource.Token()
+	if err != nil {
+		return "", fmt.Errorf("cannot fetch token for OAuth2 client: %w", err)
+	}
+
+	return t.Type() + " " + t.AccessToken, nil
+}
+
 // Config is auth config.
 type Config struct {
 	// Optional TLS config
@@ -131,16 +198,16 @@ func (ac *Config) NewTLSConfig() *tls.Config {
 
 // NewConfig creates auth config for the given hcc.
 func (hcc *HTTPClientConfig) NewConfig(baseDir string) (*Config, error) {
-	return NewConfig(baseDir, hcc.Authorization, hcc.BasicAuth, hcc.BearerToken, hcc.BearerTokenFile, hcc.TLSConfig)
+	return NewConfig(baseDir, hcc.Authorization, hcc.BasicAuth, hcc.BearerToken, hcc.BearerTokenFile, hcc.OAuth2, hcc.TLSConfig)
 }
 
 // NewConfig creates auth config for the given pcc.
 func (pcc *ProxyClientConfig) NewConfig(baseDir string) (*Config, error) {
-	return NewConfig(baseDir, pcc.Authorization, pcc.BasicAuth, pcc.BearerToken, pcc.BearerTokenFile, pcc.TLSConfig)
+	return NewConfig(baseDir, pcc.Authorization, pcc.BasicAuth, pcc.BearerToken, pcc.BearerTokenFile, nil, pcc.TLSConfig)
 }
 
 // NewConfig creates auth config from the given args.
-func NewConfig(baseDir string, az *Authorization, basicAuth *BasicAuthConfig, bearerToken, bearerTokenFile string, tlsConfig *TLSConfig) (*Config, error) {
+func NewConfig(baseDir string, az *Authorization, basicAuth *BasicAuthConfig, bearerToken, bearerTokenFile string, oauth *OAuth2Config, tlsConfig *TLSConfig) (*Config, error) {
 	var getAuthHeader func() string
 	authDigest := ""
 	if az != nil {
@@ -230,6 +297,30 @@ func NewConfig(baseDir string, az *Authorization, basicAuth *BasicAuthConfig, be
 		}
 		authDigest = fmt.Sprintf("bearer(token=%q)", bearerToken)
 	}
+	if oauth != nil {
+		if getAuthHeader != nil {
+			return nil, fmt.Errorf("cannot simultaneously use `authorization`, `basic_auth, `bearer_token` and `ouath2`")
+		}
+		if err := oauth.validate(); err != nil {
+			return nil, err
+		}
+		if oauth.ClientSecretFile != "" {
+			secret, err := readPasswordFromFile(oauth.ClientSecretFile)
+			if err != nil {
+				return nil, err
+			}
+			oauth.ClientSecret = secret
+		}
+		oauth.refreshTokenSourceLocked()
+		getAuthHeader = func() string {
+			h, err := oauth.getAuthHeader()
+			if err != nil {
+				logger.Errorf("cannot get OAuth2 header: %s", err)
+				return ""
+			}
+			return h
+		}
+	}
 	var tlsRootCA *x509.CertPool
 	var tlsCertificate *tls.Certificate
 	tlsServerName := ""
diff --git a/lib/promscrape/discovery/consul/api.go b/lib/promscrape/discovery/consul/api.go
index 9ffac6945..3e95b4ae8 100644
--- a/lib/promscrape/discovery/consul/api.go
+++ b/lib/promscrape/discovery/consul/api.go
@@ -50,7 +50,7 @@ func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
 		}
 		token = ""
 	}
-	ac, err := promauth.NewConfig(baseDir, nil, ba, token, "", sdc.TLSConfig)
+	ac, err := promauth.NewConfig(baseDir, nil, ba, token, "", nil, sdc.TLSConfig)
 	if err != nil {
 		return nil, fmt.Errorf("cannot parse auth config: %w", err)
 	}
diff --git a/lib/promscrape/discovery/kubernetes/api.go b/lib/promscrape/discovery/kubernetes/api.go
index fa39666a8..ff7db4a45 100644
--- a/lib/promscrape/discovery/kubernetes/api.go
+++ b/lib/promscrape/discovery/kubernetes/api.go
@@ -43,7 +43,7 @@ func newAPIConfig(sdc *SDConfig, baseDir string, swcFunc ScrapeWorkConstructorFu
 		tlsConfig := promauth.TLSConfig{
 			CAFile: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt",
 		}
-		acNew, err := promauth.NewConfig(".", nil, nil, "", "/var/run/secrets/kubernetes.io/serviceaccount/token", &tlsConfig)
+		acNew, err := promauth.NewConfig(".", nil, nil, "", "/var/run/secrets/kubernetes.io/serviceaccount/token", nil, &tlsConfig)
 		if err != nil {
 			return nil, fmt.Errorf("cannot initialize service account auth: %w; probably, `kubernetes_sd_config->api_server` is missing in Prometheus configs?", err)
 		}
diff --git a/lib/promscrape/discovery/openstack/api.go b/lib/promscrape/discovery/openstack/api.go
index 8e29d1d0c..c8b8eb228 100644
--- a/lib/promscrape/discovery/openstack/api.go
+++ b/lib/promscrape/discovery/openstack/api.go
@@ -81,7 +81,7 @@ func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
 		port:         sdc.Port,
 	}
 	if sdc.TLSConfig != nil {
-		ac, err := promauth.NewConfig(baseDir, nil, nil, "", "", sdc.TLSConfig)
+		ac, err := promauth.NewConfig(baseDir, nil, nil, "", "", nil, sdc.TLSConfig)
 		if err != nil {
 			return nil, err
 		}
diff --git a/vendor/golang.org/x/oauth2/clientcredentials/clientcredentials.go b/vendor/golang.org/x/oauth2/clientcredentials/clientcredentials.go
new file mode 100644
index 000000000..7a0b9ed10
--- /dev/null
+++ b/vendor/golang.org/x/oauth2/clientcredentials/clientcredentials.go
@@ -0,0 +1,120 @@
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package clientcredentials implements the OAuth2.0 "client credentials" token flow,
+// also known as the "two-legged OAuth 2.0".
+//
+// This should be used when the client is acting on its own behalf or when the client
+// is the resource owner. It may also be used when requesting access to protected
+// resources based on an authorization previously arranged with the authorization
+// server.
+//
+// See https://tools.ietf.org/html/rfc6749#section-4.4
+package clientcredentials // import "golang.org/x/oauth2/clientcredentials"
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+
+	"golang.org/x/oauth2"
+	"golang.org/x/oauth2/internal"
+)
+
+// Config describes a 2-legged OAuth2 flow, with both the
+// client application information and the server's endpoint URLs.
+type Config struct {
+	// ClientID is the application's ID.
+	ClientID string
+
+	// ClientSecret is the application's secret.
+	ClientSecret string
+
+	// TokenURL is the resource server's token endpoint
+	// URL. This is a constant specific to each server.
+	TokenURL string
+
+	// Scope specifies optional requested permissions.
+	Scopes []string
+
+	// EndpointParams specifies additional parameters for requests to the token endpoint.
+	EndpointParams url.Values
+
+	// AuthStyle optionally specifies how the endpoint wants the
+	// client ID & client secret sent. The zero value means to
+	// auto-detect.
+	AuthStyle oauth2.AuthStyle
+}
+
+// Token uses client credentials to retrieve a token.
+//
+// The provided context optionally controls which HTTP client is used. See the oauth2.HTTPClient variable.
+func (c *Config) Token(ctx context.Context) (*oauth2.Token, error) {
+	return c.TokenSource(ctx).Token()
+}
+
+// Client returns an HTTP client using the provided token.
+// The token will auto-refresh as necessary.
+//
+// The provided context optionally controls which HTTP client
+// is returned. See the oauth2.HTTPClient variable.
+//
+// The returned Client and its Transport should not be modified.
+func (c *Config) Client(ctx context.Context) *http.Client {
+	return oauth2.NewClient(ctx, c.TokenSource(ctx))
+}
+
+// TokenSource returns a TokenSource that returns t until t expires,
+// automatically refreshing it as necessary using the provided context and the
+// client ID and client secret.
+//
+// Most users will use Config.Client instead.
+func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource {
+	source := &tokenSource{
+		ctx:  ctx,
+		conf: c,
+	}
+	return oauth2.ReuseTokenSource(nil, source)
+}
+
+type tokenSource struct {
+	ctx  context.Context
+	conf *Config
+}
+
+// Token refreshes the token by using a new client credentials request.
+// tokens received this way do not include a refresh token
+func (c *tokenSource) Token() (*oauth2.Token, error) {
+	v := url.Values{
+		"grant_type": {"client_credentials"},
+	}
+	if len(c.conf.Scopes) > 0 {
+		v.Set("scope", strings.Join(c.conf.Scopes, " "))
+	}
+	for k, p := range c.conf.EndpointParams {
+		// Allow grant_type to be overridden to allow interoperability with
+		// non-compliant implementations.
+		if _, ok := v[k]; ok && k != "grant_type" {
+			return nil, fmt.Errorf("oauth2: cannot overwrite parameter %q", k)
+		}
+		v[k] = p
+	}
+
+	tk, err := internal.RetrieveToken(c.ctx, c.conf.ClientID, c.conf.ClientSecret, c.conf.TokenURL, v, internal.AuthStyle(c.conf.AuthStyle))
+	if err != nil {
+		if rErr, ok := err.(*internal.RetrieveError); ok {
+			return nil, (*oauth2.RetrieveError)(rErr)
+		}
+		return nil, err
+	}
+	t := &oauth2.Token{
+		AccessToken:  tk.AccessToken,
+		TokenType:    tk.TokenType,
+		RefreshToken: tk.RefreshToken,
+		Expiry:       tk.Expiry,
+	}
+	return t.WithExtra(tk.Raw), nil
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 61dafd0a4..72586702c 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -235,7 +235,7 @@ golang.org/x/lint/golint
 # golang.org/x/mod v0.4.2
 golang.org/x/mod/module
 golang.org/x/mod/semver
-# golang.org/x/net v0.0.0-20210510120150-4163338589ed
+# golang.org/x/net v0.0.0-20210520170846-37e1c6afe023
 ## explicit
 golang.org/x/net/context
 golang.org/x/net/context/ctxhttp
@@ -251,6 +251,7 @@ golang.org/x/net/trace
 ## explicit
 golang.org/x/oauth2
 golang.org/x/oauth2/authhandler
+golang.org/x/oauth2/clientcredentials
 golang.org/x/oauth2/google
 golang.org/x/oauth2/google/internal/externalaccount
 golang.org/x/oauth2/internal


Test patch:
diff --git a/lib/promauth/config_test.go b/lib/promauth/config_test.go
new file mode 100644
index 000000000..0d438fd3f
--- /dev/null
+++ b/lib/promauth/config_test.go
@@ -0,0 +1,125 @@
+package promauth
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+)
+
+func TestNewConfig(t *testing.T) {
+	type args struct {
+		baseDir         string
+		az              *Authorization
+		basicAuth       *BasicAuthConfig
+		bearerToken     string
+		bearerTokenFile string
+		oauth           *OAuth2Config
+		tlsConfig       *TLSConfig
+	}
+	tests := []struct {
+		name    string
+		args    args
+		wantErr bool
+	}{
+		{
+			name: "OAuth2 config",
+			args: args{
+				oauth: &OAuth2Config{
+					ClientID:     "some-id",
+					ClientSecret: "some-secret",
+					TokenURL:     "http://localhost:8511",
+				},
+			},
+		},
+		{
+			name: "OAuth2 config with file",
+			args: args{
+				oauth: &OAuth2Config{
+					ClientID:         "some-id",
+					ClientSecretFile: "testdata/test_secretfile.txt",
+					TokenURL:         "http://localhost:8511",
+				},
+			},
+		},
+
+		{
+			name: "OAuth2 want err",
+			args: args{
+				oauth: &OAuth2Config{
+					ClientID:         "some-id",
+					ClientSecret:     "some-secret",
+					ClientSecretFile: "testdata/test_secretfile.txt",
+					TokenURL:         "http://localhost:8511",
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "basic Auth config",
+			args: args{
+				basicAuth: &BasicAuthConfig{
+					Username: "user",
+					Password: "password",
+				},
+			},
+		},
+		{
+			name: "basic Auth config with file",
+			args: args{
+				basicAuth: &BasicAuthConfig{
+					Username:     "user",
+					PasswordFile: "testdata/test_secretfile.txt",
+				},
+			},
+		},
+		{
+			name: "want Authorization",
+			args: args{
+				az: &Authorization{
+					Type:        "Bearer ",
+					Credentials: "Value",
+				},
+			},
+		},
+		{
+			name: "token file",
+			args: args{
+				bearerTokenFile: "testdata/test_secretfile.txt",
+			},
+		},
+		{
+			name: "token with tls",
+			args: args{
+				bearerToken: "some-token",
+				tlsConfig: &TLSConfig{
+					InsecureSkipVerify: true,
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.args.oauth != nil {
+				r := http.NewServeMux()
+				r.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
+					w.Header().Set("Content-Type", "application/json")
+					w.Write([]byte(`{"access_token":"some-token","token_type": "Bearer "}`))
+
+				})
+				mock := httptest.NewServer(r)
+				tt.args.oauth.TokenURL = mock.URL
+			}
+			got, err := NewConfig(tt.args.baseDir, tt.args.az, tt.args.basicAuth, tt.args.bearerToken, tt.args.bearerTokenFile, tt.args.oauth, tt.args.tlsConfig)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("NewConfig() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if got != nil {
+				if ah := got.GetAuthHeader(); ah == "" {
+					t.Fatalf("unexpected empty auth header")
+				}
+			}
+
+		})
+	}
+}
diff --git a/lib/promauth/testdata/test_secretfile.txt b/lib/promauth/testdata/test_secretfile.txt
new file mode 100644
index 000000000..ce412a8da
--- /dev/null
+++ b/lib/promauth/testdata/test_secretfile.txt
@@ -0,0 +1,1 @@
+secret-content
\ No newline at end of file