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

[OTLPHTTP Exporter] OAuth2 Client Credentials Authorization support for Exporters using HTTP Clients #2464

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2fe1f66
Added OAuth Support to HTTPClients
pavankrish123 Feb 4, 2021
a4b04fb
added changes per suggestion from the review
pavankrish123 Feb 12, 2021
fb7d1d7
more changes per review
pavankrish123 Feb 12, 2021
0515670
added changes per review
pavankrish123 Feb 13, 2021
a7a1d79
Update config/configclientauth/README.md
pavankrish123 Feb 16, 2021
ab2b5b3
Update config/configclientauth/README.md
pavankrish123 Feb 16, 2021
f164c68
Update config/confighttp/confighttp.go
pavankrish123 Feb 16, 2021
7cf48e2
Update config/configclientauth/README.md
pavankrish123 Feb 16, 2021
eb7e725
Update config/configclientauth/configoauth2.go
pavankrish123 Feb 16, 2021
c9c11ef
Update config/configclientauth/README.md
pavankrish123 Feb 16, 2021
9af1ed5
added security key warnings
pavankrish123 Feb 16, 2021
7b94a85
added security key warnings
pavankrish123 Feb 16, 2021
850baaf
added sugegsted text
pavankrish123 Feb 16, 2021
468417c
added sugegsted text
pavankrish123 Feb 16, 2021
f6f630d
added suggested review comments
pavankrish123 Feb 17, 2021
e35c06e
fixed merge conflicts
pavankrish123 Feb 17, 2021
bee9d3c
reverted go.mod and go.sum
pavankrish123 Feb 17, 2021
0748fe8
fixed files
pavankrish123 Feb 17, 2021
75837ef
fixed files
pavankrish123 Feb 17, 2021
18b9fc7
merged master
pavankrish123 Mar 29, 2021
807fafb
fixed merge conflicts
pavankrish123 Mar 29, 2021
6f9aa9e
fixed stale metadata
pavankrish123 Mar 29, 2021
af0ba4c
Merge branch 'main' into oauth
pavankrish123 Apr 15, 2021
883ac54
Merge branch 'master' into oauth
pavankrish123 Apr 29, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions config/confighttp/confighttp.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (

"github.com/rs/cors"

"go.opentelemetry.io/collector/config/configoauth2"
"go.opentelemetry.io/collector/config/configtls"
"go.opentelemetry.io/collector/internal/middleware"
)
Expand All @@ -48,6 +49,9 @@ type HTTPClientSettings struct {

// Custom Round Tripper to allow for individual components to intercept HTTP requests
CustomRoundTripper func(next http.RoundTripper) (http.RoundTripper, error)

// OAuth2 Client Credentials Configuration
pavankrish123 marked this conversation as resolved.
Show resolved Hide resolved
OAuth2ClientCredentials *configoauth2.OAuth2ClientCredentials `mapstructure:"oauth2,omitempty"`
}

func (hcs *HTTPClientSettings) ToClient() (*http.Client, error) {
Expand All @@ -67,13 +71,21 @@ func (hcs *HTTPClientSettings) ToClient() (*http.Client, error) {
}

clientTransport := (http.RoundTripper)(transport)

if len(hcs.Headers) > 0 {
clientTransport = &headerRoundTripper{
transport: transport,
headers: hcs.Headers,
}
}

if hcs.OAuth2ClientCredentials != nil {
clientTransport, err = hcs.OAuth2ClientCredentials.RoundTripper(clientTransport)
if err != nil {
return nil, err
}
}

if hcs.CustomRoundTripper != nil {
clientTransport, err = hcs.CustomRoundTripper(clientTransport)
if err != nil {
Expand Down
67 changes: 67 additions & 0 deletions config/confighttp/confighttp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"go.opentelemetry.io/collector/config/configoauth2"
"go.opentelemetry.io/collector/config/configtls"
)

Expand Down Expand Up @@ -398,6 +399,7 @@ func TestHttpHeaders(t *testing.T) {
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -425,3 +427,68 @@ func TestHttpHeaders(t *testing.T) {
})
}
}

