Skip to content

Commit 10f0b67

Browse files
authored
OAuth 2.0 Client (#287)
* Added OAuth 2 Signed-off-by: Levi Harrison <[email protected]>
1 parent 3b362f5 commit 10f0b67

File tree

5 files changed

+122
-6
lines changed

5 files changed

+122
-6
lines changed

config/http_config.go

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import (
3232

3333
"github.com/mwitkow/go-conntrack"
3434
"golang.org/x/net/http2"
35+
"golang.org/x/oauth2"
36+
"golang.org/x/oauth2/clientcredentials"
3537
"gopkg.in/yaml.v2"
3638
)
3739

@@ -108,12 +110,23 @@ func (u URL) MarshalYAML() (interface{}, error) {
108110
return nil, nil
109111
}
110112

113+
// OAuth2 is the oauth2 client configuration.
114+
type OAuth2 struct {
115+
ClientID string `yaml:"client_id"`
116+
ClientSecret Secret `yaml:"client_secret"`
117+
Scopes []string `yaml:"scopes,omitempty"`
118+
TokenURL string `yaml:"token_url"`
119+
EndpointParams map[string]string `yaml:"endpoint_params,omitempty"`
120+
}
121+
111122
// HTTPClientConfig configures an HTTP client.
112123
type HTTPClientConfig struct {
113124
// The HTTP basic authentication credentials for the targets.
114125
BasicAuth *BasicAuth `yaml:"basic_auth,omitempty"`
115126
// The HTTP authorization credentials for the targets.
116127
Authorization *Authorization `yaml:"authorization,omitempty"`
128+
// The OAuth2 client credentials used to fetch a token for the targets.
129+
OAuth2 *OAuth2 `yaml:"oauth2,omitempty"`
117130
// The bearer token for the targets. Deprecated in favour of
118131
// Authorization.Credentials.
119132
BearerToken Secret `yaml:"bearer_token,omitempty"`
@@ -148,8 +161,8 @@ func (c *HTTPClientConfig) Validate() error {
148161
if len(c.BearerToken) > 0 && len(c.BearerTokenFile) > 0 {
149162
return fmt.Errorf("at most one of bearer_token & bearer_token_file must be configured")
150163
}
151-
if c.BasicAuth != nil && (len(c.BearerToken) > 0 || len(c.BearerTokenFile) > 0) {
152-
return fmt.Errorf("at most one of basic_auth, bearer_token & bearer_token_file must be configured")
164+
if (c.BasicAuth != nil || c.OAuth2 != nil) && (len(c.BearerToken) > 0 || len(c.BearerTokenFile) > 0) {
165+
return fmt.Errorf("at most one of basic_auth, oauth2, bearer_token & bearer_token_file must be configured")
153166
}
154167
if c.BasicAuth != nil && (string(c.BasicAuth.Password) != "" && c.BasicAuth.PasswordFile != "") {
155168
return fmt.Errorf("at most one of basic_auth password & password_file must be configured")
@@ -168,8 +181,8 @@ func (c *HTTPClientConfig) Validate() error {
168181
if strings.ToLower(c.Authorization.Type) == "basic" {
169182
return fmt.Errorf(`authorization type cannot be set to "basic", use "basic_auth" instead`)
170183
}
171-
if c.BasicAuth != nil {
172-
return fmt.Errorf("at most one of basic_auth & authorization must be configured")
184+
if c.BasicAuth != nil || c.OAuth2 != nil {
185+
return fmt.Errorf("at most one of basic_auth, oauth2 & authorization must be configured")
173186
}
174187
} else {
175188
if len(c.BearerToken) > 0 {
@@ -183,6 +196,9 @@ func (c *HTTPClientConfig) Validate() error {
183196
c.BearerTokenFile = ""
184197
}
185198
}
199+
if c.BasicAuth != nil && c.OAuth2 != nil {
200+
return fmt.Errorf("at most one of basic_auth, oauth2 & authorization must be configured")
201+
}
186202
return nil
187203
}
188204

@@ -329,6 +345,10 @@ func NewRoundTripperFromConfig(cfg HTTPClientConfig, name string, optFuncs ...HT
329345
if cfg.BasicAuth != nil {
330346
rt = NewBasicAuthRoundTripper(cfg.BasicAuth.Username, cfg.BasicAuth.Password, cfg.BasicAuth.PasswordFile, rt)
331347
}
348+
349+
if cfg.OAuth2 != nil {
350+
rt = cfg.OAuth2.NewOAuth2RoundTripper(context.Background(), rt)
351+
}
332352
// Return a new configured RoundTripper.
333353
return rt, nil
334354
}
@@ -442,6 +462,32 @@ func (rt *basicAuthRoundTripper) CloseIdleConnections() {
442462
}
443463
}
444464

465+
func (c *OAuth2) NewOAuth2RoundTripper(ctx context.Context, next http.RoundTripper) http.RoundTripper {
466+
config := &clientcredentials.Config{
467+
ClientID: c.ClientID,
468+
ClientSecret: string(c.ClientSecret),
469+
Scopes: c.Scopes,
470+
TokenURL: c.TokenURL,
471+
EndpointParams: mapToValues(c.EndpointParams),
472+
}
473+
474+
tokenSource := config.TokenSource(ctx)
475+
476+
return &oauth2.Transport{
477+
Base: next,
478+
Source: tokenSource,
479+
}
480+
}
481+
482+
func mapToValues(m map[string]string) url.Values {
483+
v := url.Values{}
484+
for name, value := range m {
485+
v.Set(name, value)
486+
}
487+
488+
return v
489+
}
490+
445491
// cloneRequest returns a clone of the provided *http.Request.
446492
// The clone is a shallow copy of the struct and its Header map.
447493
func cloneRequest(r *http.Request) *http.Request {

config/http_config_test.go

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"context"
2020
"crypto/tls"
2121
"crypto/x509"
22+
"encoding/json"
2223
"errors"
2324
"fmt"
2425
"io/ioutil"
@@ -76,7 +77,7 @@ var invalidHTTPClientConfigs = []struct {
7677
},
7778
{
7879
httpClientConfigFile: "testdata/http.conf.empty.bad.yml",
79-
errMsg: "at most one of basic_auth, bearer_token & bearer_token_file must be configured",
80+
errMsg: "at most one of basic_auth, oauth2, bearer_token & bearer_token_file must be configured",
8081
},
8182
{
8283
httpClientConfigFile: "testdata/http.conf.basic-auth.too-much.bad.yaml",
@@ -92,7 +93,11 @@ var invalidHTTPClientConfigs = []struct {
9293
},
9394
{
9495
httpClientConfigFile: "testdata/http.conf.basic-auth-and-auth-creds.too-much.bad.yaml",
95-
errMsg: "at most one of basic_auth & authorization must be configured",
96+
errMsg: "at most one of basic_auth, oauth2 & authorization must be configured",
97+
},
98+
{
99+
httpClientConfigFile: "testdata/http.conf.basic-auth-and-oauth2.too-much.bad.yaml",
100+
errMsg: "at most one of basic_auth, oauth2 & authorization must be configured",
96101
},
97102
{
98103
httpClientConfigFile: "testdata/http.conf.auth-creds-no-basic.bad.yaml",
@@ -1087,3 +1092,59 @@ func NewRoundTripCheckRequest(checkRequest func(*http.Request), theResponse *htt
10871092
theResponse: theResponse,
10881093
theError: theError}}
10891094
}
1095+
1096+
type testServerResponse struct {
1097+
AccessToken string `json:"access_token"`
1098+
TokenType string `json:"token_type"`
1099+
}
1100+
1101+
func TestOAuth2(t *testing.T) {
1102+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1103+
res, _ := json.Marshal(testServerResponse{
1104+
AccessToken: "12345",
1105+
TokenType: "Bearer",
1106+
})
1107+
w.Header().Add("Content-Type", "application/json")
1108+
_, _ = w.Write(res)
1109+
}))
1110+
defer ts.Close()
1111+
1112+
var yamlConfig = fmt.Sprintf(`
1113+
client_id: 1
1114+
client_secret: 2
1115+
scopes:
1116+
- A
1117+
- B
1118+
token_url: %s
1119+
endpoint_params:
1120+
hi: hello
1121+
`, ts.URL)
1122+
expectedConfig := OAuth2{
1123+
ClientID: "1",
1124+
ClientSecret: "2",
1125+
Scopes: []string{"A", "B"},
1126+
EndpointParams: map[string]string{"hi": "hello"},
1127+
TokenURL: ts.URL,
1128+
}
1129+
1130+
var unmarshalledConfig OAuth2
1131+
err := yaml.Unmarshal([]byte(yamlConfig), &unmarshalledConfig)
1132+
if err != nil {
1133+
t.Fatalf("Expected no error unmarshalling yaml, got %v", err)
1134+
}
1135+
if !reflect.DeepEqual(unmarshalledConfig, expectedConfig) {
1136+
t.Fatalf("Got unmarshalled config %q, expected %q", unmarshalledConfig, expectedConfig)
1137+
}
1138+
1139+
rt := expectedConfig.NewOAuth2RoundTripper(context.Background(), http.DefaultTransport)
1140+
1141+
client := http.Client{
1142+
Transport: rt,
1143+
}
1144+
resp, _ := client.Get(ts.URL)
1145+
1146+
authorization := resp.Request.Header.Get("Authorization")
1147+
if authorization != "Bearer 12345" {
1148+
t.Fatalf("Expected authorization header to be 'Bearer 12345', got '%s'", authorization)
1149+
}
1150+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
basic_auth:
2+
username: user
3+
password: foo
4+
oauth2:
5+
client_id: foo
6+
client_secret: bar

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require (
1313
github.com/prometheus/client_model v0.2.0
1414
github.com/sirupsen/logrus v1.6.0
1515
golang.org/x/net v0.0.0-20200625001655-4c5254603344
16+
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421
1617
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae
1718
gopkg.in/alecthomas/kingpin.v2 v2.2.6
1819
gopkg.in/yaml.v2 v2.3.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLL
313313
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
314314
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
315315
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
316+
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI=
316317
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
317318
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
318319
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -365,6 +366,7 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
365366
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
366367
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
367368
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
369+
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
368370
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
369371
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
370372
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=

0 commit comments

Comments
 (0)