Skip to content

Commit

Permalink
Added extension to support OAuth2 client credentials authentication o…
Browse files Browse the repository at this point in the history
…n HTTP and gRPC exporters (open-telemetry#3848)

**This is a port of the approved  PR open-telemetry/opentelemetry-collector#3369  from core to contrib per instructions from @tigrannajaryan**

Link to tracking Issue:
open-telemetry/opentelemetry-collector#2785
open-telemetry/opentelemetry-collector#2500
open-telemetry/opentelemetry-collector#2603

**Testing**:

- Unit Tests
- Manual testing by verifying the token is getting automatically refreshed after expiry time in HTTP and gRPC OTLP exporters


These are Manual tests 

1) To check whether the token is actually refreshed, I have set up an echo server that prints the token and I have setup a dev okta oauth2 server with access token life time for 5 mins. I could see the token being refreshed, 

```yaml
extensions:
  oauth2client:
    client_id: 0gI........      
    client_secret: _doAe0NeU.......
    token_url: https://dev-xxxxxxxx.XXX.com/oauth2/default/v1/token
    scopes: ["api.metrics"]
    # timeout for the token client
    timeout: 2s

receivers:
  hostmetrics:
    collection_interval: 20s
    scrapers:
      load:

exporters:
  logging:
  otlp:
    endpoint: localhost:5000
    ca_file: /tmp/certs/ca.pem
    auth:
      authenticator: oauth2client

service:
  extensions: [oauth2client]
  pipelines:
    metrics/agent:
      receivers:
        - hostmetrics
      processors: []
      exporters:
        - logging
        - otlp
```
sample output from server
```
2021/06/07 16:06:11 map[:authority:[localhost:5000] authorization:[Bearer eyJraWQiOiJfamJVQnpzX0RXMHdBUTF[...........]a4wK0VSQ0hpw] content-type:[application/grpc] user-agent:[grpc-go/1.38.0]]


2021/06/07 16:06:32 map[:authority:[localhost:5000] authorization:[Bearer eyJraWQiOiJfamJVQnpzX0RXMHdBUTF[..........]_cNz1zciIg_hJurzUD-5A1_w] content-type:[application/grpc] user-agent:[grpc-go/1.38.0]]
```

2) For an e2e use case, 