func TestOAuth2SettingsIncomplete(t *testing.T) {
setting := HTTPClientSettings{
Endpoint: "https://some-server/v1/endpoint",
pavankrish123 marked this conversation as resolved.
Show resolved Hide resolved
TLSSetting: configtls.TLSClientSetting{},
OAuth2ClientCredentials: &configoauth2.OAuth2ClientCredentials{
ClientID: "testclientid",
TokenURL: fmt.Sprintf("%s/v1/token", "https://some-server/v1/token"),
pavankrish123 marked this conversation as resolved.
Show resolved Hide resolved
Scopes: []string{"test.resource.read"},
},
}
client, err := setting.ToClient()
assert.Error(t, err)
assert.Nil(t, client)
}

func TestOauth2SettingsWithHeaders(t *testing.T) {
pavankrish123 marked this conversation as resolved.
Show resolved Hide resolved
mux := http.NewServeMux()
server := httptest.NewUnstartedServer(mux)
serverURL, _ := url.Parse(server.URL)

token := "someauthtesttoken"
testHeaders := map[string]string{
"header1": "value1",
}

mux.HandleFunc("/v1/token", func(writer http.ResponseWriter, request *http.Request) {
_, _ = writer.Write([]byte(token))
writer.WriteHeader(http.StatusOK)
})

mux.HandleFunc("/v1/endpoint", func(writer http.ResponseWriter, request *http.Request) {
for k, v := range testHeaders {
assert.Equal(t, request.Header.Get(k), v)
}
assert.Equal(t, request.Header.Get("Authorization"), fmt.Sprintf("Bearer %s", token))
writer.WriteHeader(200)
})

setting := HTTPClientSettings{
Endpoint: fmt.Sprintf("%s/v1/endpoint", serverURL.String()),
TLSSetting: configtls.TLSClientSetting{},
ReadBufferSize: 0,
WriteBufferSize: 0,
Timeout: 0,
Headers: testHeaders,
OAuth2ClientCredentials: &configoauth2.OAuth2ClientCredentials{
ClientID: "testclientid",
ClientSecret: "testclientsecert",
TokenURL: fmt.Sprintf("%s/v1/token", serverURL.String()),
Scopes: []string{"test.resource.read"},
},
}

server.Start()
defer server.Close()

client, err := setting.ToClient()
assert.NoError(t, err)
assert.NotNil(t, client)

req, err := http.NewRequest("GET", setting.Endpoint, nil)
assert.NoError(t, err)
_, _ = client.Do(req)
}
31 changes: 31 additions & 0 deletions config/configoauth2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# OAuth2 Client Credential Settings

This module provides HTTP client types to be configured to perform authorization for requests using OAuth2 Client Credentials.

