Repository: prometheus/common
Pull request #386 (2022-06-28 10:51:41)

Add User-Agent header to oauth2 requests
This PR adds the ability to specify the User-Agent header for oauth2 requests. 

The default User-Agent: `go-http-client/1.1` is ratelimited by many sites which leads to a lot of oauth2 requests failing. 

Fixes: [384](https://github.com/prometheus/common/issues/384)


Patch:
diff --git a/config/http_config.go b/config/http_config.go
index 063edde..803f6a1 100644
--- a/config/http_config.go
+++ b/config/http_config.go
@@ -224,6 +224,8 @@ type OAuth2 struct {
 	ProxyURL URL `yaml:"proxy_url,omitempty" json:"proxy_url,omitempty"`
 	// TLSConfig is used to connect to the token URL.
 	TLSConfig TLSConfig `yaml:"tls_config,omitempty"`
+	// UserAgent is used to set a custom User-Agent http header while making the oauth request.
+	UserAgent string `yaml:"user_agent,omitempty" json:"user_agent,omitempty"`
 }
 
 // SetDirectory joins any relative file paths with dir.
@@ -681,6 +683,10 @@ func (rt *oauth2RoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
 			}
 		}
 
+		if rt.config.UserAgent != "" {
+			t = NewUserAgentRoundTripper(rt.config.UserAgent, t)
+		}
+
 		ctx := context.WithValue(context.Background(), oauth2.HTTPClient, &http.Client{Transport: t})
 		tokenSource := config.TokenSource(ctx)
 
@@ -911,6 +917,28 @@ func (t *tlsRoundTripper) CloseIdleConnections() {
 	}
 }
 
+type userAgentRoundTripper struct {
+	userAgent string
+	rt        http.RoundTripper
+}
+
+// NewUserAgentRoundTripper adds the user agent every request header.
+func NewUserAgentRoundTripper(userAgent string, rt http.RoundTripper) http.RoundTripper {
+	return &userAgentRoundTripper{userAgent, rt}
+}
+
+func (rt *userAgentRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+	req = cloneRequest(req)
+	req.Header.Set("User-Agent", rt.userAgent)
+	return rt.rt.RoundTrip(req)
+}
+
+func (rt *userAgentRoundTripper) CloseIdleConnections() {
+	if ci, ok := rt.rt.(closeIdler); ok {
+		ci.CloseIdleConnections()
+	}
+}
+
 func (c HTTPClientConfig) String() string {
 	b, err := yaml.Marshal(c)
 	if err != nil {


Test patch:
diff --git a/config/http_config_test.go b/config/http_config_test.go
index 06eb6d0..9c9c623 100644
--- a/config/http_config_test.go
+++ b/config/http_config_test.go
@@ -1183,6 +1183,12 @@ type oauth2TestServerResponse struct {
 
 func TestOAuth2(t *testing.T) {
 	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path == "/token" {
+			if r.Header.Get("User-Agent") != "myuseragent" {
+				t.Fatalf("Expected User-Agent header in oauth request to be 'myuseragent', got '%s'", r.Header.Get("User-Agent"))
+			}
+		}
+
 		res, _ := json.Marshal(oauth2TestServerResponse{
 			AccessToken: "12345",
 			TokenType:   "Bearer",
@@ -1198,7 +1204,8 @@ client_secret: 2
 scopes:
  - A
  - B
-token_url: %s
+token_url: %s/token
+user_agent: myuseragent
 endpoint_params:
  hi: hello
 `, ts.URL)
@@ -1207,7 +1214,8 @@ endpoint_params:
 		ClientSecret:   "2",
 		Scopes:         []string{"A", "B"},
 		EndpointParams: map[string]string{"hi": "hello"},
-		TokenURL:       ts.URL,
+		TokenURL:       fmt.Sprintf("%s/token", ts.URL),
+		UserAgent:      "myuseragent",
 	}
 
 	var unmarshalledConfig OAuth2
@@ -1488,3 +1496,10 @@ func TestOAuth2Proxy(t *testing.T) {
 		t.Errorf("Error loading OAuth2 client config: %v", err)
 	}
 }
+
+func TestOAuth2UserAgent(t *testing.T) {
+	_, _, err := LoadHTTPConfigFile("testdata/http.conf.oauth2-user-agent.good.yml")
+	if err != nil {
+		t.Errorf("Error loading OAuth2 client config: %v", err)
+	}
+}
diff --git a/config/testdata/http.conf.oauth2-user-agent.good.yml b/config/testdata/http.conf.oauth2-user-agent.good.yml
new file mode 100644
index 0000000..a0a407f
--- /dev/null
+++ b/config/testdata/http.conf.oauth2-user-agent.good.yml
@@ -0,0 +1,5 @@
+oauth2:
+  client_id: "myclient"
+  client_secret: "mysecret"
+  token_url: "http://auth"
+  user_agent: "myuseragent"