I followed the steps from @jpkrohling article [here](https://medium.com/opentelemetry/securing-your-opentelemetry-collector-1a4f9fa5bd6f) and instead of directly using access token by explicitly making curl, I plugged in client secret into auth configuration as below 

(agent yaml)

```yaml
extensions:
  oauth2client:
    client_id: agent
    client_secret: 0d03380f-d0f3-XXX-XXXXXXXXX
    token_url: http://localhost:8080/auth/realms/opentelemetry/protocol/openid-connect/token
    # tls settings for the token client
    tls:
      insecure: true
      ca_file: /tmp/certs/ca.pem
      cert_file: /tmp/certs/cert.pem
      key_file: /tmp/certs/cert-key.pem
    # timeout for the token client
    timeout: 2s

receivers:
  hostmetrics:
    collection_interval: 20s
    scrapers:
      load:

exporters:
  logging:
  otlp:
    endpoint: localhost:56680
    ca_file: /tmp/certs/ca.pem
    auth:
      authenticator: oauth2client

service:
  extensions: [oauth2client]
  pipelines:
    metrics/agent:
      receivers:
        - hostmetrics
      processors: []
      exporters:
        - logging
        - otlp
 ``` 

I could see the metrics coming on the collector side 

```
2021-06-07T16:12:24.924-0700	INFO	loggingexporter/logging_exporter.go:57	MetricsExporter	{"#metrics": 3}
2021-06-07T16:12:44.838-0700	INFO	loggingexporter/logging_exporter.go:57	MetricsExporter	{"#metrics": 3}
2021-06-07T16:15:24.857-0700	INFO	loggingexporter/logging_exporter.go:57	MetricsExporter	{"#metrics": 3}
````

**Documentation:**
 Added README.md
  • Loading branch information
pavankrish123 committed Jun 28, 2021
1 parent a811dbb commit d7c94f0
Show file tree
Hide file tree
Showing 20 changed files with 2,740 additions and 1 deletion.
4 changes: 3 additions & 1 deletion cmd/otelcontribcol/components.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import (
"github.com/open-telemetry/opentelemetry-collector-contrib/exporter/tanzuobservabilityexporter"
"github.com/open-telemetry/opentelemetry-collector-contrib/extension/fluentbitextension"
"github.com/open-telemetry/opentelemetry-collector-contrib/extension/httpforwarder"
"github.com/open-telemetry/opentelemetry-collector-contrib/extension/oauth2clientauthextension"
"github.com/open-telemetry/opentelemetry-collector-contrib/extension/observer/hostobserver"
"github.com/open-telemetry/opentelemetry-collector-contrib/extension/observer/k8sobserver"
"github.com/open-telemetry/opentelemetry-collector-contrib/extension/storage/filestorage"
Expand Down Expand Up @@ -94,11 +95,12 @@ func components() (component.Factories, error) {
}

extensions := []component.ExtensionFactory{
filestorage.NewFactory(),
fluentbitextension.NewFactory(),
hostobserver.NewFactory(),
httpforwarder.NewFactory(),
k8sobserver.NewFactory(),
filestorage.NewFactory(),
oauth2clientauthextension.NewFactory(),
}

for _, ext := range factories.Extensions {
Expand Down
1 change: 1 addition & 0 deletions extension/oauth2clientauthextension/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include ../../Makefile.Common
65 changes: 65 additions & 0 deletions extension/oauth2clientauthextension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Authenticator - OAuth2 Client Credentials

This extension provides OAuth2 Client Credentials flow authenticator for HTTP and gRPC based exporters. The extension
fetches and refreshes the token after expiry automatically. For further details about OAuth2 Client Credentials flow (2-legged workflow)
refer https://datatracker.ietf.org/doc/html/rfc6749#section-4.4.

The authenticator type has to be set to `oauth2client`.

## Configuration

```yaml
extensions:
oauth2client:
client_id: someclientid
client_secret: someclientsecret
token_url: https://example.com/oauth2/default/v1/token
scopes: ["api.metrics"]
# tls settings for the token client
tls:
insecure: true
ca_file: /var/lib/mycert.pem
cert_file: certfile
key_file: keyfile
# timeout for the token client
timeout: 2s

receivers:
hostmetrics:
scrapers:
memory:
otlp:
protocols:
grpc:

exporters:
otlphttp/withauth:
endpoint: http://localhost:9000
auth:
authenticator: oauth2client

otlp/withauth:
endpoint: 0.0.0.0:5000
ca_file: /tmp/certs/ca.pem
auth:
authenticator: oauth2client

service:
extensions: [oauth2client]
pipelines:
metrics:
receivers: [hostmetrics]
processors: []
exporters: [otlphttp/withauth, otlp/withauth]
```
Following are the configuration fields
- [**token_url**](https://datatracker.ietf.org/doc/html/rfc6749#section-3.2) - The resource server's token endpoint URLs.
- [**client_id**](https://datatracker.ietf.org/doc/html/rfc6749#section-2.2) - The client identifier issued to the client.
- [**client_secret**](https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1) - The secret string associated with above identifier.
- [**scopes**](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3) - **Optional** optional requested permissions associated for the client.
- [**timeout**](https://golang.org/src/net/http/client.go#L90) - **Optional** specifies the timeout on the underlying client to authorization server for fetching the tokens (initial and while refreshing).
This is optional and not setting this configuration implies there is no timeout on the client.
For more information on client side TLS settings, see [configtls README](../../config/configtls/README.md).
74 changes: 74 additions & 0 deletions extension/oauth2clientauthextension/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// 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 oauth2clientauthextension

import (
"errors"
"time"

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

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")
)

// Config stores the configuration for OAuth2 Client Credentials (2-legged OAuth2 flow) setup.
type Config struct {
config.ExtensionSettings `mapstructure:",squash"`

// ClientID is the application's ID.
// See https://datatracker.ietf.org/doc/html/rfc6749#section-2.2
ClientID string `mapstructure:"client_id"`

// ClientSecret is the application's secret.
// See https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1
ClientSecret string `mapstructure:"client_secret"`

// TokenURL is the resource server's token endpoint
// URL. This is a constant specific to each server.
// See https://datatracker.ietf.org/doc/html/rfc6749#section-3.2
TokenURL string `mapstructure:"token_url"`

// Scope specifies optional requested permissions.
// See https://datatracker.ietf.org/doc/html/rfc6749#section-3.3
Scopes []string `mapstructure:"scopes,omitempty"`

// TLSSetting struct exposes TLS client configuration for the underneath client to authorization server.
TLSSetting configtls.TLSClientSetting `mapstructure:"tls,omitempty"`

// Timeout parameter configures `http.Client.Timeout` for the underneath client to authorization
// server while fetching and refreshing tokens.
Timeout time.Duration `mapstructure:"timeout,omitempty"`
}

var _ config.Extension = (*Config)(nil)

// Validate checks if the extension configuration is valid
func (cfg *Config) Validate() error {
if cfg.ClientID == "" {
return errNoClientIDProvided
}
if cfg.ClientSecret == "" {
return errNoClientSecretProvided
}
if cfg.TokenURL == "" {
return errNoTokenURLProvided
}
return nil
}
118 changes: 118 additions & 0 deletions extension/oauth2clientauthextension/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// 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 oauth2clientauthextension

import (
"path"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/component/componenttest"
"go.opentelemetry.io/collector/config"
"go.opentelemetry.io/collector/config/configtest"
"go.opentelemetry.io/collector/config/configtls"
)

func TestLoadConfig(t *testing.T) {
factories, err := componenttest.NopFactories()
assert.NoError(t, err)

factory := NewFactory()
factories.Extensions[typeStr] = factory
cfg, err := configtest.LoadConfigAndValidate(path.Join(".", "testdata", "config.yaml"), factories)

require.NoError(t, err)
require.NotNil(t, cfg)

expected := factory.CreateDefaultConfig().(*Config)
expected.ClientSecret = "someclientsecret"
expected.ClientID = "someclientid"
expected.Scopes = []string{"api.metrics"}
expected.TokenURL = "https://example.com/oauth2/default/v1/token"

ext := cfg.Extensions[config.NewIDWithName(typeStr, "1")]
assert.Equal(t,
&Config{
ExtensionSettings: config.NewExtensionSettings(config.NewIDWithName(typeStr, "1")),
ClientSecret: "someclientsecret",
ClientID: "someclientid",
Scopes: []string{"api.metrics"},
TokenURL: "https://example.com/oauth2/default/v1/token",
Timeout: time.Second,
},
ext)

assert.Equal(t, 2, len(cfg.Service.Extensions))
assert.Equal(t, config.NewIDWithName(typeStr, "1"), cfg.Service.Extensions[0])
}

func TestConfigTLSSettings(t *testing.T) {
factories, err := componenttest.NopFactories()
assert.NoError(t, err)

factory := NewFactory()
factories.Extensions[typeStr] = factory
cfg, err := configtest.LoadConfigAndValidate(path.Join(".", "testdata", "config.yaml"), factories)

require.NoError(t, err)
require.NotNil(t, cfg)

ext2 := cfg.Extensions[config.NewIDWithName(typeStr, "withtls")]

cfg2 := ext2.(*Config)
assert.Equal(t, cfg2.TLSSetting, configtls.TLSClientSetting{
TLSSetting: configtls.TLSSetting{
CAFile: "cafile",
CertFile: "certfile",
KeyFile: "keyfile",
},
Insecure: true,
InsecureSkipVerify: false,
ServerName: "",
})
}

func TestLoadConfigError(t *testing.T) {
factories, err := componenttest.NopFactories()
assert.NoError(t, err)

tests := []struct {
configName string
expectedErr error
}{
{
"missingurl",
errNoTokenURLProvided,
},
{
"missingid",
errNoClientIDProvided,
},
{
"missingsecret",
errNoClientSecretProvided,
},
}
for _, tt := range tests {
factory := NewFactory()
factories.Extensions[typeStr] = factory
cfg, _ := configtest.LoadConfig(path.Join(".", "testdata", "config_bad.yaml"), factories)
extension := cfg.Extensions[config.NewIDWithName(typeStr, tt.configName)]
verr := extension.Validate()
require.ErrorIs(t, verr, tt.expectedErr)
}
}
19 changes: 19 additions & 0 deletions extension/oauth2clientauthextension/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// 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 oauth2clientcredentialsauthextension implements `configauth.ClientAuthenticator`
// This extension provides OAuth2 Client Credentials flow authenticator for HTTP and gRPC based exporters.
// The extension fetches and refreshes the token after expiry
// For further details about OAuth2 Client Credentials flow refer https://datatracker.ietf.org/doc/html/rfc6749#section-4.4
package oauth2clientauthextension
Loading

0 comments on commit d7c94f0

Please sign in to comment.