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 all commits
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
61 changes: 61 additions & 0 deletions config/configclientauth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Authentication for Exporters

This module provides helpers for HTTP/gRPC exporters, allowing them to be configured to perform authentication of outgoing requests.

## OAuth2 Client Credentials. (HTTP/gRPC)

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 authorization](https://tools.ietf.org/html/rfc6749#section-4.4).

This should be used when the client (exporter) is acting on its own behalf.


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

### OAuth2 Client Credentials Configuration

> Please review the Collector's [security
> documentation](https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/security.md),
> which contains recommendations on securing sensitive information such as the
> client id/secret required by this 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 client's 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.com/oauth2/default/v1/token
scopes: ["some.resource.read"]
```

## Bearer Token (gRPC)

This is to be used when a Bearer token needs to be attached every RPC directly. No token refresh will be
done in this case.

> Please review the Collector's [security
> documentation](https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/security.md),
> which contains recommendations on securing sensitive information such as the
> bearer token required by this configuration.

### Bearer Token settings
```yaml
exporters:
otlp:
endpoint: some-grpc:9090
per_rpc_auth:
type: bearer
bearer_token: somelonglivedbearertoken
```
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package configgrpc
package configclientauth

import (
"context"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package configgrpc
package configclientauth

import (
"context"
Expand Down
90 changes: 90 additions & 0 deletions config/configclientauth/configoauth2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// 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 configclientauth

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

"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
"google.golang.org/grpc/credentials"
grpcOAuth "google.golang.org/grpc/credentials/oauth"
)

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

// OAuth2ClientSettings stores the configuration for OAuth2 Client Credentials (2-legged OAuth2 flow) setup
type OAuth2ClientSettings 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,omitempty"`
}

// buildOAuth2ClientCredentials maps OAuth2ClientSettings to a build oauth2.clientcredentials.Config
func buildOAuth2ClientCredentials(c *OAuth2ClientSettings) (clientcredentials.Config, error) {
if c.ClientID == "" {
return clientcredentials.Config{}, errNoClientIDProvided
}
if c.ClientSecret == "" {
return clientcredentials.Config{}, errNoClientSecretProvided
}
if c.TokenURL == "" {
return clientcredentials.Config{}, errNoTokenURLProvided
}
return clientcredentials.Config{
ClientID: c.ClientID,
ClientSecret: c.ClientSecret,
TokenURL: c.TokenURL,
Scopes: c.Scopes,
}, nil
}

// OAuth2RoundTripper returns oauth2.Transport, an http.RoundTripper that performs "client-credential" OAuth flow and
// also auto refreshes OAuth tokens as needed.
func OAuth2RoundTripper(c *OAuth2ClientSettings, base http.RoundTripper) (http.RoundTripper, error) {
cc, err := buildOAuth2ClientCredentials(c)
if err != nil {
return nil, err
}
return &oauth2.Transport{
Source: cc.TokenSource(context.Background()),
Base: base,
}, nil
}

// OAuth2ClientCredentialsPerRPCCredentials returns gRPC PerRPCCredentials that supports "client-credential" OAuth flow. The underneath
// oauth2.clientcredentials.Config instance will manage tokens performing auto refresh as necessary.
func OAuth2ClientCredentialsPerRPCCredentials(c *OAuth2ClientSettings) (credentials.PerRPCCredentials, error) {
cc, err := buildOAuth2ClientCredentials(c)
if err != nil {
return nil, err
}
return grpcOAuth.TokenSource{TokenSource: cc.TokenSource(context.Background())}, nil
}
189 changes: 189 additions & 0 deletions config/configclientauth/configoauth2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// 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 configclientauth

import (
"net/http"
"testing"

"github.com/stretchr/testify/assert"
"golang.org/x/oauth2"
grpcOAuth "google.golang.org/grpc/credentials/oauth"
pavankrish123 marked this conversation as resolved.
Show resolved Hide resolved
)

