Skip to content

Commit

Permalink
ASAP Client Authentication Extension (#6627)
Browse files Browse the repository at this point in the history
* asapauthextension + added to versions.yaml

* address jpkrohling comments

* reorder in correct alphabetical

* Asap -> ASAP name change

* update cfg to receive time.Duration directly, and reflect in tests and docs

* appease linter
  • Loading branch information
jamesmoessis authored and PaurushGarg committed Dec 11, 2021
1 parent 43c0783 commit c3e1196
Show file tree
Hide file tree
Showing 14 changed files with 1,006 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ exporter/sumologicexporter/ @open-telemetry/collector-c
exporter/tanzuobservabilityexporter/ @open-telemetry/collector-contrib-approvers @oppegard @thepeterstone @keep94
exporter/tencentcloudlogserviceexporter/ @open-telemetry/collector-contrib-approvers @wgliang @yiyang5055

extension/asapauthextension @open-telemetry/collector-contrib-approvers @jamesmoessis @MovieStoreGuy
extension/awsproxy/ @open-telemetry/collector-contrib-approvers @anuraaga @Aneurysm9 @mxiamxia
extension/bearertokenauthextension/ @open-telemetry/collector-contrib-approvers @jpkrohling @pavankrish123
extension/healthcheckextension/ @open-telemetry/collector-contrib-approvers @jpkrohling
Expand Down
1 change: 1 addition & 0 deletions extension/asapauthextension/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include ../../Makefile.Common
36 changes: 36 additions & 0 deletions extension/asapauthextension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# ASAP Client Authentication Extension

This extension provides [Atlassian Service Authentication Protocol](https://s2sauth.bitbucket.io/) (ASAP) client
credentials for HTTP or gRPC based exporters.

## Example Configuration

```yaml
extensions:
asapclient:
# The `kid` as specified by the asap specification.
key_id: somekeyid
# The `iss` as specified by the asap specification.
issuer: someissuer
# The `aud` as specified by the asap specification.
audience:
- someservice
- someotherservice
# The private key of the client, used to sign the token. For an example, see `testdata/config.yaml`.
private_key: ${ASAP_PRIVATE_KEY}
# The time until expiry of each given token. The token will be cached and then re-provisioned upon expiry.
# For more info see the "exp" claim in the asap specification: https://s2sauth.bitbucket.io/spec/#access-token-generation
ttl: 60s

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

otlp/withauth:
endpoint: 0.0.0.0:5000
ca_file: /tmp/certs/ca.pem
auth:
authenticator: asapclient
```
66 changes: 66 additions & 0 deletions extension/asapauthextension/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// 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 asapauthextension // import "github.com/open-telemetry/opentelemetry-collector-contrib/extension/asapauthextension"

import (
"errors"
"time"

"go.opentelemetry.io/collector/config"
"go.uber.org/multierr"
)

var (
errNoKeyIDProvided = errors.New("no key id provided in asapclient configuration")
errNoTTLProvided = errors.New("no valid ttl was provided in asapclient configuration")
errNoIssuerProvided = errors.New("no issuer provided in asapclient configuration")
errNoAudienceProvided = errors.New("no audience provided in asapclient configuration")
errNoPrivateKeyProvided = errors.New("no private key provided in asapclient configuration")
)

type Config struct {
config.ExtensionSettings `mapstructure:",squash"`

KeyID string `mapstructure:"key_id"`

TTL time.Duration `mapstructure:"ttl"`

Issuer string `mapstructure:"issuer"`

Audience []string `mapstructure:"audience"`

PrivateKey string `mapstructure:"private_key"`
}

func (c *Config) Validate() error {
var errs error
if c.KeyID == "" {
errs = multierr.Append(errs, errNoKeyIDProvided)
}
if c.TTL <= 0 {
errs = multierr.Append(errs, errNoTTLProvided)
}
if len(c.Audience) == 0 {
errs = multierr.Append(errs, errNoAudienceProvided)
}

if c.Issuer == "" {
errs = multierr.Append(errs, errNoIssuerProvided)
}
if c.PrivateKey == "" {
errs = multierr.Append(errs, errNoPrivateKeyProvided)
}
return errs
}
94 changes: 94 additions & 0 deletions extension/asapauthextension/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// 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 asapauthextension

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

// Test keys. Not for use anywhere but these tests.
const (
privateKey = "data:application/pkcs8;kid=test;base64,MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEA0ZPr5JeyVDoB8RyZqQsx6qUD+9gMFg1/0hgdAvmytWBMXQJYdwkK2dFJwwZcWJVhJGcOJBDfB/8tcbdJd34KZQIDAQABAkBZD20tJTHJDSWKGsdJyNIbjqhUu4jXTkFFPK4Hd6jz3gV3fFvGnaolsD5Bt50dTXAiSCpFNSb9M9GY6XUAAdlBAiEA6MccfdZRfVapxKtAZbjXuAgMvnPtTvkVmwvhWLT5Wy0CIQDmfE8Et/pou0Jl6eM0eniT8/8oRzBWgy9ejDGfj86PGQIgWePqIL4OofRBgu0O5TlINI0HPtTNo12U9lbUIslgMdECICXT2RQpLcvqj+cyD7wZLZj6vrHZnTFVrnyR/cL2UyxhAiBswe/MCcD7T7J4QkNrCG+ceQGypc7LsxlIxQuKh5GWYA=="
publicKey = `-----BEGIN PUBLIC KEY-----
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANGT6+SXslQ6AfEcmakLMeqlA/vYDBYN
f9IYHQL5srVgTF0CWHcJCtnRScMGXFiVYSRnDiQQ3wf/LXG3SXd+CmUCAwEAAQ==
-----END PUBLIC KEY-----`
)

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)
assert.NotNil(t, cfg)
assert.NoError(t, err)
assert.NoError(t, cfg.Validate())

expected := factory.CreateDefaultConfig().(*Config)
expected.TTL = 60 * time.Second
expected.Audience = []string{"test_service1", "test_service2"}
expected.Issuer = "test_issuer"
expected.KeyID = "test_issuer/test_kid"
expected.PrivateKey = privateKey
ext := cfg.Extensions[config.NewComponentID(typeStr)]
assert.Equal(t, expected, ext)
}

