Skip to content

Commit

Permalink
OAuth 2.0 Client (#287)
Browse files Browse the repository at this point in the history
* Added OAuth 2

Signed-off-by: Levi Harrison <[email protected]>
  • Loading branch information
LeviHarrison authored Apr 26, 2021
1 parent 3b362f5 commit 10f0b67
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 6 deletions.
54 changes: 50 additions & 4 deletions config/http_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import (

"github.com/mwitkow/go-conntrack"
"golang.org/x/net/http2"
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
"gopkg.in/yaml.v2"
)

Expand Down Expand Up @@ -108,12 +110,23 @@ func (u URL) MarshalYAML() (interface{}, error) {
return nil, nil
}

// OAuth2 is the oauth2 client configuration.
type OAuth2 struct {
ClientID string `yaml:"client_id"`
ClientSecret Secret `yaml:"client_secret"`
Scopes []string `yaml:"scopes,omitempty"`
TokenURL string `yaml:"token_url"`
EndpointParams map[string]string `yaml:"endpoint_params,omitempty"`
}

// HTTPClientConfig configures an HTTP client.
type HTTPClientConfig struct {
// The HTTP basic authentication credentials for the targets.
BasicAuth *BasicAuth `yaml:"basic_auth,omitempty"`
// The HTTP authorization credentials for the targets.
Authorization *Authorization `yaml:"authorization,omitempty"`
// The OAuth2 client credentials used to fetch a token for the targets.
OAuth2 *OAuth2 `yaml:"oauth2,omitempty"`
// The bearer token for the targets. Deprecated in favour of
// Authorization.Credentials.
BearerToken Secret `yaml:"bearer_token,omitempty"`
Expand Down Expand Up @@ -148,8 +161,8 @@ func (c *HTTPClientConfig) Validate() error {
if len(c.BearerToken) > 0 && len(c.BearerTokenFile) > 0 {
return fmt.Errorf("at most one of bearer_token & bearer_token_file must be configured")
}
if c.BasicAuth != nil && (len(c.BearerToken) > 0 || len(c.BearerTokenFile) > 0) {
return fmt.Errorf("at most one of basic_auth, bearer_token & bearer_token_file must be configured")
if (c.BasicAuth != nil || c.OAuth2 != nil) && (len(c.BearerToken) > 0 || len(c.BearerTokenFile) > 0) {
return fmt.Errorf("at most one of basic_auth, oauth2, bearer_token & bearer_token_file must be configured")
}
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")
Expand All @@ -168,8 +181,8 @@ func (c *HTTPClientConfig) Validate() error {
if strings.ToLower(c.Authorization.Type) == "basic" {
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")
if c.BasicAuth != nil || c.OAuth2 != nil {
return fmt.Errorf("at most one of basic_auth, oauth2 & authorization must be configured")
}
} else {
if len(c.BearerToken) > 0 {
Expand All @@ -183,6 +196,9 @@ func (c *HTTPClientConfig) Validate() error {
c.BearerTokenFile = ""
}
}
if c.BasicAuth != nil && c.OAuth2 != nil {
return fmt.Errorf("at most one of basic_auth, oauth2 & authorization must be configured")
}
return nil
}

Expand Down Expand Up @@ -329,6 +345,10 @@ func NewRoundTripperFromConfig(cfg HTTPClientConfig, name string, optFuncs ...HT
if cfg.BasicAuth != nil {
rt = NewBasicAuthRoundTripper(cfg.BasicAuth.Username, cfg.BasicAuth.Password, cfg.BasicAuth.PasswordFile, rt)
}

if cfg.OAuth2 != nil {
rt = cfg.OAuth2.NewOAuth2RoundTripper(context.Background(), rt)
}
// Return a new configured RoundTripper.
return rt, nil
}
Expand Down Expand Up @@ -442,6 +462,32 @@ func (rt *basicAuthRoundTripper) CloseIdleConnections() {
}
}

func (c *OAuth2) NewOAuth2RoundTripper(ctx context.Context, next http.RoundTripper) http.RoundTripper {
config := &clientcredentials.Config{
ClientID: c.ClientID,
ClientSecret: string(c.ClientSecret),
Scopes: c.Scopes,
TokenURL: c.TokenURL,
EndpointParams: mapToValues(c.EndpointParams),
}

tokenSource := config.TokenSource(ctx)

return &oauth2.Transport{
Base: next,
Source: tokenSource,
}
}

func mapToValues(m map[string]string) url.Values {
v := url.Values{}
for name, value := range m {
v.Set(name, value)
}

return v
}

// cloneRequest returns a clone of the provided *http.Request.
// The clone is a shallow copy of the struct and its Header map.
func cloneRequest(r *http.Request) *http.Request {
Expand Down
65 changes: 63 additions & 2 deletions config/http_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
Expand Down Expand Up @@ -76,7 +77,7 @@ var invalidHTTPClientConfigs = []struct {
},
{
httpClientConfigFile: "testdata/http.conf.empty.bad.yml",
errMsg: "at most one of basic_auth, bearer_token & bearer_token_file must be configured",
errMsg: "at most one of basic_auth, oauth2, bearer_token & bearer_token_file must be configured",
},
{
httpClientConfigFile: "testdata/http.conf.basic-auth.too-much.bad.yaml",
Expand All @@ -92,7 +93,11 @@ var invalidHTTPClientConfigs = []struct {
},
{
httpClientConfigFile: "testdata/http.conf.basic-auth-and-auth-creds.too-much.bad.yaml",
errMsg: "at most one of basic_auth & authorization must be configured",
errMsg: "at most one of basic_auth, oauth2 & authorization must be configured",
},
{
httpClientConfigFile: "testdata/http.conf.basic-auth-and-oauth2.too-much.bad.yaml",
errMsg: "at most one of basic_auth, oauth2 & authorization must be configured",
},
{
httpClientConfigFile: "testdata/http.conf.auth-creds-no-basic.bad.yaml",
Expand Down Expand Up @@ -1087,3 +1092,59 @@ func NewRoundTripCheckRequest(checkRequest func(*http.Request), theResponse *htt
theResponse: theResponse,
theError: theError}}
}

type testServerResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
}

func TestOAuth2(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
res, _ := json.Marshal(testServerResponse{
AccessToken: "12345",
TokenType: "Bearer",
})
w.Header().Add("Content-Type", "application/json")
_, _ = w.Write(res)
}))
defer ts.Close()

var yamlConfig = fmt.Sprintf(`
client_id: 1
client_secret: 2
scopes:
- A
- B
token_url: %s
endpoint_params:
hi: hello
`, ts.URL)
expectedConfig := OAuth2{
ClientID: "1",
ClientSecret: "2",
Scopes: []string{"A", "B"},
EndpointParams: map[string]string{"hi": "hello"},
TokenURL: ts.URL,
}

var unmarshalledConfig OAuth2
err := yaml.Unmarshal([]byte(yamlConfig), &unmarshalledConfig)
if err != nil {
t.Fatalf("Expected no error unmarshalling yaml, got %v", err)
}
if !reflect.DeepEqual(unmarshalledConfig, expectedConfig) {
t.Fatalf("Got unmarshalled config %q, expected %q", unmarshalledConfig, expectedConfig)
}

rt := expectedConfig.NewOAuth2RoundTripper(context.Background(), http.DefaultTransport)

client := http.Client{
Transport: rt,
}
resp, _ := client.Get(ts.URL)

authorization := resp.Request.Header.Get("Authorization")
if authorization != "Bearer 12345" {
t.Fatalf("Expected authorization header to be 'Bearer 12345', got '%s'", authorization)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
basic_auth:
username: user
password: foo
oauth2:
client_id: foo
client_secret: bar
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
github.com/prometheus/client_model v0.2.0
github.com/sirupsen/logrus v1.6.0
golang.org/x/net v0.0.0-20200625001655-4c5254603344
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/yaml.v2 v2.3.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down Expand Up @@ -365,6 +366,7 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
Expand Down

0 comments on commit 10f0b67

Please sign in to comment.