func TestBuildOAuth2ClientCredentials(t *testing.T) {
tests := []struct {
name string
settings *OAuth2ClientSettings
shouldError bool
}{
{
name: "all_valid_settings",
settings: &OAuth2ClientSettings{
ClientID: "testclientid",
ClientSecret: "testsecret",
TokenURL: "https://example.com/v1/token",
Scopes: []string{"resource.read"},
},
shouldError: false,
},
{
name: "missing_client_id",
settings: &OAuth2ClientSettings{
ClientSecret: "testsecret",
TokenURL: "https://example.com/v1/token",
Scopes: []string{"resource.read"},
},
shouldError: true,
},
{
name: "missing_client_secret",
settings: &OAuth2ClientSettings{
ClientID: "testclientid",
TokenURL: "https://example.com/v1/token",
Scopes: []string{"resource.read"},
},
shouldError: true,
},
{
name: "missing_token_url",
settings: &OAuth2ClientSettings{
ClientID: "testclientid",
ClientSecret: "testsecret",
Scopes: []string{"resource.read"},
},
shouldError: true,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
rc, err := buildOAuth2ClientCredentials(test.settings)
if test.shouldError {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, test.settings.Scopes, rc.Scopes)
assert.Equal(t, test.settings.TokenURL, rc.TokenURL)
assert.Equal(t, test.settings.ClientSecret, rc.ClientSecret)
assert.Equal(t, test.settings.ClientID, rc.ClientID)
})
}
}

type testRoundTripper struct {
testString string
}

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

func TestOAuth2RoundTripper(t *testing.T) {
tests := []struct {
name string
settings *OAuth2ClientSettings
shouldError bool
}{
{
name: "returns_http_round_tripper",
settings: &OAuth2ClientSettings{
ClientID: "testclientid",
ClientSecret: "testsecret",
TokenURL: "https://example.com/v1/token",
Scopes: []string{"resource.read"},
},
shouldError: false,
},
{
name: "invalid_client_settings_should_error",
settings: &OAuth2ClientSettings{
ClientID: "testclientid",
TokenURL: "https://example.com/v1/token",
Scopes: []string{"resource.read"},
},
shouldError: true,
},
}

testString := "TestString"
baseRoundTripper := &testRoundTripper{testString}

for _, testcase := range tests {
t.Run(testcase.name, func(t *testing.T) {
roundTripper, err := OAuth2RoundTripper(testcase.settings, baseRoundTripper)
if testcase.shouldError {
assert.Error(t, err)
assert.Nil(t, roundTripper)
return
}
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.(*testRoundTripper)
assert.True(t, ok)
assert.Equal(t, wrappedRoundTripper.testString, testString)
})
}
}

func TestOAuth2PerRPCCredentials(t *testing.T) {
tests := []struct {
name string
settings *OAuth2ClientSettings
shouldError bool
}{
{
name: "returns_http_round_tripper",
settings: &OAuth2ClientSettings{
ClientID: "testclientid",
ClientSecret: "testsecret",
TokenURL: "https://example.com/v1/token",
Scopes: []string{"resource.read"},
},
shouldError: false,
},
{
name: "invalid_client_settings_should_error",
settings: &OAuth2ClientSettings{
ClientID: "testclientid",
TokenURL: "https://example.com/v1/token",
Scopes: []string{"resource.read"},
},
shouldError: true,
},
}

for _, testcase := range tests {
t.Run(testcase.name, func(t *testing.T) {
grpcCredentialsOpt, err := OAuth2ClientCredentialsPerRPCCredentials(testcase.settings)
if testcase.shouldError {
assert.Error(t, err)
assert.Nil(t, grpcCredentialsOpt)
return
}
assert.NoError(t, err)

// test grpcCredentialsOpt is an grpc OAuthTokenSource
_, ok := grpcCredentialsOpt.(grpcOAuth.TokenSource)
assert.True(t, ok)
})
}
}
3 changes: 2 additions & 1 deletion config/configgrpc/configgrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/config"
"go.opentelemetry.io/collector/config/configauth"
"go.opentelemetry.io/collector/config/configclientauth"
"go.opentelemetry.io/collector/config/confignet"
"go.opentelemetry.io/collector/config/configtls"
)
Expand Down Expand Up @@ -208,7 +209,7 @@ func (gcs *GRPCClientSettings) ToDialOptions() ([]grpc.DialOption, error) {
if gcs.PerRPCAuth != nil {
if strings.EqualFold(gcs.PerRPCAuth.AuthType, PerRPCAuthTypeBearer) {
sToken := gcs.PerRPCAuth.BearerToken
token := BearerToken(sToken)
token := configclientauth.BearerToken(sToken)
opts = append(opts, grpc.WithPerRPCCredentials(token))
} else {
return nil, fmt.Errorf("unsupported per-RPC auth type %q", gcs.PerRPCAuth.AuthType)
Expand Down
Loading