Skip to content

Commit

Permalink
Authentication processor 3/4 - Add implementation
Browse files Browse the repository at this point in the history
Signed-off-by: Juraci Paixão Kröhling <juraci@kroehling.de>
  • Loading branch information
jpkrohling committed Sep 24, 2020
1 parent 430c002 commit b41363e
Show file tree
Hide file tree
Showing 7 changed files with 946 additions and 5 deletions.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/census-instrumentation/opencensus-proto v0.3.0
github.com/client9/misspell v0.3.4
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/davecgh/go-spew v1.1.1
github.com/evanphx/json-patch v4.5.0+incompatible // indirect
github.com/go-kit/kit v0.10.0
Expand Down Expand Up @@ -37,6 +38,7 @@ require (
github.com/orijtech/prometheus-go-metrics-exporter v0.0.5
github.com/ory/go-acc v0.2.6
github.com/pavius/impi v0.0.3
github.com/pquerna/cachecontrol v0.0.0-20200819021114-67c6ae64274f // indirect
github.com/prometheus/client_golang v1.7.1
github.com/prometheus/common v0.13.0
github.com/prometheus/prometheus v1.8.2-0.20200827201422-1195cc24e3c8
Expand All @@ -62,6 +64,7 @@ require (
google.golang.org/grpc v1.32.0
google.golang.org/grpc/examples v0.0.0-20200728065043-dfc0c05b2da9 // indirect
google.golang.org/protobuf v1.25.0
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
gopkg.in/yaml.v2 v2.3.0
honnef.co/go/tools v0.0.1-2020.1.5
)
7 changes: 6 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,11 @@ github.com/containerd/containerd v1.3.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMX
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
Expand Down Expand Up @@ -883,6 +884,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/pquerna/cachecontrol v0.0.0-20200819021114-67c6ae64274f h1:JDEmUDtyiLMyMlFwiaDOv2hxUp35497fkwePcLeV7j4=
github.com/pquerna/cachecontrol v0.0.0-20200819021114-67c6ae64274f/go.mod h1:hoLfEwdY11HjRfKFH6KqnPsfxlo3BP6bJehpDv8t6sQ=
github.com/prometheus/alertmanager v0.21.0/go.mod h1:h7tJ81NA0VLWvWEayi1QltevFkLF3KxmC/malTcT8Go=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
Expand Down Expand Up @@ -1519,6 +1522,8 @@ gopkg.in/jcmturner/gokrb5.v7 v7.5.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuv
gopkg.in/jcmturner/rpc.v1 v1.1.0 h1:QHIUxTX1ISuAv9dD2wJ9HWQVuWDX/Zc0PfeC2tjc4rU=
gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
Expand Down
5 changes: 3 additions & 2 deletions internal/auth/authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import (
var (
errNoOIDCProvided = errors.New("no OIDC information provided")
errMetadataNotFound = errors.New("no request metadata found")
errNotImplemented = errors.New("not implemented")
defaultAttribute = "authorization"
)

Expand All @@ -50,6 +49,8 @@ type Authenticator interface {
}

type authenticateFunc func(context.Context, map[string][]string) (context.Context, error)
type unaryInterceptorFunc func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, authenticate authenticateFunc) (interface{}, error)
type streamInterceptorFunc func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler, authenticate authenticateFunc) error

// New creates an authenticator based on the given configuration
func New(cfg configauth.Authentication) (Authenticator, error) {
Expand All @@ -61,7 +62,7 @@ func New(cfg configauth.Authentication) (Authenticator, error) {
cfg.Attribute = defaultAttribute
}

return nil, errNotImplemented
return newOIDCAuthenticator(cfg)
}

func defaultUnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, authenticate authenticateFunc) (interface{}, error) {
Expand Down
4 changes: 2 additions & 2 deletions internal/auth/authenticator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ func TestNew(t *testing.T) {
})

// verify
assert.Nil(t, p)
assert.Equal(t, errNotImplemented, err)
assert.NotNil(t, p)
assert.NoError(t, err)
}