OAuth2 Client Credentials library exposes a [variety of settings](https://pkg.go.dev/golang.org/x/oauth2/clientcredentials)
and implements the OAuth2.0 ["client credentials"](https://tools.ietf.org/html/rfc6749#section-1.3.4)
token flow, also known as the "two-legged OAuth 2.0" or machine to machine authorisation. This should be used when the client is acting on its own behalf.

When enabled as a part of HTTP Client Configuration, an HTTP Client with OAuth2 supported authorization transport is returned. The client
manages the token auto refresh

## OAuth2 Client Credentials Configuration

- [`client_id`](https://tools.ietf.org/html/rfc6749#section-2.2): The unique identifier issued by authorization server to the registered client.
- `client_secret`: Registered clients secret.
- [`token_url`](https://tools.ietf.org/html/rfc6749#section-3.2): endpoint which is used by the client to obtain an access token by
presenting its authorization grant or refresh token
- `scopes`: optional list of requested resource permissions


Example:
```yaml
exporters:
otlphttp:
endpoint: http://localhost:9000
oauth2:
client_id: someclientidentifier
client_secret: someclientsecret
token_url: https://autz.server/oauth2/default/v1/token
scopes: ["some.resource.read"]
```
69 changes: 69 additions & 0 deletions config/configoauth2/configoauth2client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package configoauth2

import (
"context"
"errors"
"net/http"

"golang.org/x/oauth2"
pavankrish123 marked this conversation as resolved.
Show resolved Hide resolved
"golang.org/x/oauth2/clientcredentials"
)

var (
errNoClientIDProvided = errors.New("no ClientID provided in OAuth Client Credentials configuration")
errNoTokenURLProvided = errors.New("no TokenURL provided in OAuth Client Credentials configuration")
errNoClientSecretProvided = errors.New("no ClientSecret provided in OAuth Client Credentials configuration")
)

// OAuth2ClientCredentials stores the configuration for OAuth2 Client Credentials (2-legged OAuth2 flow) setup
type OAuth2ClientCredentials struct {
// ClientID is the application's ID.
ClientID string `mapstructure:"client_id"`

// ClientSecret is the application's secret.
ClientSecret string `mapstructure:"client_secret"`

// TokenURL is the resource server's token endpoint
// URL. This is a constant specific to each server.
TokenURL string `mapstructure:"token_url"`

// Scope specifies optional requested permissions.
Scopes []string `mapstructure:"scopes"`
}

func (c *OAuth2ClientCredentials) RoundTripper(base http.RoundTripper) (http.RoundTripper, error) {
if c.ClientID == "" {
return nil, errNoClientIDProvided
}
if c.ClientSecret == "" {
return nil, errNoClientSecretProvided
}
if c.TokenURL == "" {
return nil, errNoTokenURLProvided
}
config := clientcredentials.Config{
ClientID: c.ClientID,
ClientSecret: c.ClientSecret,
TokenURL: c.TokenURL,
Scopes: c.Scopes,
}

return &oauth2.Transport{
Source: config.TokenSource(context.Background()),
Base: base,
}, nil
}
113 changes: 113 additions & 0 deletions config/configoauth2/configoauth2client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package configoauth2

import (
"net/http"
"testing"

"github.com/stretchr/testify/assert"
"golang.org/x/oauth2"
)

func TestOAuth2ClientCredentialsSettings(t *testing.T) {
tests := []struct {
name string
settings *OAuth2ClientCredentials
shouldError bool
}{
{
name: "all_valid_settings",
settings: &OAuth2ClientCredentials{
ClientID: "testclientid",
ClientSecret: "testsecret",
TokenURL: "https://test-url/v1/token",
pavankrish123 marked this conversation as resolved.
Show resolved Hide resolved
Scopes: []string{"resource.read"},
},
shouldError: false,
},
{
name: "missing_client_id",
settings: &OAuth2ClientCredentials{
ClientSecret: "testsecret",
TokenURL: "https://test-url/v1/token",
Scopes: []string{"resource.read"},
},
shouldError: true,
},
{
name: "missing_client_secret",
settings: &OAuth2ClientCredentials{
ClientID: "testclientid",
TokenURL: "https://test-url/v1/token",
Scopes: []string{"resource.read"},
},
shouldError: true,
},
{
name: "missing_token_url",
settings: &OAuth2ClientCredentials{
ClientID: "testclientid",
ClientSecret: "testsecret",
Scopes: []string{"resource.read"},
},
shouldError: true,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
baseRoundTripper := &testRoundTripperBase{""}
_, err := test.settings.RoundTripper(baseRoundTripper)
if test.shouldError {
assert.Error(t, err)
return
}
assert.NoError(t, err)
})
}
}

type testRoundTripperBase struct {
testString string
}

func (b *testRoundTripperBase) RoundTrip(_ *http.Request) (*http.Response, error) {
return nil, nil
}

func TestOAuth2ClientRoundTripperReturnsOAuth2RoundTripper(t *testing.T) {
creds := &OAuth2ClientCredentials{
ClientID: "testclientid",
ClientSecret: "testsecret",
TokenURL: "https://test-url/v1/token",
Scopes: []string{"resource.read"},
}

testString := "TestString"

baseRoundTripper := &testRoundTripperBase{testString}
roundTripper, err := creds.RoundTripper(baseRoundTripper)
assert.NoError(t, err)

// test roundTripper is an OAuth RoundTripper
oAuth2Transport, ok := roundTripper.(*oauth2.Transport)
assert.True(t, ok)

// test oAuthRoundTripper wrapped the base roundTripper properly
wrappedRoundTripper, ok := oAuth2Transport.Base.(*testRoundTripperBase)
assert.True(t, ok)
assert.Equal(t, wrappedRoundTripper.testString, testString)
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ require (
go.opencensus.io v0.22.5
go.uber.org/atomic v1.7.0
go.uber.org/zap v1.16.0
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e
golang.org/x/text v0.3.5
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d
Expand Down