func TestLoadBadConfig(t *testing.T) {
t.Parallel()
factories, err := componenttest.NopFactories()
require.NoError(t, err)

tests := []struct {
configName string
expectedErr error
}{
{
"missingkeyid",
errNoKeyIDProvided,
},
{
"missingissuer",
errNoIssuerProvided,
},
{
"missingaudience",
errNoAudienceProvided,
},
{
"missingpk",
errNoPrivateKeyProvided,
},
}
for _, tt := range tests {
factory := NewFactory()
factories.Extensions[typeStr] = factory
cfg, err := configtest.LoadConfig(path.Join(".", "testdata", "config_bad.yaml"), factories)
assert.NoError(t, err)
extension := cfg.Extensions[config.NewComponentIDWithName(typeStr, tt.configName)]
verr := extension.Validate()
require.ErrorIs(t, verr, tt.expectedErr)
}
}
100 changes: 100 additions & 0 deletions extension/asapauthextension/extension.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// 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 asapauthextension // import "github.com/open-telemetry/opentelemetry-collector-contrib/extension/asapauthextension"

import (
"context"
"fmt"
"net/http"

asap "bitbucket.org/atlassian/go-asap/v2"
"github.com/SermoDigital/jose/crypto"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/config/configauth"
"google.golang.org/grpc/credentials"
)

// ASAPClientAuthenticator implements ClientAuthenticator
type ASAPClientAuthenticator struct {
provisioner asap.Provisioner
privateKey interface{}
}

var _ configauth.ClientAuthenticator = (*ASAPClientAuthenticator)(nil)

func (a ASAPClientAuthenticator) RoundTripper(base http.RoundTripper) (http.RoundTripper, error) {
return asap.NewTransportDecorator(a.provisioner, a.privateKey)(base), nil
}

func (a ASAPClientAuthenticator) PerRPCCredentials() (credentials.PerRPCCredentials, error) {
return &PerRPCAuth{
authenticator: a,
}, nil
}

// Start does nothing and returns nil
func (a ASAPClientAuthenticator) Start(_ context.Context, _ component.Host) error {
return nil
}

// Shutdown does nothing and returns nil
func (a ASAPClientAuthenticator) Shutdown(_ context.Context) error {
return nil
}

func createASAPClientAuthenticator(cfg *Config) (ASAPClientAuthenticator, error) {
var a ASAPClientAuthenticator
pk, err := asap.NewPrivateKey([]byte(cfg.PrivateKey))
if err != nil {
return a, err
}

// Caching provisioner will only issue a new token after the current token's expiry (determined by TTL).
p := asap.NewCachingProvisioner(asap.NewProvisioner(
cfg.KeyID, cfg.TTL, cfg.Issuer, cfg.Audience, crypto.SigningMethodRS256))

return ASAPClientAuthenticator{
provisioner: p,
privateKey: pk,
}, nil
}

var _ credentials.PerRPCCredentials = (*PerRPCAuth)(nil)

// PerRPCAuth is a gRPC credentials.PerRPCCredentials implementation that returns an 'authorization' header.
type PerRPCAuth struct {
authenticator ASAPClientAuthenticator
}

// GetRequestMetadata returns the request metadata to be used with the RPC.
func (c *PerRPCAuth) GetRequestMetadata(context.Context, ...string) (map[string]string, error) {
token, err := c.authenticator.provisioner.Provision()
if err != nil {
return nil, err
}
headerValue, err := token.Serialize(c.authenticator.privateKey)
if err != nil {
return nil, err
}
header := map[string]string{
"authorization": fmt.Sprintf("Bearer %s", string(headerValue)),
}
return header, nil
}

// RequireTransportSecurity always returns true for this implementation.
func (*PerRPCAuth) RequireTransportSecurity() bool {
return true
}
Loading

0 comments on commit c3e1196

Please sign in to comment.