func TestMissingOIDC(t *testing.T) {
Expand Down
247 changes: 247 additions & 0 deletions internal/auth/oidc_authenticator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
// 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 auth

import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"strings"
"time"

"github.com/coreos/go-oidc"
"google.golang.org/grpc"

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

type oidcAuthenticator struct {
attribute string
config configauth.OIDC
provider *oidc.Provider
verifier *oidc.IDTokenVerifier

unaryInterceptor unaryInterceptorFunc
streamInterceptor streamInterceptorFunc
}

var (
_ Authenticator = (*oidcAuthenticator)(nil)

errNoClientIDProvided = errors.New("no ClientID provided for the OIDC configuration")
errNoIssuerURL = errors.New("no IssuerURL provided for the OIDC configuration")
errInvalidAuthenticationHeaderFormat = errors.New("invalid authorization header format")
errFailedToObtainClaimsFromToken = errors.New("failed to get the subject from the token issued by the OIDC provider")
errClaimNotFound = errors.New("username claim from the OIDC configuration not found on the token returned by the OIDC provider")
errUsernameNotString = errors.New("the username returned by the OIDC provider isn't a regular string")
errGroupsClaimNotFound = errors.New("groups claim from the OIDC configuration not found on the token returned by the OIDC provider")
errNotAuthenticated = errors.New("authentication didn't succeed")
)

func newOIDCAuthenticator(cfg configauth.Authentication) (*oidcAuthenticator, error) {
if cfg.OIDC.Audience == "" {
return nil, errNoClientIDProvided
}
if cfg.OIDC.IssuerURL == "" {
return nil, errNoIssuerURL
}
if cfg.Attribute == "" {
cfg.Attribute = defaultAttribute
}

return &oidcAuthenticator{
attribute: cfg.Attribute,
config: *cfg.OIDC,
unaryInterceptor: defaultUnaryInterceptor,
streamInterceptor: defaultStreamInterceptor,
}, nil
}

func (o *oidcAuthenticator) Authenticate(ctx context.Context, headers map[string][]string) (context.Context, error) {
authHeaders := headers[o.attribute]
if len(authHeaders) == 0 {
return ctx, errNotAuthenticated
}

// we only use the first header, if multiple values exist
parts := strings.Split(authHeaders[0], " ")
if len(parts) != 2 {
return ctx, errInvalidAuthenticationHeaderFormat
}

idToken, err := o.verifier.Verify(ctx, parts[1])
if err != nil {
return ctx, fmt.Errorf("failed to verify token: %w", err)
}

claims := map[string]interface{}{}
if err = idToken.Claims(&claims); err != nil {
// currently, this isn't a valid condition, the Verify call a few lines above
// will already attempt to parse the payload as a json and set it as the claims
// for the token. As we are using a map to hold the claims, there's no way to fail
// to read the claims. It could fail if we were using a custom struct. Instead of
// swalling the error, it's better to make this future-proof, in case the underlying
// code changes
return ctx, errFailedToObtainClaimsFromToken
}

sub, err := getSubjectFromClaims(claims, o.config.UsernameClaim, idToken.Subject)
if err != nil {
return ctx, fmt.Errorf("failed to get subject from claims in the token: %w", err)
}
ctx = context.WithValue(ctx, subjectKey, sub)

gr, err := getGroupsFromClaims(claims, o.config.GroupsClaim)
if err != nil {
return ctx, fmt.Errorf("failed to get groups from claims in the token: %w", err)
}
ctx = context.WithValue(ctx, groupsKey, gr)

return ctx, nil
}

func (o *oidcAuthenticator) Start(context.Context) error {
provider, err := getProviderForConfig(o.config)
if err != nil {
return fmt.Errorf("failed to get configuration from the auth server: %w", err)
}
o.provider = provider

o.verifier = o.provider.Verifier(&oidc.Config{
ClientID: o.config.Audience,
})

return nil
}

func (o *oidcAuthenticator) Close() error {
// no-op at the moment
// once we implement caching of the tokens we might need this
return nil
}

func (o *oidcAuthenticator) UnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
return o.unaryInterceptor(ctx, req, info, handler, o.Authenticate)
}

func (o *oidcAuthenticator) StreamInterceptor(srv interface{}, str grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
return o.streamInterceptor(srv, str, info, handler, o.Authenticate)
}

func getSubjectFromClaims(claims map[string]interface{}, usernameClaim string, fallback string) (string, error) {
if len(usernameClaim) > 0 {
username, found := claims[usernameClaim]
if !found {
return "", errClaimNotFound
}

sUsername, ok := username.(string)
if !ok {
return "", errUsernameNotString
}

return sUsername, nil
}

return fallback, nil
}

func getGroupsFromClaims(claims map[string]interface{}, groupsClaim string) ([]string, error) {
if len(groupsClaim) > 0 {
var groups []string
rawGroup, ok := claims[groupsClaim]
if !ok {
return nil, errGroupsClaimNotFound
}
switch v := rawGroup.(type) {
case string:
groups = append(groups, v)
case []string:
groups = v
case []interface{}:
groups = make([]string, 0, len(v))
for i := range v {
groups = append(groups, fmt.Sprintf("%v", v[i]))
}
}

return groups, nil
}

return []string{}, nil
}

func getProviderForConfig(config configauth.OIDC) (*oidc.Provider, error) {
t := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 10 * time.Second,
DualStack: true,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 5 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}

cert, err := getIssuerCACertFromPath(config.IssuerCAPath)
if err != nil {
return nil, err // the errors from this path have enough context already
}

if cert != nil {
t.TLSClientConfig = &tls.Config{
RootCAs: x509.NewCertPool(),
}
t.TLSClientConfig.RootCAs.AddCert(cert)
}

client := &http.Client{
Timeout: 5 * time.Second,
Transport: t,
}
oidcContext := oidc.ClientContext(context.Background(), client)
return oidc.NewProvider(oidcContext, config.IssuerURL)
}

func getIssuerCACertFromPath(path string) (*x509.Certificate, error) {
if path == "" {
return nil, nil
}

rawCA, err := ioutil.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("could not read the CA file %q: %w", path, err)
}

if len(rawCA) == 0 {
return nil, fmt.Errorf("could not read the CA file %q: empty file", path)
}

block, _ := pem.Decode(rawCA)
if block == nil {
return nil, fmt.Errorf("cannot decode the contents of the CA file %q: %w", path, err)
}

return x509.ParseCertificate(block.Bytes)
}
Loading

0 comments on commit b41363e

Please sign in to comment.