From f8a5c3b85db2dd52b23c6e4996729935f42632e0 Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Mon, 21 Jul 2025 10:02:20 -0600 Subject: [PATCH 01/38] WIP - add OIDC support to Kafka auth Signed-off-by: Rich Scott --- internal/kafka/authentication.go | 3 + internal/kafka/client.go | 2 + internal/kafka/franz_client.go | 1 + internal/kafka/go.mod | 8 +- internal/kafka/go.sum | 16 +++ internal/kafka/oidc_client.go | 155 +++++++++++++++++++++++++ internal/kafka/oidc_client_test.go | 180 +++++++++++++++++++++++++++++ pkg/kafka/configkafka/config.go | 3 +- 8 files changed, 366 insertions(+), 2 deletions(-) create mode 100644 internal/kafka/oidc_client.go create mode 100644 internal/kafka/oidc_client_test.go diff --git a/internal/kafka/authentication.go b/internal/kafka/authentication.go index da63cdf89b695..eee29f73f0801 100644 --- a/internal/kafka/authentication.go +++ b/internal/kafka/authentication.go @@ -57,6 +57,9 @@ func configureSASL(ctx context.Context, config configkafka.SASLConfig, saramaCon case AWSMSKIAMOAUTHBEARER: saramaConfig.Net.SASL.Mechanism = sarama.SASLTypeOAuth saramaConfig.Net.SASL.TokenProvider = &awsMSKTokenProvider{ctx: ctx, region: config.AWSMSK.Region} + case OIDC_FILE: + saramaConfig.Net.SASL.Mechanism = sarama.SASLTypeOAuth + saramaConfig.Net.SASL.TokenProvider = &oidcFileTokenProvider{} } } diff --git a/internal/kafka/client.go b/internal/kafka/client.go index 6cc0921886fde..86179e6c9cc34 100644 --- a/internal/kafka/client.go +++ b/internal/kafka/client.go @@ -149,6 +149,8 @@ func newSaramaClientConfig(ctx context.Context, config configkafka.ClientConfig) } else if config.Authentication.SASL != nil && config.Authentication.SASL.Mechanism == "AWS_MSK_IAM_OAUTHBEARER" { saramaConfig.Net.TLS.Config = &tls.Config{} saramaConfig.Net.TLS.Enable = true + } else if config.Authentication.SASL != nil && config.Authentication.SASL.Mechanism == OIDC_FILE { + saramaConfig.Net.TLS.Enable = true } configureSaramaAuthentication(ctx, config.Authentication, saramaConfig) return saramaConfig, nil diff --git a/internal/kafka/franz_client.go b/internal/kafka/franz_client.go index 2155345925a70..f762e0115fb1b 100644 --- a/internal/kafka/franz_client.go +++ b/internal/kafka/franz_client.go @@ -32,6 +32,7 @@ const ( SCRAMSHA256 = "SCRAM-SHA-256" PLAIN = "PLAIN" AWSMSKIAMOAUTHBEARER = "AWS_MSK_IAM_OAUTHBEARER" //nolint:gosec // These aren't credentials. + OIDC_FILE = "OIDC_FILE" ) // NewFranzSyncProducer creates a new Kafka client using the franz-go library. diff --git a/internal/kafka/go.mod b/internal/kafka/go.mod index c81797037fa9d..1864b8e9178ba 100644 --- a/internal/kafka/go.mod +++ b/internal/kafka/go.mod @@ -5,6 +5,7 @@ go 1.23.0 require ( github.com/IBM/sarama v1.45.2 github.com/aws/aws-msk-iam-sasl-signer-go v1.0.4 + github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/configkafka v0.130.0 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 github.com/stretchr/testify v1.10.0 @@ -15,9 +16,14 @@ require ( go.opentelemetry.io/collector/config/configopaque v1.36.1-0.20250715222903-0a7598ec1e19 go.opentelemetry.io/collector/config/configtls v1.36.1-0.20250715222903-0a7598ec1e19 go.uber.org/goleak v1.3.0 + golang.org/x/oauth2 v0.30.0 ) -require go.yaml.in/yaml/v3 v3.0.4 // indirect +require ( + github.com/go-jose/go-jose/v3 v3.0.1 // indirect + github.com/golang-jwt/jwt/v5 v5.2.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect +) require ( github.com/aws/aws-sdk-go-v2 v1.36.4 // indirect diff --git a/internal/kafka/go.sum b/internal/kafka/go.sum index dc4dc9a86e81b..b1e07797ae1f8 100644 --- a/internal/kafka/go.sum +++ b/internal/kafka/go.sum @@ -46,6 +46,8 @@ github.com/foxboron/swtpm_test v0.0.0-20230726224112-46aaafdf7006 h1:50sW4r0Pcvl github.com/foxboron/swtpm_test v0.0.0-20230726224112-46aaafdf7006/go.mod h1:eIXCMsMYCaqq9m1KSSxXwQG11krpuNPGP3k0uaWrbas= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= +github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -56,8 +58,13 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU= github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98= @@ -106,6 +113,8 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 h1:9bCMuD3TcnjeqjPT2gSlha4asp8NvgcFRYExCaikCxk= +github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25/go.mod h1:eDjgYHYDJbPLBLsyZ6qRaugP0mX8vePOhZ5id1fdzJw= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= @@ -120,6 +129,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -185,6 +195,7 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -192,6 +203,7 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58 golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -203,11 +215,14 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -232,6 +247,7 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= diff --git a/internal/kafka/oidc_client.go b/internal/kafka/oidc_client.go new file mode 100644 index 0000000000000..c2f1ecaf8f222 --- /dev/null +++ b/internal/kafka/oidc_client.go @@ -0,0 +1,155 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package kafka // import "github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka" + +import ( + "context" + "errors" + "fmt" + "log" + "net/url" + "os" + "sync" + "time" + + "github.com/IBM/sarama" + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" +) + +type oidcFileTokenProvider struct { + Ctx context.Context + ClientID string + ClientSecretFilePath string + TokenURL string + Scopes []string + + mu sync.RWMutex + backgroundOnce sync.Once + + cachedToken *oauth2.Token + + tokenExpiry time.Time + lastRefreshTime time.Time + + refreshAhead time.Duration + refreshCooldown time.Duration + + // TODO support the remaining fields of clientcredentials.Config + EndpointParams url.Values + AuthStyle oauth2.AuthStyle +} + +func NewOIDCFileTokenProvider(ctx context.Context, clientID, clientSecretFilePath, tokenURL string, + scopes []string, refreshAhead time.Duration, +) sarama.AccessTokenProvider { + return &oidcFileTokenProvider{ + Ctx: ctx, + ClientID: clientID, + ClientSecretFilePath: clientSecretFilePath, + TokenURL: tokenURL, + Scopes: scopes, + } +} + +func (p *oidcFileTokenProvider) Token() (*sarama.AccessToken, error) { + oauthTok, err := p.GetToken() + if err != nil { + return nil, err + } + + return &sarama.AccessToken{Token: oauthTok.AccessToken}, nil +} + +func (p *oidcFileTokenProvider) updateToken() (*oauth2.Token, error) { + p.mu.Lock() + defer p.mu.Unlock() + + now := time.Now() + log.Printf("Refreshing token for %s", p.ClientID) + if now.Sub(p.lastRefreshTime) < p.refreshCooldown { + // Someone just refreshed - skip + log.Printf("Skipping token refresh for %s, within the quiet window of %s", + p.ClientID, p.refreshCooldown) + return p.cachedToken, nil + } + + // Read the client secret every time we get a new token, + // as it may have changed in the meantime. + clientSecret, err := os.ReadFile(p.ClientSecretFilePath) + if err != nil { + return nil, fmt.Errorf("failed to read client secret: %w", err) + } + + fmt.Fprintf(os.Stderr, "updateToken(): p.TokenURL = %v\n", p.TokenURL) + oauthTok, err := (&clientcredentials.Config{ + ClientID: p.ClientID, + ClientSecret: string(clientSecret), + TokenURL: p.TokenURL + "/token", + Scopes: p.Scopes, + }).Token(p.Ctx) + + if err != nil || oauthTok == nil || oauthTok.AccessToken == "" { + return nil, fmt.Errorf("failed to refresh token: %w", err) + } + + expiresIn := time.Duration(oauthTok.ExpiresIn) * time.Second + p.cachedToken = oauthTok + p.tokenExpiry = now.Add(expiresIn) + p.lastRefreshTime = now + log.Printf("Token refreshed for %s, will expire after %s at %s", p.ClientID, + expiresIn.String(), p.tokenExpiry.String()) + + return oauthTok, nil +} + +func (p *oidcFileTokenProvider) GetToken() (*oauth2.Token, error) { + p.mu.RLock() + token := p.cachedToken + expires := p.tokenExpiry + hasToken := token != nil && token.AccessToken != "" + p.mu.RUnlock() + + if hasToken && expires.After(time.Now()) { + return token, nil + } + + // No valid cached token - do a blocking refresh + newToken, err := p.updateToken() + if err != nil { + return nil, err + } + + if newToken == nil || newToken.AccessToken == "" { + return nil, errors.New("token blank after fetch") + } + return newToken, nil +} + +func (p *oidcFileTokenProvider) startBackgroundRefresher() { + log.Printf("Will refresh access token for client %s and scope %s, %s before expiry", + p.ClientID, p.Scopes[0], p.refreshAhead.String()) + p.backgroundOnce.Do(func() { + go func() { + for { + sleepDuration := 0 * time.Minute + + p.mu.RLock() + if p.cachedToken != nil && p.cachedToken.AccessToken != "" { + sleepDuration = time.Until(p.tokenExpiry.Add(-p.refreshAhead)) + } + p.mu.RUnlock() + + if sleepDuration > 0 { + time.Sleep(sleepDuration) + continue + } + + if _, err := p.updateToken(); err != nil { + log.Printf("background token refresh failed: %v\n", err) + } + } + }() + }) +} diff --git a/internal/kafka/oidc_client_test.go b/internal/kafka/oidc_client_test.go new file mode 100644 index 0000000000000..139325bbef6e9 --- /dev/null +++ b/internal/kafka/oidc_client_test.go @@ -0,0 +1,180 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package kafka // import "github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka" +import ( + "context" + "crypto/rand" + "crypto/rsa" + "errors" + "fmt" + "net" + "net/http" + "os" + "path/filepath" + "testing" + "time" + + "github.com/IBM/sarama" + "github.com/oauth2-proxy/mockoidc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type MockOAuthProvider struct { + MockSignin func(clientID, scope string, requestedAuthority ...string) (*sarama.AccessToken, error) +} + +func (m *MockOAuthProvider) Signin(clientID, scope string, requestedAuthority ...string) (*sarama.AccessToken, error) { + if m.MockSignin != nil { + return m.MockSignin(clientID, scope, requestedAuthority...) + } + return nil, errors.New("MockSignin function not defined") +} + +func (m *MockOAuthProvider) Name() string { + return "mock" +} + +func TestOIDCProvider_GetToken_Success(t *testing.T) { + clientID := "mock-client-id" + secretFile, err := k8sSecretFile() + assert.NoError(t, err) + + clientSecret, err := os.ReadFile(secretFile) + assert.NoError(t, err) + + oidcServerQuit := make(chan any) + go func() { + oidcServer(oidcServerQuit, clientID, string(clientSecret), 10, 10) + }() + defer func() { + oidcServerQuit <- true + }() + + time.Sleep(3 * time.Second) // wait for OIDC to fully start + tokenURL := "http://127.0.0.1:3000/oidc" + + oidcProvider := NewOIDCFileTokenProvider(context.Background(), clientID, secretFile, tokenURL, []string{"mock-scope"}, 0) + + token, err := oidcProvider.Token() + require.NoError(t, err) + assert.NotNil(t, token) + assert.NotEmpty(t, token.Token) +} + +// func TestOIDCProvider_GetToken_Error(t *testing.T) { +// mockProvider := &MockOAuthProvider{ +// // TODO can we remove all requestAuthority ... ? +// MockSignin: func(clientID, scope string, requestedAuthority ...string) (*sarama.AccessToken, error) { +// return nil, errors.New("failed to sign in") +// }, +// } + +// oidcProvider := NewOIDCFileTokenProvider("mock-client-id", "mock-scopes", mockProvider, 0) + +// token, err := oidcProvider.Token() +// assert.NotNil(t, err) +// assert.Nil(t, token) +// assert.Equal(t, "failed to refresh token: failed to sign in", err.Error()) +// } + +// func TestOIDCProvider_TokenCaching(t *testing.T) { +// mockProvider := &MockOAuthProvider{ +// MockSignin: func(clientID, scope string, requestedAuthority ...string) (*AccessToken, error) { +// return &AccessToken{ +// AccessToken: "mock-access-token", +// ExpiresIn: 3600, +// TokenType: "Bearer", +// Scope: "mock-scopes", +// }, nil +// }, +// } + +// oidcProvider := NewGRSigningAccessTokenProvider("mock-client-id", "mock-scopes", mockProvider, 0) + +// token1, err1 := oidcProvider.GetToken() +// assert.Nil(t, err1) +// assert.NotNil(t, token1) + +// token2, err2 := oidcProvider.GetToken() +// assert.Nil(t, err2) +// assert.NotNil(t, token2) +// assert.Equal(t, token1, token2) +// } + +// func TestOIDCProvider_TokenExpired(t *testing.T) { +// mockProvider := &MockOAuthProvider{ +// MockSignin: func(clientID, scope string, requestedAuthority ...string) (*AccessToken, error) { +// return &AccessToken{ +// AccessToken: "mock-access-token", +// ExpiresIn: 1, +// TokenType: "Bearer", +// Scope: "mock-scopes", +// }, nil +// }, +// } + +// oidcProvider := NewGRSigningAccessTokenProvider("mock-client-id", "mock-scopes", mockProvider, 0) +// oidcProvider.refreshCooldown = 500 * time.Millisecond + +// token1, err1 := oidcProvider.GetToken() +// assert.Nil(t, err1) +// assert.NotNil(t, token1) + +// time.Sleep(2 * time.Second) + +// token2, err2 := oidcProvider.GetToken() +// assert.Nil(t, err2) +// assert.NotNil(t, token2) +// assert.NotEqual(t, token1, token2) +// } + +func oidcServer(ch <-chan any, clientID, clientSecret string, accessTTLsecs, refreshTTLsecs int) { + // Create RSA Private Key for token signing + rsaKey, _ := rsa.GenerateKey(rand.Reader, 2048) + + m, _ := mockoidc.NewServer(rsaKey) + m.ClientID = clientID + m.ClientSecret = clientSecret + m.AccessTTL = time.Duration(accessTTLsecs) * time.Second + m.RefreshTTL = time.Duration(refreshTTLsecs) * time.Second + + middleware := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + fmt.Fprintf(os.Stderr, "\n-------- oidcServer: before next.ServerHTTP(): request URL is %v\n", req.URL) + // custom middleware logic here ... + next.ServeHTTP(rw, req) + fmt.Fprintf(os.Stderr, "\n-------- oidcServer: after next.ServeHTTP(): request URL is %v\n", req.URL) + // custom middleware logic here... + }) + } + m.AddMiddleware(middleware) + + ln, _ := net.Listen("tcp", "127.0.0.1:3000") + + err := m.Start(ln, nil) // specify nil for tlsConfig to use HTTP + if err != nil { + fmt.Fprintf(os.Stderr, "could not start server: %v\n", err) + os.Exit(1) + } + defer m.Shutdown() + + // cfg := m.Config() + // fmt.Printf("%s\n", litter.Sdump(cfg)) + + <-ch + fmt.Fprintf(os.Stderr, "oidcServer shutting down\n") +} + +func k8sSecretFile() (string, error) { + k8sSAtoken := `eyJhbGciOiJSUzI1NiIsImtpZCI6IjdjdDhhT0pTSXh0Zm0yUVprUVRXaFpTVFpHUlQ0MlFkbDMzQXQ1XzRURkkifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzg0Mzk4Mzk5LCJpYXQiOjE3NTI4NjIzOTksImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiNTE3Zjg4ZDUtOTNjZC00YWIzLWFkZWItY2NiZjExMDdmZGYxIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJ0ZXN0Iiwibm9kZSI6eyJuYW1lIjoia2luZC1jb250cm9sLXBsYW5lIiwidWlkIjoiZThjZTczMWMtMmZjNC00NWZjLWJjNzItMzdhYTgyNDQzN2EwIn0sInBvZCI6eyJuYW1lIjoib2lkYy1zZXJ2ZXIteDU4bm4iLCJ1aWQiOiJmM2Q3YTBkZC04Yzk3LTRkNTgtOGYyYy01ZDRiNThjMmY5NDIifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlZmF1bHQiLCJ1aWQiOiIwZjc1MTJhNi00ZjhkLTQxYjAtOTM4NC1mYWE4YzlmZWUxMWYifSwid2FybmFmdGVyIjoxNzUyODY2MDA2fSwibmJmIjoxNzUyODYyMzk5LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6dGVzdDpkZWZhdWx0In0.A8eDX9Wz6aoAwO-Vrg2ddbxJ5d7r3pdg8J6D4gyHPNQLRmBcZHaWagRKJTZ3gDYvT_u_hCG5RJrHARt9MncftPJ5_gdRyXckbd9a9dcSSRVxFEPzdaUR6GSmTmI2sUwhU33AnWmRqlOlZW_WtslPGXl8tNsfDLfpvabjAuBFJrb7KB8MvzXVNvVcJ8BmM4oglX3e3xIxLBzSSQFkW9OGdmeWFsMh-lNaHpzXQGaZx3W2Wit2SUigbDDSJPCTs_tFMdPv-LW0AH9eRd5yU_j87gEsapu_u5j6qcNku-3g79LcGoIvTqe8QdSI7OeoWVnD05SjfAoyHhR-aoMJtSCOQg` + + tokenPath := filepath.Join(os.TempDir(), "k8sToken") + err := os.WriteFile(tokenPath, []byte(k8sSAtoken), 0o600) + if err != nil { + return "", fmt.Errorf("error writing %s: %w", tokenPath, err) + } + + return tokenPath, nil +} diff --git a/pkg/kafka/configkafka/config.go b/pkg/kafka/configkafka/config.go index e5f71f0783218..17d657de4dc07 100644 --- a/pkg/kafka/configkafka/config.go +++ b/pkg/kafka/configkafka/config.go @@ -330,7 +330,8 @@ type SASLConfig struct { Username string `mapstructure:"username"` // Password to be used on authentication Password string `mapstructure:"password"` - // SASL Mechanism to be used, possible values are: (PLAIN, AWS_MSK_IAM_OAUTHBEARER, SCRAM-SHA-256 or SCRAM-SHA-512). + // SASL Mechanism to be used, possible values are: (PLAIN, AWS_MSK_IAM_OAUTHBEARER, OIDC_FILE, + // OIDC_STRING, SCRAM-SHA-256 or SCRAM-SHA-512). Mechanism string `mapstructure:"mechanism"` // SASL Protocol Version to be used, possible values are: (0, 1). Defaults to 0. Version int `mapstructure:"version"` From fa2ffebdca39f111eaadc49e57aa3e88a9f7a6e1 Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Mon, 21 Jul 2025 10:02:20 -0600 Subject: [PATCH 02/38] WIP - add OIDC support to Kafka auth Signed-off-by: Rich Scott --- internal/kafka/authentication.go | 3 + internal/kafka/client.go | 2 + internal/kafka/franz_client.go | 1 + internal/kafka/go.mod | 8 +- internal/kafka/go.sum | 16 +++ internal/kafka/oidc_client.go | 155 +++++++++++++++++++++++++ internal/kafka/oidc_client_test.go | 180 +++++++++++++++++++++++++++++ pkg/kafka/configkafka/config.go | 3 +- 8 files changed, 366 insertions(+), 2 deletions(-) create mode 100644 internal/kafka/oidc_client.go create mode 100644 internal/kafka/oidc_client_test.go diff --git a/internal/kafka/authentication.go b/internal/kafka/authentication.go index da63cdf89b695..eee29f73f0801 100644 --- a/internal/kafka/authentication.go +++ b/internal/kafka/authentication.go @@ -57,6 +57,9 @@ func configureSASL(ctx context.Context, config configkafka.SASLConfig, saramaCon case AWSMSKIAMOAUTHBEARER: saramaConfig.Net.SASL.Mechanism = sarama.SASLTypeOAuth saramaConfig.Net.SASL.TokenProvider = &awsMSKTokenProvider{ctx: ctx, region: config.AWSMSK.Region} + case OIDC_FILE: + saramaConfig.Net.SASL.Mechanism = sarama.SASLTypeOAuth + saramaConfig.Net.SASL.TokenProvider = &oidcFileTokenProvider{} } } diff --git a/internal/kafka/client.go b/internal/kafka/client.go index 6cc0921886fde..86179e6c9cc34 100644 --- a/internal/kafka/client.go +++ b/internal/kafka/client.go @@ -149,6 +149,8 @@ func newSaramaClientConfig(ctx context.Context, config configkafka.ClientConfig) } else if config.Authentication.SASL != nil && config.Authentication.SASL.Mechanism == "AWS_MSK_IAM_OAUTHBEARER" { saramaConfig.Net.TLS.Config = &tls.Config{} saramaConfig.Net.TLS.Enable = true + } else if config.Authentication.SASL != nil && config.Authentication.SASL.Mechanism == OIDC_FILE { + saramaConfig.Net.TLS.Enable = true } configureSaramaAuthentication(ctx, config.Authentication, saramaConfig) return saramaConfig, nil diff --git a/internal/kafka/franz_client.go b/internal/kafka/franz_client.go index 2155345925a70..f762e0115fb1b 100644 --- a/internal/kafka/franz_client.go +++ b/internal/kafka/franz_client.go @@ -32,6 +32,7 @@ const ( SCRAMSHA256 = "SCRAM-SHA-256" PLAIN = "PLAIN" AWSMSKIAMOAUTHBEARER = "AWS_MSK_IAM_OAUTHBEARER" //nolint:gosec // These aren't credentials. + OIDC_FILE = "OIDC_FILE" ) // NewFranzSyncProducer creates a new Kafka client using the franz-go library. diff --git a/internal/kafka/go.mod b/internal/kafka/go.mod index c81797037fa9d..1864b8e9178ba 100644 --- a/internal/kafka/go.mod +++ b/internal/kafka/go.mod @@ -5,6 +5,7 @@ go 1.23.0 require ( github.com/IBM/sarama v1.45.2 github.com/aws/aws-msk-iam-sasl-signer-go v1.0.4 + github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/configkafka v0.130.0 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 github.com/stretchr/testify v1.10.0 @@ -15,9 +16,14 @@ require ( go.opentelemetry.io/collector/config/configopaque v1.36.1-0.20250715222903-0a7598ec1e19 go.opentelemetry.io/collector/config/configtls v1.36.1-0.20250715222903-0a7598ec1e19 go.uber.org/goleak v1.3.0 + golang.org/x/oauth2 v0.30.0 ) -require go.yaml.in/yaml/v3 v3.0.4 // indirect +require ( + github.com/go-jose/go-jose/v3 v3.0.1 // indirect + github.com/golang-jwt/jwt/v5 v5.2.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect +) require ( github.com/aws/aws-sdk-go-v2 v1.36.4 // indirect diff --git a/internal/kafka/go.sum b/internal/kafka/go.sum index dc4dc9a86e81b..b1e07797ae1f8 100644 --- a/internal/kafka/go.sum +++ b/internal/kafka/go.sum @@ -46,6 +46,8 @@ github.com/foxboron/swtpm_test v0.0.0-20230726224112-46aaafdf7006 h1:50sW4r0Pcvl github.com/foxboron/swtpm_test v0.0.0-20230726224112-46aaafdf7006/go.mod h1:eIXCMsMYCaqq9m1KSSxXwQG11krpuNPGP3k0uaWrbas= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= +github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -56,8 +58,13 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU= github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98= @@ -106,6 +113,8 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 h1:9bCMuD3TcnjeqjPT2gSlha4asp8NvgcFRYExCaikCxk= +github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25/go.mod h1:eDjgYHYDJbPLBLsyZ6qRaugP0mX8vePOhZ5id1fdzJw= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= @@ -120,6 +129,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -185,6 +195,7 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -192,6 +203,7 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58 golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -203,11 +215,14 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -232,6 +247,7 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= diff --git a/internal/kafka/oidc_client.go b/internal/kafka/oidc_client.go new file mode 100644 index 0000000000000..c2f1ecaf8f222 --- /dev/null +++ b/internal/kafka/oidc_client.go @@ -0,0 +1,155 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package kafka // import "github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka" + +import ( + "context" + "errors" + "fmt" + "log" + "net/url" + "os" + "sync" + "time" + + "github.com/IBM/sarama" + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" +) + +type oidcFileTokenProvider struct { + Ctx context.Context + ClientID string + ClientSecretFilePath string + TokenURL string + Scopes []string + + mu sync.RWMutex + backgroundOnce sync.Once + + cachedToken *oauth2.Token + + tokenExpiry time.Time + lastRefreshTime time.Time + + refreshAhead time.Duration + refreshCooldown time.Duration + + // TODO support the remaining fields of clientcredentials.Config + EndpointParams url.Values + AuthStyle oauth2.AuthStyle +} + +func NewOIDCFileTokenProvider(ctx context.Context, clientID, clientSecretFilePath, tokenURL string, + scopes []string, refreshAhead time.Duration, +) sarama.AccessTokenProvider { + return &oidcFileTokenProvider{ + Ctx: ctx, + ClientID: clientID, + ClientSecretFilePath: clientSecretFilePath, + TokenURL: tokenURL, + Scopes: scopes, + } +} + +func (p *oidcFileTokenProvider) Token() (*sarama.AccessToken, error) { + oauthTok, err := p.GetToken() + if err != nil { + return nil, err + } + + return &sarama.AccessToken{Token: oauthTok.AccessToken}, nil +} + +func (p *oidcFileTokenProvider) updateToken() (*oauth2.Token, error) { + p.mu.Lock() + defer p.mu.Unlock() + + now := time.Now() + log.Printf("Refreshing token for %s", p.ClientID) + if now.Sub(p.lastRefreshTime) < p.refreshCooldown { + // Someone just refreshed - skip + log.Printf("Skipping token refresh for %s, within the quiet window of %s", + p.ClientID, p.refreshCooldown) + return p.cachedToken, nil + } + + // Read the client secret every time we get a new token, + // as it may have changed in the meantime. + clientSecret, err := os.ReadFile(p.ClientSecretFilePath) + if err != nil { + return nil, fmt.Errorf("failed to read client secret: %w", err) + } + + fmt.Fprintf(os.Stderr, "updateToken(): p.TokenURL = %v\n", p.TokenURL) + oauthTok, err := (&clientcredentials.Config{ + ClientID: p.ClientID, + ClientSecret: string(clientSecret), + TokenURL: p.TokenURL + "/token", + Scopes: p.Scopes, + }).Token(p.Ctx) + + if err != nil || oauthTok == nil || oauthTok.AccessToken == "" { + return nil, fmt.Errorf("failed to refresh token: %w", err) + } + + expiresIn := time.Duration(oauthTok.ExpiresIn) * time.Second + p.cachedToken = oauthTok + p.tokenExpiry = now.Add(expiresIn) + p.lastRefreshTime = now + log.Printf("Token refreshed for %s, will expire after %s at %s", p.ClientID, + expiresIn.String(), p.tokenExpiry.String()) + + return oauthTok, nil +} + +func (p *oidcFileTokenProvider) GetToken() (*oauth2.Token, error) { + p.mu.RLock() + token := p.cachedToken + expires := p.tokenExpiry + hasToken := token != nil && token.AccessToken != "" + p.mu.RUnlock() + + if hasToken && expires.After(time.Now()) { + return token, nil + } + + // No valid cached token - do a blocking refresh + newToken, err := p.updateToken() + if err != nil { + return nil, err + } + + if newToken == nil || newToken.AccessToken == "" { + return nil, errors.New("token blank after fetch") + } + return newToken, nil +} + +func (p *oidcFileTokenProvider) startBackgroundRefresher() { + log.Printf("Will refresh access token for client %s and scope %s, %s before expiry", + p.ClientID, p.Scopes[0], p.refreshAhead.String()) + p.backgroundOnce.Do(func() { + go func() { + for { + sleepDuration := 0 * time.Minute + + p.mu.RLock() + if p.cachedToken != nil && p.cachedToken.AccessToken != "" { + sleepDuration = time.Until(p.tokenExpiry.Add(-p.refreshAhead)) + } + p.mu.RUnlock() + + if sleepDuration > 0 { + time.Sleep(sleepDuration) + continue + } + + if _, err := p.updateToken(); err != nil { + log.Printf("background token refresh failed: %v\n", err) + } + } + }() + }) +} diff --git a/internal/kafka/oidc_client_test.go b/internal/kafka/oidc_client_test.go new file mode 100644 index 0000000000000..139325bbef6e9 --- /dev/null +++ b/internal/kafka/oidc_client_test.go @@ -0,0 +1,180 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package kafka // import "github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka" +import ( + "context" + "crypto/rand" + "crypto/rsa" + "errors" + "fmt" + "net" + "net/http" + "os" + "path/filepath" + "testing" + "time" + + "github.com/IBM/sarama" + "github.com/oauth2-proxy/mockoidc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type MockOAuthProvider struct { + MockSignin func(clientID, scope string, requestedAuthority ...string) (*sarama.AccessToken, error) +} + +func (m *MockOAuthProvider) Signin(clientID, scope string, requestedAuthority ...string) (*sarama.AccessToken, error) { + if m.MockSignin != nil { + return m.MockSignin(clientID, scope, requestedAuthority...) + } + return nil, errors.New("MockSignin function not defined") +} + +func (m *MockOAuthProvider) Name() string { + return "mock" +} + +func TestOIDCProvider_GetToken_Success(t *testing.T) { + clientID := "mock-client-id" + secretFile, err := k8sSecretFile() + assert.NoError(t, err) + + clientSecret, err := os.ReadFile(secretFile) + assert.NoError(t, err) + + oidcServerQuit := make(chan any) + go func() { + oidcServer(oidcServerQuit, clientID, string(clientSecret), 10, 10) + }() + defer func() { + oidcServerQuit <- true + }() + + time.Sleep(3 * time.Second) // wait for OIDC to fully start + tokenURL := "http://127.0.0.1:3000/oidc" + + oidcProvider := NewOIDCFileTokenProvider(context.Background(), clientID, secretFile, tokenURL, []string{"mock-scope"}, 0) + + token, err := oidcProvider.Token() + require.NoError(t, err) + assert.NotNil(t, token) + assert.NotEmpty(t, token.Token) +} + +// func TestOIDCProvider_GetToken_Error(t *testing.T) { +// mockProvider := &MockOAuthProvider{ +// // TODO can we remove all requestAuthority ... ? +// MockSignin: func(clientID, scope string, requestedAuthority ...string) (*sarama.AccessToken, error) { +// return nil, errors.New("failed to sign in") +// }, +// } + +// oidcProvider := NewOIDCFileTokenProvider("mock-client-id", "mock-scopes", mockProvider, 0) + +// token, err := oidcProvider.Token() +// assert.NotNil(t, err) +// assert.Nil(t, token) +// assert.Equal(t, "failed to refresh token: failed to sign in", err.Error()) +// } + +// func TestOIDCProvider_TokenCaching(t *testing.T) { +// mockProvider := &MockOAuthProvider{ +// MockSignin: func(clientID, scope string, requestedAuthority ...string) (*AccessToken, error) { +// return &AccessToken{ +// AccessToken: "mock-access-token", +// ExpiresIn: 3600, +// TokenType: "Bearer", +// Scope: "mock-scopes", +// }, nil +// }, +// } + +// oidcProvider := NewGRSigningAccessTokenProvider("mock-client-id", "mock-scopes", mockProvider, 0) + +// token1, err1 := oidcProvider.GetToken() +// assert.Nil(t, err1) +// assert.NotNil(t, token1) + +// token2, err2 := oidcProvider.GetToken() +// assert.Nil(t, err2) +// assert.NotNil(t, token2) +// assert.Equal(t, token1, token2) +// } + +// func TestOIDCProvider_TokenExpired(t *testing.T) { +// mockProvider := &MockOAuthProvider{ +// MockSignin: func(clientID, scope string, requestedAuthority ...string) (*AccessToken, error) { +// return &AccessToken{ +// AccessToken: "mock-access-token", +// ExpiresIn: 1, +// TokenType: "Bearer", +// Scope: "mock-scopes", +// }, nil +// }, +// } + +// oidcProvider := NewGRSigningAccessTokenProvider("mock-client-id", "mock-scopes", mockProvider, 0) +// oidcProvider.refreshCooldown = 500 * time.Millisecond + +// token1, err1 := oidcProvider.GetToken() +// assert.Nil(t, err1) +// assert.NotNil(t, token1) + +// time.Sleep(2 * time.Second) + +// token2, err2 := oidcProvider.GetToken() +// assert.Nil(t, err2) +// assert.NotNil(t, token2) +// assert.NotEqual(t, token1, token2) +// } + +func oidcServer(ch <-chan any, clientID, clientSecret string, accessTTLsecs, refreshTTLsecs int) { + // Create RSA Private Key for token signing + rsaKey, _ := rsa.GenerateKey(rand.Reader, 2048) + + m, _ := mockoidc.NewServer(rsaKey) + m.ClientID = clientID + m.ClientSecret = clientSecret + m.AccessTTL = time.Duration(accessTTLsecs) * time.Second + m.RefreshTTL = time.Duration(refreshTTLsecs) * time.Second + + middleware := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + fmt.Fprintf(os.Stderr, "\n-------- oidcServer: before next.ServerHTTP(): request URL is %v\n", req.URL) + // custom middleware logic here ... + next.ServeHTTP(rw, req) + fmt.Fprintf(os.Stderr, "\n-------- oidcServer: after next.ServeHTTP(): request URL is %v\n", req.URL) + // custom middleware logic here... + }) + } + m.AddMiddleware(middleware) + + ln, _ := net.Listen("tcp", "127.0.0.1:3000") + + err := m.Start(ln, nil) // specify nil for tlsConfig to use HTTP + if err != nil { + fmt.Fprintf(os.Stderr, "could not start server: %v\n", err) + os.Exit(1) + } + defer m.Shutdown() + + // cfg := m.Config() + // fmt.Printf("%s\n", litter.Sdump(cfg)) + + <-ch + fmt.Fprintf(os.Stderr, "oidcServer shutting down\n") +} + +func k8sSecretFile() (string, error) { + k8sSAtoken := `eyJhbGciOiJSUzI1NiIsImtpZCI6IjdjdDhhT0pTSXh0Zm0yUVprUVRXaFpTVFpHUlQ0MlFkbDMzQXQ1XzRURkkifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzg0Mzk4Mzk5LCJpYXQiOjE3NTI4NjIzOTksImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiNTE3Zjg4ZDUtOTNjZC00YWIzLWFkZWItY2NiZjExMDdmZGYxIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJ0ZXN0Iiwibm9kZSI6eyJuYW1lIjoia2luZC1jb250cm9sLXBsYW5lIiwidWlkIjoiZThjZTczMWMtMmZjNC00NWZjLWJjNzItMzdhYTgyNDQzN2EwIn0sInBvZCI6eyJuYW1lIjoib2lkYy1zZXJ2ZXIteDU4bm4iLCJ1aWQiOiJmM2Q3YTBkZC04Yzk3LTRkNTgtOGYyYy01ZDRiNThjMmY5NDIifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlZmF1bHQiLCJ1aWQiOiIwZjc1MTJhNi00ZjhkLTQxYjAtOTM4NC1mYWE4YzlmZWUxMWYifSwid2FybmFmdGVyIjoxNzUyODY2MDA2fSwibmJmIjoxNzUyODYyMzk5LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6dGVzdDpkZWZhdWx0In0.A8eDX9Wz6aoAwO-Vrg2ddbxJ5d7r3pdg8J6D4gyHPNQLRmBcZHaWagRKJTZ3gDYvT_u_hCG5RJrHARt9MncftPJ5_gdRyXckbd9a9dcSSRVxFEPzdaUR6GSmTmI2sUwhU33AnWmRqlOlZW_WtslPGXl8tNsfDLfpvabjAuBFJrb7KB8MvzXVNvVcJ8BmM4oglX3e3xIxLBzSSQFkW9OGdmeWFsMh-lNaHpzXQGaZx3W2Wit2SUigbDDSJPCTs_tFMdPv-LW0AH9eRd5yU_j87gEsapu_u5j6qcNku-3g79LcGoIvTqe8QdSI7OeoWVnD05SjfAoyHhR-aoMJtSCOQg` + + tokenPath := filepath.Join(os.TempDir(), "k8sToken") + err := os.WriteFile(tokenPath, []byte(k8sSAtoken), 0o600) + if err != nil { + return "", fmt.Errorf("error writing %s: %w", tokenPath, err) + } + + return tokenPath, nil +} diff --git a/pkg/kafka/configkafka/config.go b/pkg/kafka/configkafka/config.go index e5f71f0783218..17d657de4dc07 100644 --- a/pkg/kafka/configkafka/config.go +++ b/pkg/kafka/configkafka/config.go @@ -330,7 +330,8 @@ type SASLConfig struct { Username string `mapstructure:"username"` // Password to be used on authentication Password string `mapstructure:"password"` - // SASL Mechanism to be used, possible values are: (PLAIN, AWS_MSK_IAM_OAUTHBEARER, SCRAM-SHA-256 or SCRAM-SHA-512). + // SASL Mechanism to be used, possible values are: (PLAIN, AWS_MSK_IAM_OAUTHBEARER, OIDC_FILE, + // OIDC_STRING, SCRAM-SHA-256 or SCRAM-SHA-512). Mechanism string `mapstructure:"mechanism"` // SASL Protocol Version to be used, possible values are: (0, 1). Defaults to 0. Version int `mapstructure:"version"` From 0b1125ac3d86022faffbb819ad7aa8855fe795a9 Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Tue, 22 Jul 2025 16:18:49 -0600 Subject: [PATCH 03/38] Move OIDC client and test into sub-package. Signed-off-by: Rich Scott --- internal/kafka/authentication.go | 3 ++- internal/kafka/{ => oidc}/oidc_client.go | 16 ++++++++-------- internal/kafka/{ => oidc}/oidc_client_test.go | 4 ++-- 3 files changed, 12 insertions(+), 11 deletions(-) rename internal/kafka/{ => oidc}/oidc_client.go (87%) rename internal/kafka/{ => oidc}/oidc_client_test.go (97%) diff --git a/internal/kafka/authentication.go b/internal/kafka/authentication.go index eee29f73f0801..11611ad315a26 100644 --- a/internal/kafka/authentication.go +++ b/internal/kafka/authentication.go @@ -11,6 +11,7 @@ import ( "github.com/IBM/sarama" "github.com/aws/aws-msk-iam-sasl-signer-go/signer" + "github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka/oidc" "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/configkafka" ) @@ -59,7 +60,7 @@ func configureSASL(ctx context.Context, config configkafka.SASLConfig, saramaCon saramaConfig.Net.SASL.TokenProvider = &awsMSKTokenProvider{ctx: ctx, region: config.AWSMSK.Region} case OIDC_FILE: saramaConfig.Net.SASL.Mechanism = sarama.SASLTypeOAuth - saramaConfig.Net.SASL.TokenProvider = &oidcFileTokenProvider{} + saramaConfig.Net.SASL.TokenProvider = &oidc.OIDCfileTokenProvider{} } } diff --git a/internal/kafka/oidc_client.go b/internal/kafka/oidc/oidc_client.go similarity index 87% rename from internal/kafka/oidc_client.go rename to internal/kafka/oidc/oidc_client.go index c2f1ecaf8f222..ac1572271f516 100644 --- a/internal/kafka/oidc_client.go +++ b/internal/kafka/oidc/oidc_client.go @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package kafka // import "github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka" +package oidc // import "github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka/oidc" import ( "context" @@ -18,7 +18,7 @@ import ( "golang.org/x/oauth2/clientcredentials" ) -type oidcFileTokenProvider struct { +type OIDCfileTokenProvider struct { Ctx context.Context ClientID string ClientSecretFilePath string @@ -41,10 +41,10 @@ type oidcFileTokenProvider struct { AuthStyle oauth2.AuthStyle } -func NewOIDCFileTokenProvider(ctx context.Context, clientID, clientSecretFilePath, tokenURL string, +func NewOIDCfileTokenProvider(ctx context.Context, clientID, clientSecretFilePath, tokenURL string, scopes []string, refreshAhead time.Duration, ) sarama.AccessTokenProvider { - return &oidcFileTokenProvider{ + return &OIDCfileTokenProvider{ Ctx: ctx, ClientID: clientID, ClientSecretFilePath: clientSecretFilePath, @@ -53,7 +53,7 @@ func NewOIDCFileTokenProvider(ctx context.Context, clientID, clientSecretFilePat } } -func (p *oidcFileTokenProvider) Token() (*sarama.AccessToken, error) { +func (p *OIDCfileTokenProvider) Token() (*sarama.AccessToken, error) { oauthTok, err := p.GetToken() if err != nil { return nil, err @@ -62,7 +62,7 @@ func (p *oidcFileTokenProvider) Token() (*sarama.AccessToken, error) { return &sarama.AccessToken{Token: oauthTok.AccessToken}, nil } -func (p *oidcFileTokenProvider) updateToken() (*oauth2.Token, error) { +func (p *OIDCfileTokenProvider) updateToken() (*oauth2.Token, error) { p.mu.Lock() defer p.mu.Unlock() @@ -104,7 +104,7 @@ func (p *oidcFileTokenProvider) updateToken() (*oauth2.Token, error) { return oauthTok, nil } -func (p *oidcFileTokenProvider) GetToken() (*oauth2.Token, error) { +func (p *OIDCfileTokenProvider) GetToken() (*oauth2.Token, error) { p.mu.RLock() token := p.cachedToken expires := p.tokenExpiry @@ -127,7 +127,7 @@ func (p *oidcFileTokenProvider) GetToken() (*oauth2.Token, error) { return newToken, nil } -func (p *oidcFileTokenProvider) startBackgroundRefresher() { +func (p *OIDCfileTokenProvider) startBackgroundRefresher() { log.Printf("Will refresh access token for client %s and scope %s, %s before expiry", p.ClientID, p.Scopes[0], p.refreshAhead.String()) p.backgroundOnce.Do(func() { diff --git a/internal/kafka/oidc_client_test.go b/internal/kafka/oidc/oidc_client_test.go similarity index 97% rename from internal/kafka/oidc_client_test.go rename to internal/kafka/oidc/oidc_client_test.go index 139325bbef6e9..d32bad6d5a5ec 100644 --- a/internal/kafka/oidc_client_test.go +++ b/internal/kafka/oidc/oidc_client_test.go @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package kafka // import "github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka" +package oidc // import "github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka" import ( "context" "crypto/rand" @@ -55,7 +55,7 @@ func TestOIDCProvider_GetToken_Success(t *testing.T) { time.Sleep(3 * time.Second) // wait for OIDC to fully start tokenURL := "http://127.0.0.1:3000/oidc" - oidcProvider := NewOIDCFileTokenProvider(context.Background(), clientID, secretFile, tokenURL, []string{"mock-scope"}, 0) + oidcProvider := NewOIDCfileTokenProvider(context.Background(), clientID, secretFile, tokenURL, []string{"mock-scope"}, 0) token, err := oidcProvider.Token() require.NoError(t, err) From eca5df49968de48420f17924e8c4391d2765da8a Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Tue, 22 Jul 2025 16:42:10 -0600 Subject: [PATCH 04/38] Start new implementation of OIDC server. Signed-off-by: Rich Scott --- internal/kafka/go.mod | 2 +- internal/kafka/oidc/oidc_client_test.go | 169 ++++++++++++++++++++++ internal/kafka/oidc_client.go | 155 -------------------- internal/kafka/oidc_client_test.go | 180 ------------------------ 4 files changed, 170 insertions(+), 336 deletions(-) delete mode 100644 internal/kafka/oidc_client.go delete mode 100644 internal/kafka/oidc_client_test.go diff --git a/internal/kafka/go.mod b/internal/kafka/go.mod index 1864b8e9178ba..9bb862fd05e6f 100644 --- a/internal/kafka/go.mod +++ b/internal/kafka/go.mod @@ -5,6 +5,7 @@ go 1.23.0 require ( github.com/IBM/sarama v1.45.2 github.com/aws/aws-msk-iam-sasl-signer-go v1.0.4 + github.com/golang-jwt/jwt/v5 v5.2.0 github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/configkafka v0.130.0 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 @@ -21,7 +22,6 @@ require ( require ( github.com/go-jose/go-jose/v3 v3.0.1 // indirect - github.com/golang-jwt/jwt/v5 v5.2.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect ) diff --git a/internal/kafka/oidc/oidc_client_test.go b/internal/kafka/oidc/oidc_client_test.go index d32bad6d5a5ec..9361c90854db9 100644 --- a/internal/kafka/oidc/oidc_client_test.go +++ b/internal/kafka/oidc/oidc_client_test.go @@ -6,8 +6,10 @@ import ( "context" "crypto/rand" "crypto/rsa" + "encoding/json" "errors" "fmt" + "log" "net" "net/http" "os" @@ -16,6 +18,7 @@ import ( "time" "github.com/IBM/sarama" + "github.com/golang-jwt/jwt/v5" "github.com/oauth2-proxy/mockoidc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -178,3 +181,169 @@ func k8sSecretFile() (string, error) { return tokenPath, nil } + +// An implementation of a very basic OIDC server that supports only +// the "client_credentials" grant type. +type TokenRequest struct { + GrantType string `json:"grant_type"` + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` + Scope string `json:"scope"` +} + +type TokenResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + Scope string `json:"scope,omitempty"` +} + +type ErrorResponse struct { + Error string `json:"error"` + ErrorDescription string `json:"error_description,omitempty"` +} + +const ( + PORT = 8080 +) + +var ( + privateKey *rsa.PrivateKey + publicKey *rsa.PublicKey + tokenExpireSecs int +) + +func init() { + var err error + privateKey, err = rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + log.Fatal("Failed to generate RSA key:", err) + } + publicKey = &privateKey.PublicKey +} + +func tokenHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusMethodNotAllowed) + json.NewEncoder(w).Encode(ErrorResponse{ + Error: "invalid_request", + ErrorDescription: "Method not allowed", + }) + return + } + + if err := r.ParseForm(); err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(ErrorResponse{ + Error: "invalid_request", + ErrorDescription: "Failed to parse form data", + }) + return + } + + grantType := r.FormValue("grant_type") + clientID := r.FormValue("client_id") + clientSecret := r.FormValue("client_secret") + scope := r.FormValue("scope") + + if grantType != "client_credentials" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(ErrorResponse{ + Error: "unsupported_grant_type", + ErrorDescription: "Only client_credentials grant type is supported", + }) + return + } + + if clientID == "" || clientSecret == "" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(ErrorResponse{ + Error: "invalid_client", + ErrorDescription: "Client ID and secret are required", + }) + return + } + + // Simple client validation (in production, use proper authentication) + if !validateClient(clientID, clientSecret) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(ErrorResponse{ + Error: "invalid_client", + ErrorDescription: "Invalid client credentials", + }) + return + } + + // Generate JWT token + token, err := generateJWTToken(clientID, scope) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(ErrorResponse{ + Error: "server_error", + ErrorDescription: "Failed to generate token", + }) + return + } + + response := TokenResponse{ + AccessToken: token, + TokenType: "Bearer", + ExpiresIn: tokenExpireSecs, + Scope: scope, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) +} + +func validateClient(clientID, clientSecret string) bool { + // Simple hardcoded validation for demo purposes + // In production, validate against a database or external service + validClients := map[string]string{ + "test_client": "test_secret", + "demo_client": "demo_secret", + } + + expectedSecret, exists := validClients[clientID] + return exists && expectedSecret == clientSecret +} + +func generateJWTToken(clientID, scope string) (string, error) { + now := time.Now() + + claims := jwt.MapClaims{ + "iss": "oidc-mock-server", + "sub": clientID, + "aud": "api", + "exp": now.Add(time.Duration(tokenExpireSecs) * time.Second).Unix(), + "iat": now.Unix(), + "client_id": clientID, + } + + if scope != "" { + claims["scope"] = scope + } + + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + + return token.SignedString(privateKey) +} + +func main() { + tokenExpireSecs = 3600 + + http.HandleFunc("/token", tokenHandler) + + fmt.Printf("OIDC Mock Server starting on :%d\n", PORT) + fmt.Printf("Token endpoint: http://localhost:%d/token\n", PORT) + fmt.Println("Valid clients: test_client/test_secret, demo_client/demo_secret") + + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", PORT), nil)) +} diff --git a/internal/kafka/oidc_client.go b/internal/kafka/oidc_client.go deleted file mode 100644 index c2f1ecaf8f222..0000000000000 --- a/internal/kafka/oidc_client.go +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package kafka // import "github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka" - -import ( - "context" - "errors" - "fmt" - "log" - "net/url" - "os" - "sync" - "time" - - "github.com/IBM/sarama" - "golang.org/x/oauth2" - "golang.org/x/oauth2/clientcredentials" -) - -type oidcFileTokenProvider struct { - Ctx context.Context - ClientID string - ClientSecretFilePath string - TokenURL string - Scopes []string - - mu sync.RWMutex - backgroundOnce sync.Once - - cachedToken *oauth2.Token - - tokenExpiry time.Time - lastRefreshTime time.Time - - refreshAhead time.Duration - refreshCooldown time.Duration - - // TODO support the remaining fields of clientcredentials.Config - EndpointParams url.Values - AuthStyle oauth2.AuthStyle -} - -func NewOIDCFileTokenProvider(ctx context.Context, clientID, clientSecretFilePath, tokenURL string, - scopes []string, refreshAhead time.Duration, -) sarama.AccessTokenProvider { - return &oidcFileTokenProvider{ - Ctx: ctx, - ClientID: clientID, - ClientSecretFilePath: clientSecretFilePath, - TokenURL: tokenURL, - Scopes: scopes, - } -} - -func (p *oidcFileTokenProvider) Token() (*sarama.AccessToken, error) { - oauthTok, err := p.GetToken() - if err != nil { - return nil, err - } - - return &sarama.AccessToken{Token: oauthTok.AccessToken}, nil -} - -func (p *oidcFileTokenProvider) updateToken() (*oauth2.Token, error) { - p.mu.Lock() - defer p.mu.Unlock() - - now := time.Now() - log.Printf("Refreshing token for %s", p.ClientID) - if now.Sub(p.lastRefreshTime) < p.refreshCooldown { - // Someone just refreshed - skip - log.Printf("Skipping token refresh for %s, within the quiet window of %s", - p.ClientID, p.refreshCooldown) - return p.cachedToken, nil - } - - // Read the client secret every time we get a new token, - // as it may have changed in the meantime. - clientSecret, err := os.ReadFile(p.ClientSecretFilePath) - if err != nil { - return nil, fmt.Errorf("failed to read client secret: %w", err) - } - - fmt.Fprintf(os.Stderr, "updateToken(): p.TokenURL = %v\n", p.TokenURL) - oauthTok, err := (&clientcredentials.Config{ - ClientID: p.ClientID, - ClientSecret: string(clientSecret), - TokenURL: p.TokenURL + "/token", - Scopes: p.Scopes, - }).Token(p.Ctx) - - if err != nil || oauthTok == nil || oauthTok.AccessToken == "" { - return nil, fmt.Errorf("failed to refresh token: %w", err) - } - - expiresIn := time.Duration(oauthTok.ExpiresIn) * time.Second - p.cachedToken = oauthTok - p.tokenExpiry = now.Add(expiresIn) - p.lastRefreshTime = now - log.Printf("Token refreshed for %s, will expire after %s at %s", p.ClientID, - expiresIn.String(), p.tokenExpiry.String()) - - return oauthTok, nil -} - -func (p *oidcFileTokenProvider) GetToken() (*oauth2.Token, error) { - p.mu.RLock() - token := p.cachedToken - expires := p.tokenExpiry - hasToken := token != nil && token.AccessToken != "" - p.mu.RUnlock() - - if hasToken && expires.After(time.Now()) { - return token, nil - } - - // No valid cached token - do a blocking refresh - newToken, err := p.updateToken() - if err != nil { - return nil, err - } - - if newToken == nil || newToken.AccessToken == "" { - return nil, errors.New("token blank after fetch") - } - return newToken, nil -} - -func (p *oidcFileTokenProvider) startBackgroundRefresher() { - log.Printf("Will refresh access token for client %s and scope %s, %s before expiry", - p.ClientID, p.Scopes[0], p.refreshAhead.String()) - p.backgroundOnce.Do(func() { - go func() { - for { - sleepDuration := 0 * time.Minute - - p.mu.RLock() - if p.cachedToken != nil && p.cachedToken.AccessToken != "" { - sleepDuration = time.Until(p.tokenExpiry.Add(-p.refreshAhead)) - } - p.mu.RUnlock() - - if sleepDuration > 0 { - time.Sleep(sleepDuration) - continue - } - - if _, err := p.updateToken(); err != nil { - log.Printf("background token refresh failed: %v\n", err) - } - } - }() - }) -} diff --git a/internal/kafka/oidc_client_test.go b/internal/kafka/oidc_client_test.go deleted file mode 100644 index 139325bbef6e9..0000000000000 --- a/internal/kafka/oidc_client_test.go +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package kafka // import "github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka" -import ( - "context" - "crypto/rand" - "crypto/rsa" - "errors" - "fmt" - "net" - "net/http" - "os" - "path/filepath" - "testing" - "time" - - "github.com/IBM/sarama" - "github.com/oauth2-proxy/mockoidc" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type MockOAuthProvider struct { - MockSignin func(clientID, scope string, requestedAuthority ...string) (*sarama.AccessToken, error) -} - -func (m *MockOAuthProvider) Signin(clientID, scope string, requestedAuthority ...string) (*sarama.AccessToken, error) { - if m.MockSignin != nil { - return m.MockSignin(clientID, scope, requestedAuthority...) - } - return nil, errors.New("MockSignin function not defined") -} - -func (m *MockOAuthProvider) Name() string { - return "mock" -} - -func TestOIDCProvider_GetToken_Success(t *testing.T) { - clientID := "mock-client-id" - secretFile, err := k8sSecretFile() - assert.NoError(t, err) - - clientSecret, err := os.ReadFile(secretFile) - assert.NoError(t, err) - - oidcServerQuit := make(chan any) - go func() { - oidcServer(oidcServerQuit, clientID, string(clientSecret), 10, 10) - }() - defer func() { - oidcServerQuit <- true - }() - - time.Sleep(3 * time.Second) // wait for OIDC to fully start - tokenURL := "http://127.0.0.1:3000/oidc" - - oidcProvider := NewOIDCFileTokenProvider(context.Background(), clientID, secretFile, tokenURL, []string{"mock-scope"}, 0) - - token, err := oidcProvider.Token() - require.NoError(t, err) - assert.NotNil(t, token) - assert.NotEmpty(t, token.Token) -} - -// func TestOIDCProvider_GetToken_Error(t *testing.T) { -// mockProvider := &MockOAuthProvider{ -// // TODO can we remove all requestAuthority ... ? -// MockSignin: func(clientID, scope string, requestedAuthority ...string) (*sarama.AccessToken, error) { -// return nil, errors.New("failed to sign in") -// }, -// } - -// oidcProvider := NewOIDCFileTokenProvider("mock-client-id", "mock-scopes", mockProvider, 0) - -// token, err := oidcProvider.Token() -// assert.NotNil(t, err) -// assert.Nil(t, token) -// assert.Equal(t, "failed to refresh token: failed to sign in", err.Error()) -// } - -// func TestOIDCProvider_TokenCaching(t *testing.T) { -// mockProvider := &MockOAuthProvider{ -// MockSignin: func(clientID, scope string, requestedAuthority ...string) (*AccessToken, error) { -// return &AccessToken{ -// AccessToken: "mock-access-token", -// ExpiresIn: 3600, -// TokenType: "Bearer", -// Scope: "mock-scopes", -// }, nil -// }, -// } - -// oidcProvider := NewGRSigningAccessTokenProvider("mock-client-id", "mock-scopes", mockProvider, 0) - -// token1, err1 := oidcProvider.GetToken() -// assert.Nil(t, err1) -// assert.NotNil(t, token1) - -// token2, err2 := oidcProvider.GetToken() -// assert.Nil(t, err2) -// assert.NotNil(t, token2) -// assert.Equal(t, token1, token2) -// } - -// func TestOIDCProvider_TokenExpired(t *testing.T) { -// mockProvider := &MockOAuthProvider{ -// MockSignin: func(clientID, scope string, requestedAuthority ...string) (*AccessToken, error) { -// return &AccessToken{ -// AccessToken: "mock-access-token", -// ExpiresIn: 1, -// TokenType: "Bearer", -// Scope: "mock-scopes", -// }, nil -// }, -// } - -// oidcProvider := NewGRSigningAccessTokenProvider("mock-client-id", "mock-scopes", mockProvider, 0) -// oidcProvider.refreshCooldown = 500 * time.Millisecond - -// token1, err1 := oidcProvider.GetToken() -// assert.Nil(t, err1) -// assert.NotNil(t, token1) - -// time.Sleep(2 * time.Second) - -// token2, err2 := oidcProvider.GetToken() -// assert.Nil(t, err2) -// assert.NotNil(t, token2) -// assert.NotEqual(t, token1, token2) -// } - -func oidcServer(ch <-chan any, clientID, clientSecret string, accessTTLsecs, refreshTTLsecs int) { - // Create RSA Private Key for token signing - rsaKey, _ := rsa.GenerateKey(rand.Reader, 2048) - - m, _ := mockoidc.NewServer(rsaKey) - m.ClientID = clientID - m.ClientSecret = clientSecret - m.AccessTTL = time.Duration(accessTTLsecs) * time.Second - m.RefreshTTL = time.Duration(refreshTTLsecs) * time.Second - - middleware := func(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - fmt.Fprintf(os.Stderr, "\n-------- oidcServer: before next.ServerHTTP(): request URL is %v\n", req.URL) - // custom middleware logic here ... - next.ServeHTTP(rw, req) - fmt.Fprintf(os.Stderr, "\n-------- oidcServer: after next.ServeHTTP(): request URL is %v\n", req.URL) - // custom middleware logic here... - }) - } - m.AddMiddleware(middleware) - - ln, _ := net.Listen("tcp", "127.0.0.1:3000") - - err := m.Start(ln, nil) // specify nil for tlsConfig to use HTTP - if err != nil { - fmt.Fprintf(os.Stderr, "could not start server: %v\n", err) - os.Exit(1) - } - defer m.Shutdown() - - // cfg := m.Config() - // fmt.Printf("%s\n", litter.Sdump(cfg)) - - <-ch - fmt.Fprintf(os.Stderr, "oidcServer shutting down\n") -} - -func k8sSecretFile() (string, error) { - k8sSAtoken := `eyJhbGciOiJSUzI1NiIsImtpZCI6IjdjdDhhT0pTSXh0Zm0yUVprUVRXaFpTVFpHUlQ0MlFkbDMzQXQ1XzRURkkifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzg0Mzk4Mzk5LCJpYXQiOjE3NTI4NjIzOTksImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiNTE3Zjg4ZDUtOTNjZC00YWIzLWFkZWItY2NiZjExMDdmZGYxIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJ0ZXN0Iiwibm9kZSI6eyJuYW1lIjoia2luZC1jb250cm9sLXBsYW5lIiwidWlkIjoiZThjZTczMWMtMmZjNC00NWZjLWJjNzItMzdhYTgyNDQzN2EwIn0sInBvZCI6eyJuYW1lIjoib2lkYy1zZXJ2ZXIteDU4bm4iLCJ1aWQiOiJmM2Q3YTBkZC04Yzk3LTRkNTgtOGYyYy01ZDRiNThjMmY5NDIifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlZmF1bHQiLCJ1aWQiOiIwZjc1MTJhNi00ZjhkLTQxYjAtOTM4NC1mYWE4YzlmZWUxMWYifSwid2FybmFmdGVyIjoxNzUyODY2MDA2fSwibmJmIjoxNzUyODYyMzk5LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6dGVzdDpkZWZhdWx0In0.A8eDX9Wz6aoAwO-Vrg2ddbxJ5d7r3pdg8J6D4gyHPNQLRmBcZHaWagRKJTZ3gDYvT_u_hCG5RJrHARt9MncftPJ5_gdRyXckbd9a9dcSSRVxFEPzdaUR6GSmTmI2sUwhU33AnWmRqlOlZW_WtslPGXl8tNsfDLfpvabjAuBFJrb7KB8MvzXVNvVcJ8BmM4oglX3e3xIxLBzSSQFkW9OGdmeWFsMh-lNaHpzXQGaZx3W2Wit2SUigbDDSJPCTs_tFMdPv-LW0AH9eRd5yU_j87gEsapu_u5j6qcNku-3g79LcGoIvTqe8QdSI7OeoWVnD05SjfAoyHhR-aoMJtSCOQg` - - tokenPath := filepath.Join(os.TempDir(), "k8sToken") - err := os.WriteFile(tokenPath, []byte(k8sSAtoken), 0o600) - if err != nil { - return "", fmt.Errorf("error writing %s: %w", tokenPath, err) - } - - return tokenPath, nil -} From 65393c30df241701c5ee8f31317a248c68730143 Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Tue, 22 Jul 2025 17:49:50 -0600 Subject: [PATCH 05/38] golangci-lint fixes; fixes to use new OIDC server Signed-off-by: Rich Scott --- internal/kafka/authentication.go | 2 +- internal/kafka/client.go | 2 +- internal/kafka/franz_client.go | 2 +- internal/kafka/oidc/oidc_client_test.go | 160 +++++++++++++++--------- 4 files changed, 103 insertions(+), 63 deletions(-) diff --git a/internal/kafka/authentication.go b/internal/kafka/authentication.go index 11611ad315a26..d01d546954145 100644 --- a/internal/kafka/authentication.go +++ b/internal/kafka/authentication.go @@ -58,7 +58,7 @@ func configureSASL(ctx context.Context, config configkafka.SASLConfig, saramaCon case AWSMSKIAMOAUTHBEARER: saramaConfig.Net.SASL.Mechanism = sarama.SASLTypeOAuth saramaConfig.Net.SASL.TokenProvider = &awsMSKTokenProvider{ctx: ctx, region: config.AWSMSK.Region} - case OIDC_FILE: + case OIDCFILE: saramaConfig.Net.SASL.Mechanism = sarama.SASLTypeOAuth saramaConfig.Net.SASL.TokenProvider = &oidc.OIDCfileTokenProvider{} } diff --git a/internal/kafka/client.go b/internal/kafka/client.go index 86179e6c9cc34..baf419ecc0bc0 100644 --- a/internal/kafka/client.go +++ b/internal/kafka/client.go @@ -149,7 +149,7 @@ func newSaramaClientConfig(ctx context.Context, config configkafka.ClientConfig) } else if config.Authentication.SASL != nil && config.Authentication.SASL.Mechanism == "AWS_MSK_IAM_OAUTHBEARER" { saramaConfig.Net.TLS.Config = &tls.Config{} saramaConfig.Net.TLS.Enable = true - } else if config.Authentication.SASL != nil && config.Authentication.SASL.Mechanism == OIDC_FILE { + } else if config.Authentication.SASL != nil && config.Authentication.SASL.Mechanism == OIDCFILE { saramaConfig.Net.TLS.Enable = true } configureSaramaAuthentication(ctx, config.Authentication, saramaConfig) diff --git a/internal/kafka/franz_client.go b/internal/kafka/franz_client.go index f762e0115fb1b..7f2466ffd948f 100644 --- a/internal/kafka/franz_client.go +++ b/internal/kafka/franz_client.go @@ -32,7 +32,7 @@ const ( SCRAMSHA256 = "SCRAM-SHA-256" PLAIN = "PLAIN" AWSMSKIAMOAUTHBEARER = "AWS_MSK_IAM_OAUTHBEARER" //nolint:gosec // These aren't credentials. - OIDC_FILE = "OIDC_FILE" + OIDCFILE = "OIDC_FILE" ) // NewFranzSyncProducer creates a new Kafka client using the franz-go library. diff --git a/internal/kafka/oidc/oidc_client_test.go b/internal/kafka/oidc/oidc_client_test.go index 9361c90854db9..65fdcabad09ce 100644 --- a/internal/kafka/oidc/oidc_client_test.go +++ b/internal/kafka/oidc/oidc_client_test.go @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 package oidc // import "github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka" + import ( "context" "crypto/rand" @@ -10,7 +11,6 @@ import ( "errors" "fmt" "log" - "net" "net/http" "os" "path/filepath" @@ -19,7 +19,6 @@ import ( "github.com/IBM/sarama" "github.com/golang-jwt/jwt/v5" - "github.com/oauth2-proxy/mockoidc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -35,7 +34,7 @@ func (m *MockOAuthProvider) Signin(clientID, scope string, requestedAuthority .. return nil, errors.New("MockSignin function not defined") } -func (m *MockOAuthProvider) Name() string { +func (*MockOAuthProvider) Name() string { return "mock" } @@ -49,14 +48,14 @@ func TestOIDCProvider_GetToken_Success(t *testing.T) { oidcServerQuit := make(chan any) go func() { - oidcServer(oidcServerQuit, clientID, string(clientSecret), 10, 10) + oidcServer(oidcServerQuit, clientID, string(clientSecret), 10) }() defer func() { oidcServerQuit <- true }() time.Sleep(3 * time.Second) // wait for OIDC to fully start - tokenURL := "http://127.0.0.1:3000/oidc" + tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", PORT) oidcProvider := NewOIDCfileTokenProvider(context.Background(), clientID, secretFile, tokenURL, []string{"mock-scope"}, 0) @@ -133,42 +132,42 @@ func TestOIDCProvider_GetToken_Success(t *testing.T) { // assert.NotEqual(t, token1, token2) // } -func oidcServer(ch <-chan any, clientID, clientSecret string, accessTTLsecs, refreshTTLsecs int) { - // Create RSA Private Key for token signing - rsaKey, _ := rsa.GenerateKey(rand.Reader, 2048) - - m, _ := mockoidc.NewServer(rsaKey) - m.ClientID = clientID - m.ClientSecret = clientSecret - m.AccessTTL = time.Duration(accessTTLsecs) * time.Second - m.RefreshTTL = time.Duration(refreshTTLsecs) * time.Second - - middleware := func(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - fmt.Fprintf(os.Stderr, "\n-------- oidcServer: before next.ServerHTTP(): request URL is %v\n", req.URL) - // custom middleware logic here ... - next.ServeHTTP(rw, req) - fmt.Fprintf(os.Stderr, "\n-------- oidcServer: after next.ServeHTTP(): request URL is %v\n", req.URL) - // custom middleware logic here... - }) - } - m.AddMiddleware(middleware) +// func OLDoidcServer(ch <-chan any, clientID, clientSecret string, accessTTLsecs, refreshTTLsecs int) { +// // Create RSA Private Key for token signing +// rsaKey, _ := rsa.GenerateKey(rand.Reader, 2048) + +// m, _ := mockoidc.NewServer(rsaKey) +// m.ClientID = clientID +// m.ClientSecret = clientSecret +// m.AccessTTL = time.Duration(accessTTLsecs) * time.Second +// m.RefreshTTL = time.Duration(refreshTTLsecs) * time.Second + +// middleware := func(next http.Handler) http.Handler { +// return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { +// fmt.Fprintf(os.Stderr, "\n-------- oidcServer: before next.ServerHTTP(): request URL is %v\n", req.URL) +// // custom middleware logic here ... +// next.ServeHTTP(rw, req) +// fmt.Fprintf(os.Stderr, "\n-------- oidcServer: after next.ServeHTTP(): request URL is %v\n", req.URL) +// // custom middleware logic here... +// }) +// } +// m.AddMiddleware(middleware) - ln, _ := net.Listen("tcp", "127.0.0.1:3000") +// ln, _ := net.Listen("tcp", "127.0.0.1:3000") - err := m.Start(ln, nil) // specify nil for tlsConfig to use HTTP - if err != nil { - fmt.Fprintf(os.Stderr, "could not start server: %v\n", err) - os.Exit(1) - } - defer m.Shutdown() +// err := m.Start(ln, nil) // specify nil for tlsConfig to use HTTP +// if err != nil { +// fmt.Fprintf(os.Stderr, "could not start server: %v\n", err) +// os.Exit(1) +// } +// defer m.Shutdown() - // cfg := m.Config() - // fmt.Printf("%s\n", litter.Sdump(cfg)) +// // cfg := m.Config() +// // fmt.Printf("%s\n", litter.Sdump(cfg)) - <-ch - fmt.Fprintf(os.Stderr, "oidcServer shutting down\n") -} +// <-ch +// fmt.Fprintf(os.Stderr, "oidcServer shutting down\n") +// } func k8sSecretFile() (string, error) { k8sSAtoken := `eyJhbGciOiJSUzI1NiIsImtpZCI6IjdjdDhhT0pTSXh0Zm0yUVprUVRXaFpTVFpHUlQ0MlFkbDMzQXQ1XzRURkkifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzg0Mzk4Mzk5LCJpYXQiOjE3NTI4NjIzOTksImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiNTE3Zjg4ZDUtOTNjZC00YWIzLWFkZWItY2NiZjExMDdmZGYxIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJ0ZXN0Iiwibm9kZSI6eyJuYW1lIjoia2luZC1jb250cm9sLXBsYW5lIiwidWlkIjoiZThjZTczMWMtMmZjNC00NWZjLWJjNzItMzdhYTgyNDQzN2EwIn0sInBvZCI6eyJuYW1lIjoib2lkYy1zZXJ2ZXIteDU4bm4iLCJ1aWQiOiJmM2Q3YTBkZC04Yzk3LTRkNTgtOGYyYy01ZDRiNThjMmY5NDIifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlZmF1bHQiLCJ1aWQiOiIwZjc1MTJhNi00ZjhkLTQxYjAtOTM4NC1mYWE4YzlmZWUxMWYifSwid2FybmFmdGVyIjoxNzUyODY2MDA2fSwibmJmIjoxNzUyODYyMzk5LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6dGVzdDpkZWZhdWx0In0.A8eDX9Wz6aoAwO-Vrg2ddbxJ5d7r3pdg8J6D4gyHPNQLRmBcZHaWagRKJTZ3gDYvT_u_hCG5RJrHARt9MncftPJ5_gdRyXckbd9a9dcSSRVxFEPzdaUR6GSmTmI2sUwhU33AnWmRqlOlZW_WtslPGXl8tNsfDLfpvabjAuBFJrb7KB8MvzXVNvVcJ8BmM4oglX3e3xIxLBzSSQFkW9OGdmeWFsMh-lNaHpzXQGaZx3W2Wit2SUigbDDSJPCTs_tFMdPv-LW0AH9eRd5yU_j87gEsapu_u5j6qcNku-3g79LcGoIvTqe8QdSI7OeoWVnD05SjfAoyHhR-aoMJtSCOQg` @@ -204,13 +203,15 @@ type ErrorResponse struct { } const ( - PORT = 8080 + PORT = 3000 ) var ( privateKey *rsa.PrivateKey publicKey *rsa.PublicKey tokenExpireSecs int + clientID string + clientSecret string ) func init() { @@ -226,68 +227,86 @@ func tokenHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusMethodNotAllowed) - json.NewEncoder(w).Encode(ErrorResponse{ + err := json.NewEncoder(w).Encode(ErrorResponse{ Error: "invalid_request", ErrorDescription: "Method not allowed", }) + if err != nil { + log.Printf("could not encode error response: %v", err) + } return } if err := r.ParseForm(); err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(ErrorResponse{ + err := json.NewEncoder(w).Encode(ErrorResponse{ Error: "invalid_request", ErrorDescription: "Failed to parse form data", }) + if err != nil { + log.Printf("could not encode error response: %v", err) + } return } grantType := r.FormValue("grant_type") - clientID := r.FormValue("client_id") - clientSecret := r.FormValue("client_secret") + submittedClientID := r.FormValue("client_id") + submittedClientSecret := r.FormValue("client_secret") scope := r.FormValue("scope") if grantType != "client_credentials" { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(ErrorResponse{ + err := json.NewEncoder(w).Encode(ErrorResponse{ Error: "unsupported_grant_type", ErrorDescription: "Only client_credentials grant type is supported", }) + if err != nil { + log.Printf("could not encode error response: %v", err) + } return } - if clientID == "" || clientSecret == "" { + if submittedClientID == "" || submittedClientSecret == "" { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(ErrorResponse{ + err := json.NewEncoder(w).Encode(ErrorResponse{ Error: "invalid_client", ErrorDescription: "Client ID and secret are required", }) + if err != nil { + log.Printf("could not encode error response: %v", err) + } return } // Simple client validation (in production, use proper authentication) - if !validateClient(clientID, clientSecret) { + if !validateClient(submittedClientID, submittedClientSecret) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) - json.NewEncoder(w).Encode(ErrorResponse{ + err := json.NewEncoder(w).Encode(ErrorResponse{ Error: "invalid_client", ErrorDescription: "Invalid client credentials", }) + if err != nil { + log.Printf("could not encode error response: %v", err) + } return } // Generate JWT token - token, err := generateJWTToken(clientID, scope) + token, err := generateJWTToken(submittedClientID, scope) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(ErrorResponse{ + encodeErr := json.NewEncoder(w).Encode(ErrorResponse{ Error: "server_error", ErrorDescription: "Failed to generate token", }) + if encodeErr != nil { + log.Printf("could not encode error response: %v", encodeErr) + } return } @@ -300,15 +319,17 @@ func tokenHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(response) + err = json.NewEncoder(w).Encode(response) + if err != nil { + log.Printf("could not encode response: %v", err) + } } func validateClient(clientID, clientSecret string) bool { - // Simple hardcoded validation for demo purposes - // In production, validate against a database or external service validClients := map[string]string{ - "test_client": "test_secret", - "demo_client": "demo_secret", + clientID: clientSecret, + // "test_client": "test_secret", + // "demo_client": "demo_secret", } expectedSecret, exists := validClients[clientID] @@ -336,14 +357,33 @@ func generateJWTToken(clientID, scope string) (string, error) { return token.SignedString(privateKey) } -func main() { - tokenExpireSecs = 3600 +func oidcServer(ch <-chan any, cid, csecret string, accessTTLsecs int) { + tokenExpireSecs = accessTTLsecs + clientID = cid + clientSecret = csecret + + fmt.Printf("OIDC Mock Server starting on :%d\n", PORT) + // fmt.Printf("Token endpoint: http://localhost:%d/token\n", PORT) http.HandleFunc("/token", tokenHandler) + // log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", PORT), nil)) - fmt.Printf("OIDC Mock Server starting on :%d\n", PORT) - fmt.Printf("Token endpoint: http://localhost:%d/token\n", PORT) - fmt.Println("Valid clients: test_client/test_secret, demo_client/demo_secret") + s := &http.Server{ + Addr: fmt.Sprintf(":%d", PORT), + ReadHeaderTimeout: 10 * time.Second, + ReadTimeout: 10 * time.Second, + } + err := s.ListenAndServe() + if err != nil { + log.Fatalf("could not start OIDC server: %v", err) + } + defer func() { + err := s.Shutdown(context.Background()) + if err != nil { + log.Fatalf("error shutting down OIDC server: %v", err) + } + }() - log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", PORT), nil)) + <-ch + fmt.Fprintf(os.Stderr, "oidcServer shutting down\n") } From 6a6e112f3e94c1686bc7b4404d66bf1e5c06dccb Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Wed, 23 Jul 2025 09:21:52 -0600 Subject: [PATCH 06/38] Get OIDC mock server goroutine working. Signed-off-by: Rich Scott --- internal/kafka/oidc/oidc_client.go | 3 +-- internal/kafka/oidc/oidc_client_test.go | 26 ++++++++++++------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/internal/kafka/oidc/oidc_client.go b/internal/kafka/oidc/oidc_client.go index ac1572271f516..5ff3c3bdf89a1 100644 --- a/internal/kafka/oidc/oidc_client.go +++ b/internal/kafka/oidc/oidc_client.go @@ -82,11 +82,10 @@ func (p *OIDCfileTokenProvider) updateToken() (*oauth2.Token, error) { return nil, fmt.Errorf("failed to read client secret: %w", err) } - fmt.Fprintf(os.Stderr, "updateToken(): p.TokenURL = %v\n", p.TokenURL) oauthTok, err := (&clientcredentials.Config{ ClientID: p.ClientID, ClientSecret: string(clientSecret), - TokenURL: p.TokenURL + "/token", + TokenURL: p.TokenURL, Scopes: p.Scopes, }).Token(p.Ctx) diff --git a/internal/kafka/oidc/oidc_client_test.go b/internal/kafka/oidc/oidc_client_test.go index 65fdcabad09ce..41ee38abd331a 100644 --- a/internal/kafka/oidc/oidc_client_test.go +++ b/internal/kafka/oidc/oidc_client_test.go @@ -54,7 +54,7 @@ func TestOIDCProvider_GetToken_Success(t *testing.T) { oidcServerQuit <- true }() - time.Sleep(3 * time.Second) // wait for OIDC to fully start + time.Sleep(500 * time.Millisecond) // wait for OIDC server to fully start tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", PORT) oidcProvider := NewOIDCfileTokenProvider(context.Background(), clientID, secretFile, tokenURL, []string{"mock-scope"}, 0) @@ -362,28 +362,26 @@ func oidcServer(ch <-chan any, cid, csecret string, accessTTLsecs int) { clientID = cid clientSecret = csecret - fmt.Printf("OIDC Mock Server starting on :%d\n", PORT) - // fmt.Printf("Token endpoint: http://localhost:%d/token\n", PORT) - http.HandleFunc("/token", tokenHandler) - // log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", PORT), nil)) - s := &http.Server{ Addr: fmt.Sprintf(":%d", PORT), ReadHeaderTimeout: 10 * time.Second, ReadTimeout: 10 * time.Second, } - err := s.ListenAndServe() - if err != nil { - log.Fatalf("could not start OIDC server: %v", err) - } - defer func() { - err := s.Shutdown(context.Background()) + + go func() { + fmt.Printf("OIDC Mock Server starting on :%d\n", PORT) + err := s.ListenAndServe() if err != nil { - log.Fatalf("error shutting down OIDC server: %v", err) + log.Fatalf("could not start OIDC server: %v", err) } }() <-ch - fmt.Fprintf(os.Stderr, "oidcServer shutting down\n") + fmt.Printf("OIDC server shutting down\n") + err := s.Shutdown(context.Background()) + if err != nil { + log.Fatalf("error shutting down OIDC server: %v", err) + } + } From 379fb01ae264f2b41d0c73fb852388794af99824 Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Wed, 23 Jul 2025 10:18:29 -0600 Subject: [PATCH 07/38] Reduce wait for mock server startup to 100ms. Signed-off-by: Rich Scott --- internal/kafka/go.mod | 7 ++----- internal/kafka/go.sum | 17 +++++------------ internal/kafka/oidc/oidc_client.go | 6 ++++-- internal/kafka/oidc/oidc_client_test.go | 3 +-- 4 files changed, 12 insertions(+), 21 deletions(-) diff --git a/internal/kafka/go.mod b/internal/kafka/go.mod index 9bb862fd05e6f..5d579fe5afa8d 100644 --- a/internal/kafka/go.mod +++ b/internal/kafka/go.mod @@ -6,9 +6,9 @@ require ( github.com/IBM/sarama v1.45.2 github.com/aws/aws-msk-iam-sasl-signer-go v1.0.4 github.com/golang-jwt/jwt/v5 v5.2.0 - github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/configkafka v0.130.0 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 + github.com/sanity-io/litter v1.5.8 github.com/stretchr/testify v1.10.0 github.com/twmb/franz-go/pkg/kfake v0.0.0-20250320172111-35ab5e5f5327 github.com/twmb/franz-go/pkg/sasl/kerberos v1.1.0 @@ -20,10 +20,7 @@ require ( golang.org/x/oauth2 v0.30.0 ) -require ( - github.com/go-jose/go-jose/v3 v3.0.1 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect -) +require go.yaml.in/yaml/v3 v3.0.4 // indirect require ( github.com/aws/aws-sdk-go-v2 v1.36.4 // indirect diff --git a/internal/kafka/go.sum b/internal/kafka/go.sum index b1e07797ae1f8..3a8ec34b84968 100644 --- a/internal/kafka/go.sum +++ b/internal/kafka/go.sum @@ -28,6 +28,7 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.33.21 h1:nyLjs8sYJShFYj6aiyjCBI3EcLn github.com/aws/aws-sdk-go-v2/service/sts v1.33.21/go.mod h1:EhdxtZ+g84MSGrSrHzZiUm9PYiZkrADNja15wtRJSJo= github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -46,8 +47,6 @@ github.com/foxboron/swtpm_test v0.0.0-20230726224112-46aaafdf7006 h1:50sW4r0Pcvl github.com/foxboron/swtpm_test v0.0.0-20230726224112-46aaafdf7006/go.mod h1:eIXCMsMYCaqq9m1KSSxXwQG11krpuNPGP3k0uaWrbas= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= -github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -62,9 +61,6 @@ github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1 github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU= github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98= @@ -113,11 +109,10 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 h1:9bCMuD3TcnjeqjPT2gSlha4asp8NvgcFRYExCaikCxk= -github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25/go.mod h1:eDjgYHYDJbPLBLsyZ6qRaugP0mX8vePOhZ5id1fdzJw= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -125,11 +120,13 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5X github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg= +github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -195,7 +192,6 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -203,7 +199,6 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58 golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -222,7 +217,6 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -247,7 +241,6 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= diff --git a/internal/kafka/oidc/oidc_client.go b/internal/kafka/oidc/oidc_client.go index 5ff3c3bdf89a1..d4d2f57c662f6 100644 --- a/internal/kafka/oidc/oidc_client.go +++ b/internal/kafka/oidc/oidc_client.go @@ -14,6 +14,7 @@ import ( "time" "github.com/IBM/sarama" + "github.com/sanity-io/litter" "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" ) @@ -93,12 +94,13 @@ func (p *OIDCfileTokenProvider) updateToken() (*oauth2.Token, error) { return nil, fmt.Errorf("failed to refresh token: %w", err) } + log.Print(fmt.Sprintf("oauthTok = %s", litter.Sdump(oauthTok))) expiresIn := time.Duration(oauthTok.ExpiresIn) * time.Second p.cachedToken = oauthTok p.tokenExpiry = now.Add(expiresIn) p.lastRefreshTime = now - log.Printf("Token refreshed for %s, will expire after %s at %s", p.ClientID, - expiresIn.String(), p.tokenExpiry.String()) + // log.Printf("Token refreshed for %s, will expire after %s at %s", p.ClientID, + // expiresIn.String(), p.tokenExpiry.String()) return oauthTok, nil } diff --git a/internal/kafka/oidc/oidc_client_test.go b/internal/kafka/oidc/oidc_client_test.go index 41ee38abd331a..e82586bf4b127 100644 --- a/internal/kafka/oidc/oidc_client_test.go +++ b/internal/kafka/oidc/oidc_client_test.go @@ -54,7 +54,7 @@ func TestOIDCProvider_GetToken_Success(t *testing.T) { oidcServerQuit <- true }() - time.Sleep(500 * time.Millisecond) // wait for OIDC server to fully start + time.Sleep(100 * time.Millisecond) // wait for OIDC server to fully start tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", PORT) oidcProvider := NewOIDCfileTokenProvider(context.Background(), clientID, secretFile, tokenURL, []string{"mock-scope"}, 0) @@ -383,5 +383,4 @@ func oidcServer(ch <-chan any, cid, csecret string, accessTTLsecs int) { if err != nil { log.Fatalf("error shutting down OIDC server: %v", err) } - } From 57f9113cb7cc6eb60b2a6d2bd9206c9faf29efeb Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Fri, 25 Jul 2025 12:01:07 -0600 Subject: [PATCH 08/38] Set our expiresIn from either token's ExpiresIn or Expiry Add more assertions on the inner token string in the resultant sarama access token. Signed-off-by: Rich Scott --- internal/kafka/oidc/oidc_client.go | 22 ++++++++-- internal/kafka/oidc/oidc_client_test.go | 54 ++++++++++++++++--------- 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/internal/kafka/oidc/oidc_client.go b/internal/kafka/oidc/oidc_client.go index d4d2f57c662f6..a386007970d52 100644 --- a/internal/kafka/oidc/oidc_client.go +++ b/internal/kafka/oidc/oidc_client.go @@ -14,7 +14,6 @@ import ( "time" "github.com/IBM/sarama" - "github.com/sanity-io/litter" "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" ) @@ -94,10 +93,19 @@ func (p *OIDCfileTokenProvider) updateToken() (*oauth2.Token, error) { return nil, fmt.Errorf("failed to refresh token: %w", err) } - log.Print(fmt.Sprintf("oauthTok = %s", litter.Sdump(oauthTok))) - expiresIn := time.Duration(oauthTok.ExpiresIn) * time.Second + // log.Printf("CLIENT updateToken(): oauthTok = %s", DumpOauth2Token(oauthTok)) + + // oauth2.Token() in golang.org/x/oauth2 v0.30.0 appears not to populate the `ExpiresIn` field(?) from + // the server response. The Expiry/`expiry` field is not standard in the OIDC or Oauth2 specs. + var expiresIn int64 + if oauthTok.ExpiresIn != 0 { + expiresIn = oauthTok.ExpiresIn + } else { + expiresIn = (oauthTok.Expiry.UnixMilli() - time.Now().UnixMilli()) / 1000 + } + p.cachedToken = oauthTok - p.tokenExpiry = now.Add(expiresIn) + p.tokenExpiry = now.Add(time.Duration(expiresIn) * time.Second) p.lastRefreshTime = now // log.Printf("Token refreshed for %s, will expire after %s at %s", p.ClientID, // expiresIn.String(), p.tokenExpiry.String()) @@ -154,3 +162,9 @@ func (p *OIDCfileTokenProvider) startBackgroundRefresher() { }() }) } + +func DumpOauth2Token(tok *oauth2.Token) string { + return fmt.Sprintf("\nAccessToken: %s...%s\nTokenType: %s\nRefreshToken: %s\nExpiry: %s\nExpiresIn: %d\n", + tok.AccessToken[0:8], tok.AccessToken[len(tok.AccessToken)-8:], tok.TokenType, + tok.RefreshToken, tok.Expiry.Format(time.RFC3339), tok.ExpiresIn) +} diff --git a/internal/kafka/oidc/oidc_client_test.go b/internal/kafka/oidc/oidc_client_test.go index e82586bf4b127..dc1dd0c6bbc76 100644 --- a/internal/kafka/oidc/oidc_client_test.go +++ b/internal/kafka/oidc/oidc_client_test.go @@ -19,8 +19,10 @@ import ( "github.com/IBM/sarama" "github.com/golang-jwt/jwt/v5" + // "github.com/sanity-io/litter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/oauth2" ) type MockOAuthProvider struct { @@ -39,7 +41,10 @@ func (*MockOAuthProvider) Name() string { } func TestOIDCProvider_GetToken_Success(t *testing.T) { - clientID := "mock-client-id" + const clientID = "mock-client-id" + const timeOutSecs = 60 + const scope = "mock-scope" + secretFile, err := k8sSecretFile() assert.NoError(t, err) @@ -48,21 +53,34 @@ func TestOIDCProvider_GetToken_Success(t *testing.T) { oidcServerQuit := make(chan any) go func() { - oidcServer(oidcServerQuit, clientID, string(clientSecret), 10) + oidcServer(oidcServerQuit, clientID, string(clientSecret), timeOutSecs) }() defer func() { oidcServerQuit <- true }() - time.Sleep(100 * time.Millisecond) // wait for OIDC server to fully start + time.Sleep(50 * time.Millisecond) // wait for OIDC server to fully start tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", PORT) - oidcProvider := NewOIDCfileTokenProvider(context.Background(), clientID, secretFile, tokenURL, []string{"mock-scope"}, 0) + oidcProvider := NewOIDCfileTokenProvider(context.Background(), clientID, secretFile, tokenURL, []string{scope}, 0) - token, err := oidcProvider.Token() + saramaToken, err := oidcProvider.Token() require.NoError(t, err) - assert.NotNil(t, token) - assert.NotEmpty(t, token.Token) + assert.NotNil(t, saramaToken) + assert.NotEmpty(t, saramaToken.Token) + + parser := jwt.NewParser(jwt.WithoutClaimsValidation()) + tokenObj, err := parser.Parse(saramaToken.Token, func(token *jwt.Token) (any, error) { + return publicKey, nil + }) + assert.NoError(t, err) + assert.NotNil(t, tokenObj) + claims := tokenObj.Claims.(jwt.MapClaims) + assert.Equal(t, clientID, claims["client_id"]) + assert.Equal(t, scope, claims["scope"]) + + assert.WithinDuration(t, time.Now(), time.Unix(int64(claims["iat"].(float64)), 0), 2*time.Second) + assert.WithinDuration(t, time.Now().Add(timeOutSecs*time.Second), time.Unix(int64(claims["exp"].(float64)), 0), 2*time.Second) } // func TestOIDCProvider_GetToken_Error(t *testing.T) { @@ -190,13 +208,6 @@ type TokenRequest struct { Scope string `json:"scope"` } -type TokenResponse struct { - AccessToken string `json:"access_token"` - TokenType string `json:"token_type"` - ExpiresIn int `json:"expires_in"` - Scope string `json:"scope,omitempty"` -} - type ErrorResponse struct { Error string `json:"error"` ErrorDescription string `json:"error_description,omitempty"` @@ -281,7 +292,6 @@ func tokenHandler(w http.ResponseWriter, r *http.Request) { return } - // Simple client validation (in production, use proper authentication) if !validateClient(submittedClientID, submittedClientSecret) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) @@ -295,7 +305,6 @@ func tokenHandler(w http.ResponseWriter, r *http.Request) { return } - // Generate JWT token token, err := generateJWTToken(submittedClientID, scope) if err != nil { w.Header().Set("Content-Type", "application/json") @@ -310,13 +319,18 @@ func tokenHandler(w http.ResponseWriter, r *http.Request) { return } - response := TokenResponse{ + // log.Printf("tokenHandler(): tokenExpireSecs = %v", tokenExpireSecs) + + response := oauth2.Token{ AccessToken: token, TokenType: "Bearer", - ExpiresIn: tokenExpireSecs, - Scope: scope, + // Note that `expiry` is not an official field in the OIDC or OAuth2 specs + Expiry: time.Now().Add(time.Duration(tokenExpireSecs) * time.Second), + ExpiresIn: int64(tokenExpireSecs), } + // log.Printf("SERVER tokenHandler(): response token = %s\n", DumpOauth2Token(&response)) + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) err = json.NewEncoder(w).Encode(response) @@ -370,7 +384,7 @@ func oidcServer(ch <-chan any, cid, csecret string, accessTTLsecs int) { } go func() { - fmt.Printf("OIDC Mock Server starting on :%d\n", PORT) + fmt.Printf("OIDC Mock Server starting\n") err := s.ListenAndServe() if err != nil { log.Fatalf("could not start OIDC server: %v", err) From 31f0a0f1647d8859d33efd275b08b3ef97307f87 Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Fri, 25 Jul 2025 16:53:55 -0600 Subject: [PATCH 09/38] Simplify mock OIDC server invocation args; remove dead code Signed-off-by: Rich Scott --- internal/kafka/oidc/oidc_client_test.go | 113 +++++++++++------------- 1 file changed, 50 insertions(+), 63 deletions(-) diff --git a/internal/kafka/oidc/oidc_client_test.go b/internal/kafka/oidc/oidc_client_test.go index dc1dd0c6bbc76..2d877a90ec160 100644 --- a/internal/kafka/oidc/oidc_client_test.go +++ b/internal/kafka/oidc/oidc_client_test.go @@ -19,12 +19,18 @@ import ( "github.com/IBM/sarama" "github.com/golang-jwt/jwt/v5" - // "github.com/sanity-io/litter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/oauth2" ) +const ( + PORT = 3000 + testClientID = "mock-client-id" + testScope = "mock-scope" + tokenExpireSecs = 60 +) + type MockOAuthProvider struct { MockSignin func(clientID, scope string, requestedAuthority ...string) (*sarama.AccessToken, error) } @@ -41,19 +47,15 @@ func (*MockOAuthProvider) Name() string { } func TestOIDCProvider_GetToken_Success(t *testing.T) { - const clientID = "mock-client-id" - const timeOutSecs = 60 - const scope = "mock-scope" - secretFile, err := k8sSecretFile() assert.NoError(t, err) - clientSecret, err := os.ReadFile(secretFile) + testClientSecret, err = os.ReadFile(secretFile) assert.NoError(t, err) oidcServerQuit := make(chan any) go func() { - oidcServer(oidcServerQuit, clientID, string(clientSecret), timeOutSecs) + oidcServer(oidcServerQuit) }() defer func() { oidcServerQuit <- true @@ -62,7 +64,7 @@ func TestOIDCProvider_GetToken_Success(t *testing.T) { time.Sleep(50 * time.Millisecond) // wait for OIDC server to fully start tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", PORT) - oidcProvider := NewOIDCfileTokenProvider(context.Background(), clientID, secretFile, tokenURL, []string{scope}, 0) + oidcProvider := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, []string{testScope}, 0) saramaToken, err := oidcProvider.Token() require.NoError(t, err) @@ -76,11 +78,39 @@ func TestOIDCProvider_GetToken_Success(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, tokenObj) claims := tokenObj.Claims.(jwt.MapClaims) - assert.Equal(t, clientID, claims["client_id"]) - assert.Equal(t, scope, claims["scope"]) + assert.Equal(t, testClientID, claims["client_id"]) + assert.Equal(t, testScope, claims["scope"]) assert.WithinDuration(t, time.Now(), time.Unix(int64(claims["iat"].(float64)), 0), 2*time.Second) - assert.WithinDuration(t, time.Now().Add(timeOutSecs*time.Second), time.Unix(int64(claims["exp"].(float64)), 0), 2*time.Second) + expectedTimeout := time.Now().Add(tokenExpireSecs * time.Second) + actualTimeout := time.Unix(int64(claims["exp"].(float64)), 0) + assert.WithinDuration(t, expectedTimeout, actualTimeout, 2*time.Second) +} + +func TestOIDCProvider_GetToken_Error(t *testing.T) { + secretFile, err := k8sSecretFile() + assert.NoError(t, err) + + testClientSecret, err = os.ReadFile(secretFile) + assert.NoError(t, err) + + oidcServerQuit := make(chan any) + go func() { + oidcServer(oidcServerQuit) + }() + defer func() { + oidcServerQuit <- true + }() + + time.Sleep(50 * time.Millisecond) // wait for OIDC server to fully start + tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", PORT) + + oidcProvider := NewOIDCfileTokenProvider(context.Background(), "wrong-client-id", secretFile, + tokenURL, []string{testScope}, 0) + + saramaToken, err := oidcProvider.Token() + require.Error(t, err) + assert.Nil(t, saramaToken) } // func TestOIDCProvider_GetToken_Error(t *testing.T) { @@ -150,43 +180,6 @@ func TestOIDCProvider_GetToken_Success(t *testing.T) { // assert.NotEqual(t, token1, token2) // } -// func OLDoidcServer(ch <-chan any, clientID, clientSecret string, accessTTLsecs, refreshTTLsecs int) { -// // Create RSA Private Key for token signing -// rsaKey, _ := rsa.GenerateKey(rand.Reader, 2048) - -// m, _ := mockoidc.NewServer(rsaKey) -// m.ClientID = clientID -// m.ClientSecret = clientSecret -// m.AccessTTL = time.Duration(accessTTLsecs) * time.Second -// m.RefreshTTL = time.Duration(refreshTTLsecs) * time.Second - -// middleware := func(next http.Handler) http.Handler { -// return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { -// fmt.Fprintf(os.Stderr, "\n-------- oidcServer: before next.ServerHTTP(): request URL is %v\n", req.URL) -// // custom middleware logic here ... -// next.ServeHTTP(rw, req) -// fmt.Fprintf(os.Stderr, "\n-------- oidcServer: after next.ServeHTTP(): request URL is %v\n", req.URL) -// // custom middleware logic here... -// }) -// } -// m.AddMiddleware(middleware) - -// ln, _ := net.Listen("tcp", "127.0.0.1:3000") - -// err := m.Start(ln, nil) // specify nil for tlsConfig to use HTTP -// if err != nil { -// fmt.Fprintf(os.Stderr, "could not start server: %v\n", err) -// os.Exit(1) -// } -// defer m.Shutdown() - -// // cfg := m.Config() -// // fmt.Printf("%s\n", litter.Sdump(cfg)) - -// <-ch -// fmt.Fprintf(os.Stderr, "oidcServer shutting down\n") -// } - func k8sSecretFile() (string, error) { k8sSAtoken := `eyJhbGciOiJSUzI1NiIsImtpZCI6IjdjdDhhT0pTSXh0Zm0yUVprUVRXaFpTVFpHUlQ0MlFkbDMzQXQ1XzRURkkifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzg0Mzk4Mzk5LCJpYXQiOjE3NTI4NjIzOTksImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiNTE3Zjg4ZDUtOTNjZC00YWIzLWFkZWItY2NiZjExMDdmZGYxIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJ0ZXN0Iiwibm9kZSI6eyJuYW1lIjoia2luZC1jb250cm9sLXBsYW5lIiwidWlkIjoiZThjZTczMWMtMmZjNC00NWZjLWJjNzItMzdhYTgyNDQzN2EwIn0sInBvZCI6eyJuYW1lIjoib2lkYy1zZXJ2ZXIteDU4bm4iLCJ1aWQiOiJmM2Q3YTBkZC04Yzk3LTRkNTgtOGYyYy01ZDRiNThjMmY5NDIifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlZmF1bHQiLCJ1aWQiOiIwZjc1MTJhNi00ZjhkLTQxYjAtOTM4NC1mYWE4YzlmZWUxMWYifSwid2FybmFmdGVyIjoxNzUyODY2MDA2fSwibmJmIjoxNzUyODYyMzk5LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6dGVzdDpkZWZhdWx0In0.A8eDX9Wz6aoAwO-Vrg2ddbxJ5d7r3pdg8J6D4gyHPNQLRmBcZHaWagRKJTZ3gDYvT_u_hCG5RJrHARt9MncftPJ5_gdRyXckbd9a9dcSSRVxFEPzdaUR6GSmTmI2sUwhU33AnWmRqlOlZW_WtslPGXl8tNsfDLfpvabjAuBFJrb7KB8MvzXVNvVcJ8BmM4oglX3e3xIxLBzSSQFkW9OGdmeWFsMh-lNaHpzXQGaZx3W2Wit2SUigbDDSJPCTs_tFMdPv-LW0AH9eRd5yU_j87gEsapu_u5j6qcNku-3g79LcGoIvTqe8QdSI7OeoWVnD05SjfAoyHhR-aoMJtSCOQg` @@ -213,16 +206,10 @@ type ErrorResponse struct { ErrorDescription string `json:"error_description,omitempty"` } -const ( - PORT = 3000 -) - var ( - privateKey *rsa.PrivateKey - publicKey *rsa.PublicKey - tokenExpireSecs int - clientID string - clientSecret string + privateKey *rsa.PrivateKey + publicKey *rsa.PublicKey + testClientSecret []byte ) func init() { @@ -341,7 +328,7 @@ func tokenHandler(w http.ResponseWriter, r *http.Request) { func validateClient(clientID, clientSecret string) bool { validClients := map[string]string{ - clientID: clientSecret, + testClientID: string(testClientSecret), // "test_client": "test_secret", // "demo_client": "demo_secret", } @@ -371,11 +358,7 @@ func generateJWTToken(clientID, scope string) (string, error) { return token.SignedString(privateKey) } -func oidcServer(ch <-chan any, cid, csecret string, accessTTLsecs int) { - tokenExpireSecs = accessTTLsecs - clientID = cid - clientSecret = csecret - +func oidcServer(ch <-chan any) { http.HandleFunc("/token", tokenHandler) s := &http.Server{ Addr: fmt.Sprintf(":%d", PORT), @@ -397,4 +380,8 @@ func oidcServer(ch <-chan any, cid, csecret string, accessTTLsecs int) { if err != nil { log.Fatalf("error shutting down OIDC server: %v", err) } + err = s.Close() + if err != nil { + log.Fatalf("error closing OIDC socket: %v", err) + } } From c7f701856a249da828edbf0dde4a3d12218ee686 Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Mon, 28 Jul 2025 09:43:06 -0600 Subject: [PATCH 10/38] Use dynamic port allocation for mock OIDC server This is so we can quickly stop/start a new server in each test func. Signed-off-by: Rich Scott --- internal/kafka/oidc/oidc_client_test.go | 61 +++++++++++++++---------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/internal/kafka/oidc/oidc_client_test.go b/internal/kafka/oidc/oidc_client_test.go index 2d877a90ec160..51b551e6cbb58 100644 --- a/internal/kafka/oidc/oidc_client_test.go +++ b/internal/kafka/oidc/oidc_client_test.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "log" + "net" "net/http" "os" "path/filepath" @@ -25,7 +26,6 @@ import ( ) const ( - PORT = 3000 testClientID = "mock-client-id" testScope = "mock-scope" tokenExpireSecs = 60 @@ -53,16 +53,18 @@ func TestOIDCProvider_GetToken_Success(t *testing.T) { testClientSecret, err = os.ReadFile(secretFile) assert.NoError(t, err) - oidcServerQuit := make(chan any) + oidcServerQuit := make(chan bool, 1) + portCh := make(chan int, 1) go func() { - oidcServer(oidcServerQuit) + oidcServer(oidcServerQuit, portCh) }() defer func() { oidcServerQuit <- true }() - time.Sleep(50 * time.Millisecond) // wait for OIDC server to fully start - tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", PORT) + // Wait for server to start and get the port + port := <-portCh + tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) oidcProvider := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, []string{testScope}, 0) @@ -94,16 +96,18 @@ func TestOIDCProvider_GetToken_Error(t *testing.T) { testClientSecret, err = os.ReadFile(secretFile) assert.NoError(t, err) - oidcServerQuit := make(chan any) + oidcServerQuit := make(chan bool) + portCh := make(chan int, 1) go func() { - oidcServer(oidcServerQuit) + oidcServer(oidcServerQuit, portCh) }() defer func() { oidcServerQuit <- true }() - time.Sleep(50 * time.Millisecond) // wait for OIDC server to fully start - tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", PORT) + // Wait for server to start and get the port + port := <-portCh + tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) oidcProvider := NewOIDCfileTokenProvider(context.Background(), "wrong-client-id", secretFile, tokenURL, []string{testScope}, 0) @@ -358,30 +362,41 @@ func generateJWTToken(clientID, scope string) (string, error) { return token.SignedString(privateKey) } -func oidcServer(ch <-chan any) { - http.HandleFunc("/token", tokenHandler) +func oidcServer(ch <-chan bool, portCh chan<- int) { + mux := http.NewServeMux() + mux.HandleFunc("/token", tokenHandler) s := &http.Server{ - Addr: fmt.Sprintf(":%d", PORT), + Addr: ":0", // Use port 0 for dynamic allocation + Handler: mux, ReadHeaderTimeout: 10 * time.Second, ReadTimeout: 10 * time.Second, } + listener, err := net.Listen("tcp", s.Addr) + if err != nil { + log.Fatalf("could not create listener: %v", err) + } + + // Get the actual port number and send it back + port := listener.Addr().(*net.TCPAddr).Port + portCh <- port + go func() { - fmt.Printf("OIDC Mock Server starting\n") - err := s.ListenAndServe() - if err != nil { - log.Fatalf("could not start OIDC server: %v", err) + // fmt.Printf("OIDC Mock Server starting on port %d\n", port) + err := s.Serve(listener) + if err != nil && err != http.ErrServerClosed { + log.Printf("OIDC server error: %v", err) } }() <-ch - fmt.Printf("OIDC server shutting down\n") - err := s.Shutdown(context.Background()) - if err != nil { - log.Fatalf("error shutting down OIDC server: %v", err) - } - err = s.Close() + // fmt.Printf("OIDC server shutting down\n") + + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + err = s.Shutdown(shutdownCtx) if err != nil { - log.Fatalf("error closing OIDC socket: %v", err) + log.Printf("error shutting down OIDC server: %v", err) } } From f965e0fe4e7230dcd140b3599c9122b1416bd40c Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Mon, 28 Jul 2025 10:32:20 -0600 Subject: [PATCH 11/38] Add test to verify token-caching. Also give some vars better names; disable some logging messages. Signed-off-by: Rich Scott --- internal/kafka/oidc/oidc_client.go | 2 +- internal/kafka/oidc/oidc_client_test.go | 76 +++++++++++-------------- 2 files changed, 35 insertions(+), 43 deletions(-) diff --git a/internal/kafka/oidc/oidc_client.go b/internal/kafka/oidc/oidc_client.go index a386007970d52..1ed7e910393d2 100644 --- a/internal/kafka/oidc/oidc_client.go +++ b/internal/kafka/oidc/oidc_client.go @@ -67,7 +67,7 @@ func (p *OIDCfileTokenProvider) updateToken() (*oauth2.Token, error) { defer p.mu.Unlock() now := time.Now() - log.Printf("Refreshing token for %s", p.ClientID) + // log.Printf("Refreshing token for %s", p.ClientID) if now.Sub(p.lastRefreshTime) < p.refreshCooldown { // Someone just refreshed - skip log.Printf("Skipping token refresh for %s, within the quiet window of %s", diff --git a/internal/kafka/oidc/oidc_client_test.go b/internal/kafka/oidc/oidc_client_test.go index 51b551e6cbb58..127f1d85dbe28 100644 --- a/internal/kafka/oidc/oidc_client_test.go +++ b/internal/kafka/oidc/oidc_client_test.go @@ -26,9 +26,12 @@ import ( ) const ( - testClientID = "mock-client-id" - testScope = "mock-scope" - tokenExpireSecs = 60 + testClientID = "mock-client-id" + testScope = "mock-scope" +) + +var ( + tokenExpireSecs int = 10 ) type MockOAuthProvider struct { @@ -84,7 +87,7 @@ func TestOIDCProvider_GetToken_Success(t *testing.T) { assert.Equal(t, testScope, claims["scope"]) assert.WithinDuration(t, time.Now(), time.Unix(int64(claims["iat"].(float64)), 0), 2*time.Second) - expectedTimeout := time.Now().Add(tokenExpireSecs * time.Second) + expectedTimeout := time.Now().Add(time.Duration(tokenExpireSecs) * time.Second) actualTimeout := time.Unix(int64(claims["exp"].(float64)), 0) assert.WithinDuration(t, expectedTimeout, actualTimeout, 2*time.Second) } @@ -117,45 +120,37 @@ func TestOIDCProvider_GetToken_Error(t *testing.T) { assert.Nil(t, saramaToken) } -// func TestOIDCProvider_GetToken_Error(t *testing.T) { -// mockProvider := &MockOAuthProvider{ -// // TODO can we remove all requestAuthority ... ? -// MockSignin: func(clientID, scope string, requestedAuthority ...string) (*sarama.AccessToken, error) { -// return nil, errors.New("failed to sign in") -// }, -// } +func TestOIDCProvider_TokenCaching(t *testing.T) { + secretFile, err := k8sSecretFile() + assert.NoError(t, err) -// oidcProvider := NewOIDCFileTokenProvider("mock-client-id", "mock-scopes", mockProvider, 0) + testClientSecret, err = os.ReadFile(secretFile) + assert.NoError(t, err) -// token, err := oidcProvider.Token() -// assert.NotNil(t, err) -// assert.Nil(t, token) -// assert.Equal(t, "failed to refresh token: failed to sign in", err.Error()) -// } + oidcServerQuit := make(chan bool, 1) + portCh := make(chan int, 1) + go func() { + oidcServer(oidcServerQuit, portCh) + }() + defer func() { + oidcServerQuit <- true + }() -// func TestOIDCProvider_TokenCaching(t *testing.T) { -// mockProvider := &MockOAuthProvider{ -// MockSignin: func(clientID, scope string, requestedAuthority ...string) (*AccessToken, error) { -// return &AccessToken{ -// AccessToken: "mock-access-token", -// ExpiresIn: 3600, -// TokenType: "Bearer", -// Scope: "mock-scopes", -// }, nil -// }, -// } + // Wait for server to start and get the port + port := <-portCh + tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) -// oidcProvider := NewGRSigningAccessTokenProvider("mock-client-id", "mock-scopes", mockProvider, 0) + oidcProvider := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, []string{testScope}, 0) -// token1, err1 := oidcProvider.GetToken() -// assert.Nil(t, err1) -// assert.NotNil(t, token1) + token1, err1 := oidcProvider.Token() + assert.Nil(t, err1) + assert.NotNil(t, token1) -// token2, err2 := oidcProvider.GetToken() -// assert.Nil(t, err2) -// assert.NotNil(t, token2) -// assert.Equal(t, token1, token2) -// } + token2, err2 := oidcProvider.Token() + assert.Nil(t, err2) + assert.NotNil(t, token2) + assert.Equal(t, token1, token2) +} // func TestOIDCProvider_TokenExpired(t *testing.T) { // mockProvider := &MockOAuthProvider{ @@ -334,7 +329,6 @@ func validateClient(clientID, clientSecret string) bool { validClients := map[string]string{ testClientID: string(testClientSecret), // "test_client": "test_secret", - // "demo_client": "demo_secret", } expectedSecret, exists := validClients[clientID] @@ -362,7 +356,7 @@ func generateJWTToken(clientID, scope string) (string, error) { return token.SignedString(privateKey) } -func oidcServer(ch <-chan bool, portCh chan<- int) { +func oidcServer(shutdownCh <-chan bool, portCh chan<- int) { mux := http.NewServeMux() mux.HandleFunc("/token", tokenHandler) s := &http.Server{ @@ -382,15 +376,13 @@ func oidcServer(ch <-chan bool, portCh chan<- int) { portCh <- port go func() { - // fmt.Printf("OIDC Mock Server starting on port %d\n", port) err := s.Serve(listener) if err != nil && err != http.ErrServerClosed { log.Printf("OIDC server error: %v", err) } }() - <-ch - // fmt.Printf("OIDC server shutting down\n") + <-shutdownCh shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() From 6b14e441729a31e37736d19db822938219d2913d Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Mon, 28 Jul 2025 14:11:39 -0600 Subject: [PATCH 12/38] Add test for expired token; lint fixes Remove debugging messages. Signed-off-by: Rich Scott --- internal/kafka/oidc/oidc_client.go | 3 - internal/kafka/oidc/oidc_client_test.go | 76 ++++++++++++++----------- 2 files changed, 43 insertions(+), 36 deletions(-) diff --git a/internal/kafka/oidc/oidc_client.go b/internal/kafka/oidc/oidc_client.go index 1ed7e910393d2..e3de783e37700 100644 --- a/internal/kafka/oidc/oidc_client.go +++ b/internal/kafka/oidc/oidc_client.go @@ -67,7 +67,6 @@ func (p *OIDCfileTokenProvider) updateToken() (*oauth2.Token, error) { defer p.mu.Unlock() now := time.Now() - // log.Printf("Refreshing token for %s", p.ClientID) if now.Sub(p.lastRefreshTime) < p.refreshCooldown { // Someone just refreshed - skip log.Printf("Skipping token refresh for %s, within the quiet window of %s", @@ -93,8 +92,6 @@ func (p *OIDCfileTokenProvider) updateToken() (*oauth2.Token, error) { return nil, fmt.Errorf("failed to refresh token: %w", err) } - // log.Printf("CLIENT updateToken(): oauthTok = %s", DumpOauth2Token(oauthTok)) - // oauth2.Token() in golang.org/x/oauth2 v0.30.0 appears not to populate the `ExpiresIn` field(?) from // the server response. The Expiry/`expiry` field is not standard in the OIDC or Oauth2 specs. var expiresIn int64 diff --git a/internal/kafka/oidc/oidc_client_test.go b/internal/kafka/oidc/oidc_client_test.go index 127f1d85dbe28..776951af24e33 100644 --- a/internal/kafka/oidc/oidc_client_test.go +++ b/internal/kafka/oidc/oidc_client_test.go @@ -30,9 +30,7 @@ const ( testScope = "mock-scope" ) -var ( - tokenExpireSecs int = 10 -) +var tokenExpireSecs = 10 type MockOAuthProvider struct { MockSignin func(clientID, scope string, requestedAuthority ...string) (*sarama.AccessToken, error) @@ -77,7 +75,7 @@ func TestOIDCProvider_GetToken_Success(t *testing.T) { assert.NotEmpty(t, saramaToken.Token) parser := jwt.NewParser(jwt.WithoutClaimsValidation()) - tokenObj, err := parser.Parse(saramaToken.Token, func(token *jwt.Token) (any, error) { + tokenObj, err := parser.Parse(saramaToken.Token, func(_ *jwt.Token) (any, error) { return publicKey, nil }) assert.NoError(t, err) @@ -143,41 +141,53 @@ func TestOIDCProvider_TokenCaching(t *testing.T) { oidcProvider := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, []string{testScope}, 0) token1, err1 := oidcProvider.Token() - assert.Nil(t, err1) + assert.NoError(t, err1) assert.NotNil(t, token1) token2, err2 := oidcProvider.Token() - assert.Nil(t, err2) + assert.NoError(t, err2) assert.NotNil(t, token2) assert.Equal(t, token1, token2) } -// func TestOIDCProvider_TokenExpired(t *testing.T) { -// mockProvider := &MockOAuthProvider{ -// MockSignin: func(clientID, scope string, requestedAuthority ...string) (*AccessToken, error) { -// return &AccessToken{ -// AccessToken: "mock-access-token", -// ExpiresIn: 1, -// TokenType: "Bearer", -// Scope: "mock-scopes", -// }, nil -// }, -// } - -// oidcProvider := NewGRSigningAccessTokenProvider("mock-client-id", "mock-scopes", mockProvider, 0) -// oidcProvider.refreshCooldown = 500 * time.Millisecond - -// token1, err1 := oidcProvider.GetToken() -// assert.Nil(t, err1) -// assert.NotNil(t, token1) - -// time.Sleep(2 * time.Second) - -// token2, err2 := oidcProvider.GetToken() -// assert.Nil(t, err2) -// assert.NotNil(t, token2) -// assert.NotEqual(t, token1, token2) -// } +func TestOIDCProvider_TokenExpired(t *testing.T) { + secretFile, err := k8sSecretFile() + assert.NoError(t, err) + + testClientSecret, err = os.ReadFile(secretFile) + assert.NoError(t, err) + + oidcServerQuit := make(chan bool, 1) + portCh := make(chan int, 1) + go func() { + oidcServer(oidcServerQuit, portCh) + }() + defer func() { + oidcServerQuit <- true + }() + + // Wait for server to start and get the port + port := <-portCh + tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) + + originalTokenExpireSecs := tokenExpireSecs + defer func() { + tokenExpireSecs = originalTokenExpireSecs + }() + tokenExpireSecs = 3 + oidcProvider := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, []string{testScope}, 0) + + token1, err1 := oidcProvider.Token() + assert.NoError(t, err1) + assert.NotNil(t, token1) + + time.Sleep(3500 * time.Millisecond) + + token2, err2 := oidcProvider.Token() + assert.NoError(t, err2) + assert.NotNil(t, token2) + assert.NotEqual(t, token1, token2) +} func k8sSecretFile() (string, error) { k8sSAtoken := `eyJhbGciOiJSUzI1NiIsImtpZCI6IjdjdDhhT0pTSXh0Zm0yUVprUVRXaFpTVFpHUlQ0MlFkbDMzQXQ1XzRURkkifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzg0Mzk4Mzk5LCJpYXQiOjE3NTI4NjIzOTksImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiNTE3Zjg4ZDUtOTNjZC00YWIzLWFkZWItY2NiZjExMDdmZGYxIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJ0ZXN0Iiwibm9kZSI6eyJuYW1lIjoia2luZC1jb250cm9sLXBsYW5lIiwidWlkIjoiZThjZTczMWMtMmZjNC00NWZjLWJjNzItMzdhYTgyNDQzN2EwIn0sInBvZCI6eyJuYW1lIjoib2lkYy1zZXJ2ZXIteDU4bm4iLCJ1aWQiOiJmM2Q3YTBkZC04Yzk3LTRkNTgtOGYyYy01ZDRiNThjMmY5NDIifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlZmF1bHQiLCJ1aWQiOiIwZjc1MTJhNi00ZjhkLTQxYjAtOTM4NC1mYWE4YzlmZWUxMWYifSwid2FybmFmdGVyIjoxNzUyODY2MDA2fSwibmJmIjoxNzUyODYyMzk5LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6dGVzdDpkZWZhdWx0In0.A8eDX9Wz6aoAwO-Vrg2ddbxJ5d7r3pdg8J6D4gyHPNQLRmBcZHaWagRKJTZ3gDYvT_u_hCG5RJrHARt9MncftPJ5_gdRyXckbd9a9dcSSRVxFEPzdaUR6GSmTmI2sUwhU33AnWmRqlOlZW_WtslPGXl8tNsfDLfpvabjAuBFJrb7KB8MvzXVNvVcJ8BmM4oglX3e3xIxLBzSSQFkW9OGdmeWFsMh-lNaHpzXQGaZx3W2Wit2SUigbDDSJPCTs_tFMdPv-LW0AH9eRd5yU_j87gEsapu_u5j6qcNku-3g79LcGoIvTqe8QdSI7OeoWVnD05SjfAoyHhR-aoMJtSCOQg` @@ -376,7 +386,7 @@ func oidcServer(shutdownCh <-chan bool, portCh chan<- int) { portCh <- port go func() { - err := s.Serve(listener) + err = s.Serve(listener) if err != nil && err != http.ErrServerClosed { log.Printf("OIDC server error: %v", err) } From f0fed8f20adb32cd16f7da6ae0e2e4f8924a6a04 Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Mon, 28 Jul 2025 14:24:37 -0600 Subject: [PATCH 13/38] Tidy up go.mod and go.sum Signed-off-by: Rich Scott --- internal/kafka/go.mod | 1 - internal/kafka/go.sum | 5 ----- 2 files changed, 6 deletions(-) diff --git a/internal/kafka/go.mod b/internal/kafka/go.mod index 86a07f047a9ae..decfc2d18e6db 100644 --- a/internal/kafka/go.mod +++ b/internal/kafka/go.mod @@ -8,7 +8,6 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.0 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/configkafka v0.130.0 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 - github.com/sanity-io/litter v1.5.8 github.com/stretchr/testify v1.10.0 github.com/twmb/franz-go/pkg/kfake v0.0.0-20250320172111-35ab5e5f5327 github.com/twmb/franz-go/pkg/sasl/kerberos v1.1.0 diff --git a/internal/kafka/go.sum b/internal/kafka/go.sum index 4cadc10007748..e9013bf8c87a5 100644 --- a/internal/kafka/go.sum +++ b/internal/kafka/go.sum @@ -28,7 +28,6 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.33.21 h1:nyLjs8sYJShFYj6aiyjCBI3EcLn github.com/aws/aws-sdk-go-v2/service/sts v1.33.21/go.mod h1:EhdxtZ+g84MSGrSrHzZiUm9PYiZkrADNja15wtRJSJo= github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= -github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -118,7 +117,6 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWu github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -126,12 +124,9 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5X github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg= -github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= From e5a79a5b84f8e1af77eda3207c55068729c46624 Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Tue, 29 Jul 2025 11:02:58 -0600 Subject: [PATCH 14/38] Remove global expireSecs, pass down into handler and generator Signed-off-by: Rich Scott --- internal/kafka/oidc/oidc_client_test.go | 203 ++++++++++++------------ 1 file changed, 99 insertions(+), 104 deletions(-) diff --git a/internal/kafka/oidc/oidc_client_test.go b/internal/kafka/oidc/oidc_client_test.go index 776951af24e33..2ef9abc4e9304 100644 --- a/internal/kafka/oidc/oidc_client_test.go +++ b/internal/kafka/oidc/oidc_client_test.go @@ -30,8 +30,6 @@ const ( testScope = "mock-scope" ) -var tokenExpireSecs = 10 - type MockOAuthProvider struct { MockSignin func(clientID, scope string, requestedAuthority ...string) (*sarama.AccessToken, error) } @@ -57,7 +55,7 @@ func TestOIDCProvider_GetToken_Success(t *testing.T) { oidcServerQuit := make(chan bool, 1) portCh := make(chan int, 1) go func() { - oidcServer(oidcServerQuit, portCh) + oidcServer(oidcServerQuit, portCh, 10) }() defer func() { oidcServerQuit <- true @@ -85,7 +83,7 @@ func TestOIDCProvider_GetToken_Success(t *testing.T) { assert.Equal(t, testScope, claims["scope"]) assert.WithinDuration(t, time.Now(), time.Unix(int64(claims["iat"].(float64)), 0), 2*time.Second) - expectedTimeout := time.Now().Add(time.Duration(tokenExpireSecs) * time.Second) + expectedTimeout := time.Now().Add(time.Duration(10) * time.Second) actualTimeout := time.Unix(int64(claims["exp"].(float64)), 0) assert.WithinDuration(t, expectedTimeout, actualTimeout, 2*time.Second) } @@ -100,7 +98,7 @@ func TestOIDCProvider_GetToken_Error(t *testing.T) { oidcServerQuit := make(chan bool) portCh := make(chan int, 1) go func() { - oidcServer(oidcServerQuit, portCh) + oidcServer(oidcServerQuit, portCh, 10) }() defer func() { oidcServerQuit <- true @@ -128,7 +126,7 @@ func TestOIDCProvider_TokenCaching(t *testing.T) { oidcServerQuit := make(chan bool, 1) portCh := make(chan int, 1) go func() { - oidcServer(oidcServerQuit, portCh) + oidcServer(oidcServerQuit, portCh, 10) }() defer func() { oidcServerQuit <- true @@ -160,7 +158,7 @@ func TestOIDCProvider_TokenExpired(t *testing.T) { oidcServerQuit := make(chan bool, 1) portCh := make(chan int, 1) go func() { - oidcServer(oidcServerQuit, portCh) + oidcServer(oidcServerQuit, portCh, 3) }() defer func() { oidcServerQuit <- true @@ -170,11 +168,6 @@ func TestOIDCProvider_TokenExpired(t *testing.T) { port := <-portCh tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) - originalTokenExpireSecs := tokenExpireSecs - defer func() { - tokenExpireSecs = originalTokenExpireSecs - }() - tokenExpireSecs = 3 oidcProvider := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, []string{testScope}, 0) token1, err1 := oidcProvider.Token() @@ -230,108 +223,110 @@ func init() { publicKey = &privateKey.PublicKey } -func tokenHandler(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusMethodNotAllowed) - err := json.NewEncoder(w).Encode(ErrorResponse{ - Error: "invalid_request", - ErrorDescription: "Method not allowed", - }) - if err != nil { - log.Printf("could not encode error response: %v", err) +func NewTokenHandler(expireSecs int) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusMethodNotAllowed) + err := json.NewEncoder(w).Encode(ErrorResponse{ + Error: "invalid_request", + ErrorDescription: "Method not allowed", + }) + if err != nil { + log.Printf("could not encode error response: %v", err) + } + return } - return - } - if err := r.ParseForm(); err != nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - err := json.NewEncoder(w).Encode(ErrorResponse{ - Error: "invalid_request", - ErrorDescription: "Failed to parse form data", - }) - if err != nil { - log.Printf("could not encode error response: %v", err) + if err := r.ParseForm(); err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + err := json.NewEncoder(w).Encode(ErrorResponse{ + Error: "invalid_request", + ErrorDescription: "Failed to parse form data", + }) + if err != nil { + log.Printf("could not encode error response: %v", err) + } + return } - return - } - - grantType := r.FormValue("grant_type") - submittedClientID := r.FormValue("client_id") - submittedClientSecret := r.FormValue("client_secret") - scope := r.FormValue("scope") - if grantType != "client_credentials" { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - err := json.NewEncoder(w).Encode(ErrorResponse{ - Error: "unsupported_grant_type", - ErrorDescription: "Only client_credentials grant type is supported", - }) - if err != nil { - log.Printf("could not encode error response: %v", err) + grantType := r.FormValue("grant_type") + submittedClientID := r.FormValue("client_id") + submittedClientSecret := r.FormValue("client_secret") + scope := r.FormValue("scope") + + if grantType != "client_credentials" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + err := json.NewEncoder(w).Encode(ErrorResponse{ + Error: "unsupported_grant_type", + ErrorDescription: "Only client_credentials grant type is supported", + }) + if err != nil { + log.Printf("could not encode error response: %v", err) + } + return } - return - } - if submittedClientID == "" || submittedClientSecret == "" { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - err := json.NewEncoder(w).Encode(ErrorResponse{ - Error: "invalid_client", - ErrorDescription: "Client ID and secret are required", - }) - if err != nil { - log.Printf("could not encode error response: %v", err) + if submittedClientID == "" || submittedClientSecret == "" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + err := json.NewEncoder(w).Encode(ErrorResponse{ + Error: "invalid_client", + ErrorDescription: "Client ID and secret are required", + }) + if err != nil { + log.Printf("could not encode error response: %v", err) + } + return } - return - } - if !validateClient(submittedClientID, submittedClientSecret) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusUnauthorized) - err := json.NewEncoder(w).Encode(ErrorResponse{ - Error: "invalid_client", - ErrorDescription: "Invalid client credentials", - }) - if err != nil { - log.Printf("could not encode error response: %v", err) + if !validateClient(submittedClientID, submittedClientSecret) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + err := json.NewEncoder(w).Encode(ErrorResponse{ + Error: "invalid_client", + ErrorDescription: "Invalid client credentials", + }) + if err != nil { + log.Printf("could not encode error response: %v", err) + } + return } - return - } - token, err := generateJWTToken(submittedClientID, scope) - if err != nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusInternalServerError) - encodeErr := json.NewEncoder(w).Encode(ErrorResponse{ - Error: "server_error", - ErrorDescription: "Failed to generate token", - }) - if encodeErr != nil { - log.Printf("could not encode error response: %v", encodeErr) + token, err := generateJWTToken(submittedClientID, scope, expireSecs) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + encodeErr := json.NewEncoder(w).Encode(ErrorResponse{ + Error: "server_error", + ErrorDescription: "Failed to generate token", + }) + if encodeErr != nil { + log.Printf("could not encode error response: %v", encodeErr) + } + return } - return - } - // log.Printf("tokenHandler(): tokenExpireSecs = %v", tokenExpireSecs) + // log.Printf("tokenHandler(): expireSecs = %v", expireSecs) - response := oauth2.Token{ - AccessToken: token, - TokenType: "Bearer", - // Note that `expiry` is not an official field in the OIDC or OAuth2 specs - Expiry: time.Now().Add(time.Duration(tokenExpireSecs) * time.Second), - ExpiresIn: int64(tokenExpireSecs), - } + response := oauth2.Token{ + AccessToken: token, + TokenType: "Bearer", + // Note that `expiry` is not an official field in the OIDC or OAuth2 specs + Expiry: time.Now().Add(time.Duration(expireSecs) * time.Second), + ExpiresIn: int64(expireSecs), + } - // log.Printf("SERVER tokenHandler(): response token = %s\n", DumpOauth2Token(&response)) + // log.Printf("SERVER tokenHandler(): response token = %s\n", DumpOauth2Token(&response)) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - err = json.NewEncoder(w).Encode(response) - if err != nil { - log.Printf("could not encode response: %v", err) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(response) + if err != nil { + log.Printf("could not encode response: %v", err) + } } } @@ -345,14 +340,14 @@ func validateClient(clientID, clientSecret string) bool { return exists && expectedSecret == clientSecret } -func generateJWTToken(clientID, scope string) (string, error) { +func generateJWTToken(clientID, scope string, expireSecs int) (string, error) { now := time.Now() claims := jwt.MapClaims{ "iss": "oidc-mock-server", "sub": clientID, "aud": "api", - "exp": now.Add(time.Duration(tokenExpireSecs) * time.Second).Unix(), + "exp": now.Add(time.Duration(expireSecs) * time.Second).Unix(), "iat": now.Unix(), "client_id": clientID, } @@ -366,9 +361,9 @@ func generateJWTToken(clientID, scope string) (string, error) { return token.SignedString(privateKey) } -func oidcServer(shutdownCh <-chan bool, portCh chan<- int) { +func oidcServer(shutdownCh <-chan bool, portCh chan<- int, expireSecs int) { mux := http.NewServeMux() - mux.HandleFunc("/token", tokenHandler) + mux.HandleFunc("/token", NewTokenHandler(expireSecs)) s := &http.Server{ Addr: ":0", // Use port 0 for dynamic allocation Handler: mux, @@ -387,7 +382,7 @@ func oidcServer(shutdownCh <-chan bool, portCh chan<- int) { go func() { err = s.Serve(listener) - if err != nil && err != http.ErrServerClosed { + if err != nil && !errors.Is(err, http.ErrServerClosed) { log.Printf("OIDC server error: %v", err) } }() From a1fc704caab769b1683b085f5eab382e7dd1805e Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Tue, 29 Jul 2025 14:38:53 -0600 Subject: [PATCH 15/38] Start background refresher when OIDC provider is created Also, manually build a (small subset of a) K8S token, for unit-testing. Signed-off-by: Rich Scott --- internal/kafka/go.mod | 1 + internal/kafka/oidc/oidc_client.go | 8 +++++++- internal/kafka/oidc/oidc_client_test.go | 19 +++++++++++++++++-- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/internal/kafka/go.mod b/internal/kafka/go.mod index decfc2d18e6db..0cbcf85dcf88d 100644 --- a/internal/kafka/go.mod +++ b/internal/kafka/go.mod @@ -6,6 +6,7 @@ require ( github.com/IBM/sarama v1.45.2 github.com/aws/aws-msk-iam-sasl-signer-go v1.0.4 github.com/golang-jwt/jwt/v5 v5.2.0 + github.com/google/uuid v1.6.0 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/configkafka v0.130.0 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 github.com/stretchr/testify v1.10.0 diff --git a/internal/kafka/oidc/oidc_client.go b/internal/kafka/oidc/oidc_client.go index e3de783e37700..5bdcb2cd36756 100644 --- a/internal/kafka/oidc/oidc_client.go +++ b/internal/kafka/oidc/oidc_client.go @@ -44,13 +44,19 @@ type OIDCfileTokenProvider struct { func NewOIDCfileTokenProvider(ctx context.Context, clientID, clientSecretFilePath, tokenURL string, scopes []string, refreshAhead time.Duration, ) sarama.AccessTokenProvider { - return &OIDCfileTokenProvider{ + prov := &OIDCfileTokenProvider{ Ctx: ctx, ClientID: clientID, ClientSecretFilePath: clientSecretFilePath, TokenURL: tokenURL, Scopes: scopes, } + + if refreshAhead.Milliseconds() > 0 { + prov.startBackgroundRefresher() + } + + return prov } func (p *OIDCfileTokenProvider) Token() (*sarama.AccessToken, error) { diff --git a/internal/kafka/oidc/oidc_client_test.go b/internal/kafka/oidc/oidc_client_test.go index 2ef9abc4e9304..210ee55a614c2 100644 --- a/internal/kafka/oidc/oidc_client_test.go +++ b/internal/kafka/oidc/oidc_client_test.go @@ -20,6 +20,7 @@ import ( "github.com/IBM/sarama" "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/oauth2" @@ -183,10 +184,24 @@ func TestOIDCProvider_TokenExpired(t *testing.T) { } func k8sSecretFile() (string, error) { - k8sSAtoken := `eyJhbGciOiJSUzI1NiIsImtpZCI6IjdjdDhhT0pTSXh0Zm0yUVprUVRXaFpTVFpHUlQ0MlFkbDMzQXQ1XzRURkkifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzg0Mzk4Mzk5LCJpYXQiOjE3NTI4NjIzOTksImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiNTE3Zjg4ZDUtOTNjZC00YWIzLWFkZWItY2NiZjExMDdmZGYxIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJ0ZXN0Iiwibm9kZSI6eyJuYW1lIjoia2luZC1jb250cm9sLXBsYW5lIiwidWlkIjoiZThjZTczMWMtMmZjNC00NWZjLWJjNzItMzdhYTgyNDQzN2EwIn0sInBvZCI6eyJuYW1lIjoib2lkYy1zZXJ2ZXIteDU4bm4iLCJ1aWQiOiJmM2Q3YTBkZC04Yzk3LTRkNTgtOGYyYy01ZDRiNThjMmY5NDIifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlZmF1bHQiLCJ1aWQiOiIwZjc1MTJhNi00ZjhkLTQxYjAtOTM4NC1mYWE4YzlmZWUxMWYifSwid2FybmFmdGVyIjoxNzUyODY2MDA2fSwibmJmIjoxNzUyODYyMzk5LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6dGVzdDpkZWZhdWx0In0.A8eDX9Wz6aoAwO-Vrg2ddbxJ5d7r3pdg8J6D4gyHPNQLRmBcZHaWagRKJTZ3gDYvT_u_hCG5RJrHARt9MncftPJ5_gdRyXckbd9a9dcSSRVxFEPzdaUR6GSmTmI2sUwhU33AnWmRqlOlZW_WtslPGXl8tNsfDLfpvabjAuBFJrb7KB8MvzXVNvVcJ8BmM4oglX3e3xIxLBzSSQFkW9OGdmeWFsMh-lNaHpzXQGaZx3W2Wit2SUigbDDSJPCTs_tFMdPv-LW0AH9eRd5yU_j87gEsapu_u5j6qcNku-3g79LcGoIvTqe8QdSI7OeoWVnD05SjfAoyHhR-aoMJtSCOQg` + // Mock a small subset of a Kubernetes Service Account token + claims := jwt.MapClaims{ + "iss": "https://kubernetes.default.svc.cluster.local", + "sub": "system:serviceaccount:test:default", + "aud": []string{"https://kubernetes.default.svc.cluster.local"}, + "exp": time.Now().Add(time.Duration(120) * time.Second).Unix(), + "iat": time.Now().Unix(), + "jti": uuid.NewString(), + } + + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + k8sSAtoken, err := token.SignedString(privateKey) + if err != nil { + return "", fmt.Errorf("error creating mock K8S service account token: %w", err) + } tokenPath := filepath.Join(os.TempDir(), "k8sToken") - err := os.WriteFile(tokenPath, []byte(k8sSAtoken), 0o600) + err = os.WriteFile(tokenPath, []byte(k8sSAtoken), 0o600) if err != nil { return "", fmt.Errorf("error writing %s: %w", tokenPath, err) } From 462d4784351358d75760db1fb2ba784570b524fb Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Wed, 30 Jul 2025 17:49:04 -0600 Subject: [PATCH 16/38] Use a var for token expire; add refreshAhead test Signed-off-by: Rich Scott --- internal/kafka/oidc/oidc_client.go | 1 + internal/kafka/oidc/oidc_client_test.go | 57 ++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/internal/kafka/oidc/oidc_client.go b/internal/kafka/oidc/oidc_client.go index 5bdcb2cd36756..7dbad8bd17fb8 100644 --- a/internal/kafka/oidc/oidc_client.go +++ b/internal/kafka/oidc/oidc_client.go @@ -50,6 +50,7 @@ func NewOIDCfileTokenProvider(ctx context.Context, clientID, clientSecretFilePat ClientSecretFilePath: clientSecretFilePath, TokenURL: tokenURL, Scopes: scopes, + refreshAhead: refreshAhead, } if refreshAhead.Milliseconds() > 0 { diff --git a/internal/kafka/oidc/oidc_client_test.go b/internal/kafka/oidc/oidc_client_test.go index 210ee55a614c2..dbd6212d75c3f 100644 --- a/internal/kafka/oidc/oidc_client_test.go +++ b/internal/kafka/oidc/oidc_client_test.go @@ -53,10 +53,12 @@ func TestOIDCProvider_GetToken_Success(t *testing.T) { testClientSecret, err = os.ReadFile(secretFile) assert.NoError(t, err) + tokenTTLsecs := 10 + oidcServerQuit := make(chan bool, 1) portCh := make(chan int, 1) go func() { - oidcServer(oidcServerQuit, portCh, 10) + oidcServer(oidcServerQuit, portCh, tokenTTLsecs) }() defer func() { oidcServerQuit <- true @@ -84,7 +86,7 @@ func TestOIDCProvider_GetToken_Success(t *testing.T) { assert.Equal(t, testScope, claims["scope"]) assert.WithinDuration(t, time.Now(), time.Unix(int64(claims["iat"].(float64)), 0), 2*time.Second) - expectedTimeout := time.Now().Add(time.Duration(10) * time.Second) + expectedTimeout := time.Now().Add(time.Duration(tokenTTLsecs) * time.Second) actualTimeout := time.Unix(int64(claims["exp"].(float64)), 0) assert.WithinDuration(t, expectedTimeout, actualTimeout, 2*time.Second) } @@ -96,10 +98,12 @@ func TestOIDCProvider_GetToken_Error(t *testing.T) { testClientSecret, err = os.ReadFile(secretFile) assert.NoError(t, err) + tokenTTLsecs := 10 + oidcServerQuit := make(chan bool) portCh := make(chan int, 1) go func() { - oidcServer(oidcServerQuit, portCh, 10) + oidcServer(oidcServerQuit, portCh, tokenTTLsecs) }() defer func() { oidcServerQuit <- true @@ -124,10 +128,12 @@ func TestOIDCProvider_TokenCaching(t *testing.T) { testClientSecret, err = os.ReadFile(secretFile) assert.NoError(t, err) + tokenTTLsecs := 10 + oidcServerQuit := make(chan bool, 1) portCh := make(chan int, 1) go func() { - oidcServer(oidcServerQuit, portCh, 10) + oidcServer(oidcServerQuit, portCh, tokenTTLsecs) }() defer func() { oidcServerQuit <- true @@ -156,10 +162,12 @@ func TestOIDCProvider_TokenExpired(t *testing.T) { testClientSecret, err = os.ReadFile(secretFile) assert.NoError(t, err) + tokenTTLsecs := 3 + oidcServerQuit := make(chan bool, 1) portCh := make(chan int, 1) go func() { - oidcServer(oidcServerQuit, portCh, 3) + oidcServer(oidcServerQuit, portCh, tokenTTLsecs) }() defer func() { oidcServerQuit <- true @@ -175,7 +183,44 @@ func TestOIDCProvider_TokenExpired(t *testing.T) { assert.NoError(t, err1) assert.NotNil(t, token1) - time.Sleep(3500 * time.Millisecond) + time.Sleep(time.Duration((tokenTTLsecs*1000)+500) * time.Millisecond) + + token2, err2 := oidcProvider.Token() + assert.NoError(t, err2) + assert.NotNil(t, token2) + assert.NotEqual(t, token1, token2) +} + +func TestOIDCProvider_RefreshAhead(t *testing.T) { + secretFile, err := k8sSecretFile() + assert.NoError(t, err) + + testClientSecret, err = os.ReadFile(secretFile) + assert.NoError(t, err) + + tokenTTLsecs := 5 + + oidcServerQuit := make(chan bool, 1) + portCh := make(chan int, 1) + go func() { + oidcServer(oidcServerQuit, portCh, tokenTTLsecs) + }() + defer func() { + oidcServerQuit <- true + }() + + // Wait for server to start and get the port + port := <-portCh + tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) + + oidcProvider := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, + []string{testScope}, 2*time.Second) + + token1, err1 := oidcProvider.Token() + assert.NoError(t, err1) + assert.NotNil(t, token1) + + time.Sleep(time.Duration((tokenTTLsecs*1000)+500) * time.Millisecond) token2, err2 := oidcProvider.Token() assert.NoError(t, err2) From ed1a135b9d3f372efdf0c633f85cf13e0f5586f4 Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Fri, 1 Aug 2025 16:27:08 -0600 Subject: [PATCH 17/38] Add more assertions for refreshAhead test. Also accept and propagate EndpointParams and AuthStyle fields for oauth2 clientcredentials.Config. Signed-off-by: Rich Scott --- internal/kafka/oidc/oidc_client.go | 15 ++++++---- internal/kafka/oidc/oidc_client_test.go | 40 +++++++++++++++++++++---- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/internal/kafka/oidc/oidc_client.go b/internal/kafka/oidc/oidc_client.go index 7dbad8bd17fb8..9960aa1d0417c 100644 --- a/internal/kafka/oidc/oidc_client.go +++ b/internal/kafka/oidc/oidc_client.go @@ -36,13 +36,12 @@ type OIDCfileTokenProvider struct { refreshAhead time.Duration refreshCooldown time.Duration - // TODO support the remaining fields of clientcredentials.Config EndpointParams url.Values AuthStyle oauth2.AuthStyle } func NewOIDCfileTokenProvider(ctx context.Context, clientID, clientSecretFilePath, tokenURL string, - scopes []string, refreshAhead time.Duration, + scopes []string, refreshAhead time.Duration, endPointParams url.Values, authStyle oauth2.AuthStyle, ) sarama.AccessTokenProvider { prov := &OIDCfileTokenProvider{ Ctx: ctx, @@ -51,6 +50,8 @@ func NewOIDCfileTokenProvider(ctx context.Context, clientID, clientSecretFilePat TokenURL: tokenURL, Scopes: scopes, refreshAhead: refreshAhead, + EndpointParams: endPointParams, + AuthStyle: authStyle, } if refreshAhead.Milliseconds() > 0 { @@ -89,10 +90,12 @@ func (p *OIDCfileTokenProvider) updateToken() (*oauth2.Token, error) { } oauthTok, err := (&clientcredentials.Config{ - ClientID: p.ClientID, - ClientSecret: string(clientSecret), - TokenURL: p.TokenURL, - Scopes: p.Scopes, + ClientID: p.ClientID, + ClientSecret: string(clientSecret), + TokenURL: p.TokenURL, + Scopes: p.Scopes, + EndpointParams: p.EndpointParams, + AuthStyle: p.AuthStyle, }).Token(p.Ctx) if err != nil || oauthTok == nil || oauthTok.AccessToken == "" { diff --git a/internal/kafka/oidc/oidc_client_test.go b/internal/kafka/oidc/oidc_client_test.go index dbd6212d75c3f..310cf8a0c1b42 100644 --- a/internal/kafka/oidc/oidc_client_test.go +++ b/internal/kafka/oidc/oidc_client_test.go @@ -13,6 +13,7 @@ import ( "log" "net" "net/http" + "net/url" "os" "path/filepath" "testing" @@ -68,7 +69,8 @@ func TestOIDCProvider_GetToken_Success(t *testing.T) { port := <-portCh tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) - oidcProvider := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, []string{testScope}, 0) + oidcProvider := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, + []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) saramaToken, err := oidcProvider.Token() require.NoError(t, err) @@ -114,7 +116,7 @@ func TestOIDCProvider_GetToken_Error(t *testing.T) { tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) oidcProvider := NewOIDCfileTokenProvider(context.Background(), "wrong-client-id", secretFile, - tokenURL, []string{testScope}, 0) + tokenURL, []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) saramaToken, err := oidcProvider.Token() require.Error(t, err) @@ -143,7 +145,7 @@ func TestOIDCProvider_TokenCaching(t *testing.T) { port := <-portCh tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) - oidcProvider := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, []string{testScope}, 0) + oidcProvider := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) token1, err1 := oidcProvider.Token() assert.NoError(t, err1) @@ -177,7 +179,7 @@ func TestOIDCProvider_TokenExpired(t *testing.T) { port := <-portCh tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) - oidcProvider := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, []string{testScope}, 0) + oidcProvider := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) token1, err1 := oidcProvider.Token() assert.NoError(t, err1) @@ -214,7 +216,9 @@ func TestOIDCProvider_RefreshAhead(t *testing.T) { tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) oidcProvider := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, - []string{testScope}, 2*time.Second) + []string{testScope}, 2*time.Second, url.Values{}, oauth2.AuthStyleAutoDetect) + + parser := jwt.NewParser(jwt.WithoutClaimsValidation()) token1, err1 := oidcProvider.Token() assert.NoError(t, err1) @@ -226,6 +230,32 @@ func TestOIDCProvider_RefreshAhead(t *testing.T) { assert.NoError(t, err2) assert.NotNil(t, token2) assert.NotEqual(t, token1, token2) + + // Verify second token is different and issued after the first + token1obj, err := parser.Parse(token1.Token, func(_ *jwt.Token) (any, error) { + return publicKey, nil + }) + assert.NoError(t, err) + assert.NotNil(t, token1obj) + claims1 := token1obj.Claims.(jwt.MapClaims) + assert.Equal(t, testClientID, claims1["client_id"]) + assert.Equal(t, testScope, claims1["scope"]) + tok1IssuedAt := time.Unix(int64(claims1["iat"].(float64)), 0) + tok1ExpAt := time.Unix(int64(claims1["exp"].(float64)), 0) + + token2obj, err := parser.Parse(token2.Token, func(_ *jwt.Token) (any, error) { + return publicKey, nil + }) + assert.NoError(t, err) + assert.NotNil(t, token2obj) + claims2 := token2obj.Claims.(jwt.MapClaims) + assert.Equal(t, testClientID, claims2["client_id"]) + assert.Equal(t, testScope, claims2["scope"]) + tok2IssuedAt := time.Unix(int64(claims2["iat"].(float64)), 0) + tok2ExpAt := time.Unix(int64(claims2["exp"].(float64)), 0) + + assert.True(t, tok2IssuedAt.After(tok1IssuedAt)) + assert.True(t, tok2ExpAt.After(tok1ExpAt)) } func k8sSecretFile() (string, error) { From 8a33c50d24327d0d343e480bb0d0d46f0a4ef1c2 Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Tue, 5 Aug 2025 19:42:37 -0600 Subject: [PATCH 18/38] Add wiring struct and logic for configureSASL() Also add authentication configuration setup test. Signed-off-by: Rich Scott --- internal/kafka/authentication.go | 6 +++++- internal/kafka/authentication_test.go | 30 +++++++++++++++++++++++++++ pkg/kafka/configkafka/config.go | 17 +++++++++++++++ pkg/kafka/configkafka/go.mod | 1 + pkg/kafka/configkafka/go.sum | 2 ++ 5 files changed, 55 insertions(+), 1 deletion(-) diff --git a/internal/kafka/authentication.go b/internal/kafka/authentication.go index d01d546954145..14f522884d9ba 100644 --- a/internal/kafka/authentication.go +++ b/internal/kafka/authentication.go @@ -7,6 +7,7 @@ import ( "context" "crypto/sha256" "crypto/sha512" + "time" "github.com/IBM/sarama" "github.com/aws/aws-msk-iam-sasl-signer-go/signer" @@ -60,7 +61,10 @@ func configureSASL(ctx context.Context, config configkafka.SASLConfig, saramaCon saramaConfig.Net.SASL.TokenProvider = &awsMSKTokenProvider{ctx: ctx, region: config.AWSMSK.Region} case OIDCFILE: saramaConfig.Net.SASL.Mechanism = sarama.SASLTypeOAuth - saramaConfig.Net.SASL.TokenProvider = &oidc.OIDCfileTokenProvider{} + saramaConfig.Net.SASL.TokenProvider = oidc.NewOIDCfileTokenProvider(ctx, saramaConfig.ClientID, + config.OIDC_FILE.ClientSecretFilePath, config.OIDC_FILE.TokenURL, + config.OIDC_FILE.Scopes, time.Duration(config.OIDC_FILE.RefreshAheadSecs)*time.Second, + config.OIDC_FILE.EndPointParams, config.OIDC_FILE.AuthStyle) } } diff --git a/internal/kafka/authentication_test.go b/internal/kafka/authentication_test.go index 707efd4dfb4c2..d8290c7838749 100644 --- a/internal/kafka/authentication_test.go +++ b/internal/kafka/authentication_test.go @@ -5,11 +5,15 @@ package kafka import ( "context" + "net/url" "testing" + "time" "github.com/IBM/sarama" "github.com/stretchr/testify/assert" + "golang.org/x/oauth2" + "github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka/oidc" "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/configkafka" ) @@ -52,6 +56,16 @@ func TestAuthentication(t *testing.T) { region: "region", } + saramaSASLOIDCFILEConfig := &sarama.Config{} + saramaSASLOIDCFILEConfig.Net.SASL.Enable = true + saramaSASLOIDCFILEConfig.Net.SASL.Mechanism = sarama.SASLTypeOAuth + // Specify 0 seconds for the RefreshAhead, as we don't want to have it launch + // the background refresher goroutine - we are just verifying authentication + // configuration setup here. + saramaSASLOIDCFILEConfig.Net.SASL.TokenProvider = oidc.NewOIDCfileTokenProvider( + context.Background(), saramaSASLOIDCFILEConfig.ClientID, "/etc/hosts", "http://127.0.0.1:3000/oidc", + []string{"mock-scope"}, time.Duration(0)*time.Second, url.Values{}, oauth2.AuthStyleAutoDetect) + saramaKerberosCfg := &sarama.Config{} saramaKerberosCfg.Net.SASL.Mechanism = sarama.SASLTypeGSSAPI saramaKerberosCfg.Net.SASL.Enable = true @@ -139,6 +153,22 @@ func TestAuthentication(t *testing.T) { }, saramaConfig: saramaSASLAWSIAMOAUTHConfig, }, + { + auth: configkafka.AuthenticationConfig{ + SASL: &configkafka.SASLConfig{ + Mechanism: "OIDC_FILE", + OIDC_FILE: configkafka.OIDCFileConfig{ + ClientSecretFilePath: "/etc/hosts", + TokenURL: "http://127.0.0.1:3000/oidc", + Scopes: []string{"mock-scope"}, + RefreshAheadSecs: 0, + EndPointParams: url.Values{}, + AuthStyle: oauth2.AuthStyleAutoDetect, + }, + }, + }, + saramaConfig: saramaSASLOIDCFILEConfig, + }, } for _, test := range tests { t.Run("", func(t *testing.T) { diff --git a/pkg/kafka/configkafka/config.go b/pkg/kafka/configkafka/config.go index 17d657de4dc07..57bddfb8b9be4 100644 --- a/pkg/kafka/configkafka/config.go +++ b/pkg/kafka/configkafka/config.go @@ -6,12 +6,14 @@ package configkafka // import "github.com/open-telemetry/opentelemetry-collector import ( "errors" "fmt" + "net/url" "time" "github.com/IBM/sarama" "go.opentelemetry.io/collector/config/configcompression" "go.opentelemetry.io/collector/config/configtls" "go.opentelemetry.io/collector/confmap" + "golang.org/x/oauth2" ) const ( @@ -337,12 +339,16 @@ type SASLConfig struct { Version int `mapstructure:"version"` // AWSMSK holds configuration specific to AWS MSK. AWSMSK AWSMSKConfig `mapstructure:"aws_msk"` + // OIDC_FILE holds configuration for OIDC with file-based secret + OIDC_FILE OIDCFileConfig `mapstructure:"oidc_file"` } func (c SASLConfig) Validate() error { switch c.Mechanism { case "AWS_MSK_IAM_OAUTHBEARER": // TODO validate c.AWSMSK + case "OIDC_FILE": + // TODO valid c.OIDC_FILE case "PLAIN", "SCRAM-SHA-256", "SCRAM-SHA-512": // Do nothing, valid mechanism if c.Username == "" { @@ -370,6 +376,17 @@ type AWSMSKConfig struct { Region string `mapstructure:"region"` } +// OIDCFileConfig defines the additional configuration fields +// for the OIDC_FILE mechanism +type OIDCFileConfig struct { + ClientSecretFilePath string `mapstructure:"client_secret_file_path"` + TokenURL string `mapstructure:"token_url"` + Scopes []string `mapstructure:"scopes"` + RefreshAheadSecs int `mapstructure:"refresh_ahead_secs"` + EndPointParams url.Values `mapstructure:"endpoint_params"` + AuthStyle oauth2.AuthStyle `mapstructure:"auth_style"` +} + // KerberosConfig defines kerberos configuration. type KerberosConfig struct { ServiceName string `mapstructure:"service_name"` diff --git a/pkg/kafka/configkafka/go.mod b/pkg/kafka/configkafka/go.mod index b901ce421aa62..508008db26b87 100644 --- a/pkg/kafka/configkafka/go.mod +++ b/pkg/kafka/configkafka/go.mod @@ -11,6 +11,7 @@ require ( go.opentelemetry.io/collector/confmap v1.37.1-0.20250801020258-8b73477b9810 go.opentelemetry.io/collector/confmap/xconfmap v0.131.1-0.20250801020258-8b73477b9810 go.uber.org/goleak v1.3.0 + golang.org/x/oauth2 v0.30.0 ) require ( diff --git a/pkg/kafka/configkafka/go.sum b/pkg/kafka/configkafka/go.sum index 47c8a7d6055e2..a96e7d691f757 100644 --- a/pkg/kafka/configkafka/go.sum +++ b/pkg/kafka/configkafka/go.sum @@ -178,6 +178,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= From 99ad70e1d1c6399491eae872947b4d4e851ebf74 Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Tue, 5 Aug 2025 20:02:58 -0600 Subject: [PATCH 19/38] Remove old unused MockOauthProvider type and member func --- internal/kafka/oidc/oidc_client_test.go | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/internal/kafka/oidc/oidc_client_test.go b/internal/kafka/oidc/oidc_client_test.go index 310cf8a0c1b42..32baa75c493ab 100644 --- a/internal/kafka/oidc/oidc_client_test.go +++ b/internal/kafka/oidc/oidc_client_test.go @@ -19,7 +19,6 @@ import ( "testing" "time" - "github.com/IBM/sarama" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" "github.com/stretchr/testify/assert" @@ -32,21 +31,6 @@ const ( testScope = "mock-scope" ) -type MockOAuthProvider struct { - MockSignin func(clientID, scope string, requestedAuthority ...string) (*sarama.AccessToken, error) -} - -func (m *MockOAuthProvider) Signin(clientID, scope string, requestedAuthority ...string) (*sarama.AccessToken, error) { - if m.MockSignin != nil { - return m.MockSignin(clientID, scope, requestedAuthority...) - } - return nil, errors.New("MockSignin function not defined") -} - -func (*MockOAuthProvider) Name() string { - return "mock" -} - func TestOIDCProvider_GetToken_Success(t *testing.T) { secretFile, err := k8sSecretFile() assert.NoError(t, err) From 5ae05011a00ff6fda6daee411eee23fa7577e4ce Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Wed, 6 Aug 2025 15:12:53 -0600 Subject: [PATCH 20/38] Fix missing oauth2 module in kafkametricsreceiver package Signed-off-by: Rich Scott --- receiver/kafkametricsreceiver/go.mod | 1 + receiver/kafkametricsreceiver/go.sum | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/receiver/kafkametricsreceiver/go.mod b/receiver/kafkametricsreceiver/go.mod index 4bd57825e8b50..c9584c40d198e 100644 --- a/receiver/kafkametricsreceiver/go.mod +++ b/receiver/kafkametricsreceiver/go.mod @@ -104,6 +104,7 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.38.0 // indirect golang.org/x/net v0.40.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.25.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect diff --git a/receiver/kafkametricsreceiver/go.sum b/receiver/kafkametricsreceiver/go.sum index 650a73f7cbc35..cd5d97023cd78 100644 --- a/receiver/kafkametricsreceiver/go.sum +++ b/receiver/kafkametricsreceiver/go.sum @@ -57,6 +57,8 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -265,6 +267,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= From 1b30400ab24e49364e0ea23985dd02527042f83a Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Thu, 7 Aug 2025 10:26:35 -0600 Subject: [PATCH 21/38] Un-camelcase variable name, per golangci-lint. Also add outh2 mention in go.mod/go.sum for kafkareceiver. Signed-off-by: Rich Scott --- internal/kafka/authentication.go | 6 +++--- internal/kafka/authentication_test.go | 4 ++-- internal/kafka/franz_client.go | 2 +- pkg/kafka/configkafka/config.go | 12 ++++++------ receiver/kafkareceiver/go.mod | 1 + receiver/kafkareceiver/go.sum | 4 ++++ 6 files changed, 17 insertions(+), 12 deletions(-) diff --git a/internal/kafka/authentication.go b/internal/kafka/authentication.go index 14f522884d9ba..7eee9fc2e8ea5 100644 --- a/internal/kafka/authentication.go +++ b/internal/kafka/authentication.go @@ -62,9 +62,9 @@ func configureSASL(ctx context.Context, config configkafka.SASLConfig, saramaCon case OIDCFILE: saramaConfig.Net.SASL.Mechanism = sarama.SASLTypeOAuth saramaConfig.Net.SASL.TokenProvider = oidc.NewOIDCfileTokenProvider(ctx, saramaConfig.ClientID, - config.OIDC_FILE.ClientSecretFilePath, config.OIDC_FILE.TokenURL, - config.OIDC_FILE.Scopes, time.Duration(config.OIDC_FILE.RefreshAheadSecs)*time.Second, - config.OIDC_FILE.EndPointParams, config.OIDC_FILE.AuthStyle) + config.OIDCFILE.ClientSecretFilePath, config.OIDCFILE.TokenURL, + config.OIDCFILE.Scopes, time.Duration(config.OIDCFILE.RefreshAheadSecs)*time.Second, + config.OIDCFILE.EndPointParams, config.OIDCFILE.AuthStyle) } } diff --git a/internal/kafka/authentication_test.go b/internal/kafka/authentication_test.go index d8290c7838749..9a54171b6d1ee 100644 --- a/internal/kafka/authentication_test.go +++ b/internal/kafka/authentication_test.go @@ -156,8 +156,8 @@ func TestAuthentication(t *testing.T) { { auth: configkafka.AuthenticationConfig{ SASL: &configkafka.SASLConfig{ - Mechanism: "OIDC_FILE", - OIDC_FILE: configkafka.OIDCFileConfig{ + Mechanism: "OIDCFILE", + OIDCFILE: configkafka.OIDCFileConfig{ ClientSecretFilePath: "/etc/hosts", TokenURL: "http://127.0.0.1:3000/oidc", Scopes: []string{"mock-scope"}, diff --git a/internal/kafka/franz_client.go b/internal/kafka/franz_client.go index 3bfcfe2c94178..9efc50eae9dac 100644 --- a/internal/kafka/franz_client.go +++ b/internal/kafka/franz_client.go @@ -32,7 +32,7 @@ const ( SCRAMSHA256 = "SCRAM-SHA-256" PLAIN = "PLAIN" AWSMSKIAMOAUTHBEARER = "AWS_MSK_IAM_OAUTHBEARER" //nolint:gosec // These aren't credentials. - OIDCFILE = "OIDC_FILE" + OIDCFILE = "OIDCFILE" ) // NewFranzSyncProducer creates a new Kafka client using the franz-go library. diff --git a/pkg/kafka/configkafka/config.go b/pkg/kafka/configkafka/config.go index 57bddfb8b9be4..61ba006161834 100644 --- a/pkg/kafka/configkafka/config.go +++ b/pkg/kafka/configkafka/config.go @@ -332,23 +332,23 @@ type SASLConfig struct { Username string `mapstructure:"username"` // Password to be used on authentication Password string `mapstructure:"password"` - // SASL Mechanism to be used, possible values are: (PLAIN, AWS_MSK_IAM_OAUTHBEARER, OIDC_FILE, + // SASL Mechanism to be used, possible values are: (PLAIN, AWS_MSK_IAM_OAUTHBEARER, OIDCFILE, // OIDC_STRING, SCRAM-SHA-256 or SCRAM-SHA-512). Mechanism string `mapstructure:"mechanism"` // SASL Protocol Version to be used, possible values are: (0, 1). Defaults to 0. Version int `mapstructure:"version"` // AWSMSK holds configuration specific to AWS MSK. AWSMSK AWSMSKConfig `mapstructure:"aws_msk"` - // OIDC_FILE holds configuration for OIDC with file-based secret - OIDC_FILE OIDCFileConfig `mapstructure:"oidc_file"` + // OIDCFile holds configuration for OIDC with file-based secret + OIDCFILE OIDCFileConfig `mapstructure:"oidc_file"` } func (c SASLConfig) Validate() error { switch c.Mechanism { case "AWS_MSK_IAM_OAUTHBEARER": // TODO validate c.AWSMSK - case "OIDC_FILE": - // TODO valid c.OIDC_FILE + case "OIDCFILE": + // TODO valid c.OIDCFile case "PLAIN", "SCRAM-SHA-256", "SCRAM-SHA-512": // Do nothing, valid mechanism if c.Username == "" { @@ -377,7 +377,7 @@ type AWSMSKConfig struct { } // OIDCFileConfig defines the additional configuration fields -// for the OIDC_FILE mechanism +// for the OIDCFILE mechanism type OIDCFileConfig struct { ClientSecretFilePath string `mapstructure:"client_secret_file_path"` TokenURL string `mapstructure:"token_url"` diff --git a/receiver/kafkareceiver/go.mod b/receiver/kafkareceiver/go.mod index d358d61c2ffd1..7e77aa2180aa3 100644 --- a/receiver/kafkareceiver/go.mod +++ b/receiver/kafkareceiver/go.mod @@ -127,6 +127,7 @@ require ( golang.org/x/crypto v0.38.0 // indirect golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect golang.org/x/net v0.40.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.33.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect google.golang.org/grpc v1.74.2 // indirect diff --git a/receiver/kafkareceiver/go.sum b/receiver/kafkareceiver/go.sum index ac71c33f63ee9..85f06efbc29f5 100644 --- a/receiver/kafkareceiver/go.sum +++ b/receiver/kafkareceiver/go.sum @@ -65,6 +65,8 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -297,6 +299,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= From 16db2ef0c82c2a283f0a6a1e0c1158a9b36c6f16 Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Thu, 7 Aug 2025 12:53:27 -0600 Subject: [PATCH 22/38] Add oauth2 module to Kafka exporter and observer Signed-off-by: Rich Scott --- exporter/kafkaexporter/go.mod | 1 + exporter/kafkaexporter/go.sum | 4 ++++ extension/observer/kafkatopicsobserver/go.mod | 1 + extension/observer/kafkatopicsobserver/go.sum | 4 ++++ 4 files changed, 10 insertions(+) diff --git a/exporter/kafkaexporter/go.mod b/exporter/kafkaexporter/go.mod index 2a2a14a10fa2f..6ede8aa9440b3 100644 --- a/exporter/kafkaexporter/go.mod +++ b/exporter/kafkaexporter/go.mod @@ -126,6 +126,7 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.38.0 // indirect golang.org/x/net v0.40.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.27.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect diff --git a/exporter/kafkaexporter/go.sum b/exporter/kafkaexporter/go.sum index 23220596d003a..0effc8d801411 100644 --- a/exporter/kafkaexporter/go.sum +++ b/exporter/kafkaexporter/go.sum @@ -63,6 +63,8 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -291,6 +293,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/extension/observer/kafkatopicsobserver/go.mod b/extension/observer/kafkatopicsobserver/go.mod index 91ece84f0bf80..7b348bf0e20b8 100644 --- a/extension/observer/kafkatopicsobserver/go.mod +++ b/extension/observer/kafkatopicsobserver/go.mod @@ -93,6 +93,7 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.38.0 // indirect golang.org/x/net v0.40.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.25.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect diff --git a/extension/observer/kafkatopicsobserver/go.sum b/extension/observer/kafkatopicsobserver/go.sum index f42bb14afe46d..0d048405aab76 100644 --- a/extension/observer/kafkatopicsobserver/go.sum +++ b/extension/observer/kafkatopicsobserver/go.sum @@ -57,6 +57,8 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -241,6 +243,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= From a1783051e9a52eeeeb6b0d90cf5e0fdc9102c138 Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Thu, 7 Aug 2025 14:56:39 -0600 Subject: [PATCH 23/38] Use separate err var for server shutdown. We cannot use the same, earlier `err` variable, as it causes a sporadic race condition. Signed-off-by: Rich Scott --- internal/kafka/oidc/oidc_client_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/kafka/oidc/oidc_client_test.go b/internal/kafka/oidc/oidc_client_test.go index 32baa75c493ab..e051b09d641ee 100644 --- a/internal/kafka/oidc/oidc_client_test.go +++ b/internal/kafka/oidc/oidc_client_test.go @@ -455,9 +455,9 @@ func oidcServer(shutdownCh <-chan bool, portCh chan<- int, expireSecs int) { portCh <- port go func() { - err = s.Serve(listener) - if err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Printf("OIDC server error: %v", err) + serveErr := s.Serve(listener) + if serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) { + log.Printf("OIDC server error: %v", serveErr) } }() From 425986ac96c3c89a028b6a25ca7a2efe655db026 Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Fri, 8 Aug 2025 13:23:56 -0600 Subject: [PATCH 24/38] Add cancellation of background token refresher goroutine This avoids leaking the refresher goroutine, notably when running the unit tests. Signed-off-by: Rich Scott --- internal/kafka/authentication.go | 2 +- internal/kafka/authentication_test.go | 2 +- internal/kafka/oidc/oidc_client.go | 23 +++++++++++++++++------ internal/kafka/oidc/oidc_client_test.go | 15 ++++++++++----- 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/internal/kafka/authentication.go b/internal/kafka/authentication.go index 7eee9fc2e8ea5..2e4c1dba4085b 100644 --- a/internal/kafka/authentication.go +++ b/internal/kafka/authentication.go @@ -61,7 +61,7 @@ func configureSASL(ctx context.Context, config configkafka.SASLConfig, saramaCon saramaConfig.Net.SASL.TokenProvider = &awsMSKTokenProvider{ctx: ctx, region: config.AWSMSK.Region} case OIDCFILE: saramaConfig.Net.SASL.Mechanism = sarama.SASLTypeOAuth - saramaConfig.Net.SASL.TokenProvider = oidc.NewOIDCfileTokenProvider(ctx, saramaConfig.ClientID, + saramaConfig.Net.SASL.TokenProvider, _ = oidc.NewOIDCfileTokenProvider(ctx, saramaConfig.ClientID, config.OIDCFILE.ClientSecretFilePath, config.OIDCFILE.TokenURL, config.OIDCFILE.Scopes, time.Duration(config.OIDCFILE.RefreshAheadSecs)*time.Second, config.OIDCFILE.EndPointParams, config.OIDCFILE.AuthStyle) diff --git a/internal/kafka/authentication_test.go b/internal/kafka/authentication_test.go index 9a54171b6d1ee..fb8c76a66fd0b 100644 --- a/internal/kafka/authentication_test.go +++ b/internal/kafka/authentication_test.go @@ -62,7 +62,7 @@ func TestAuthentication(t *testing.T) { // Specify 0 seconds for the RefreshAhead, as we don't want to have it launch // the background refresher goroutine - we are just verifying authentication // configuration setup here. - saramaSASLOIDCFILEConfig.Net.SASL.TokenProvider = oidc.NewOIDCfileTokenProvider( + saramaSASLOIDCFILEConfig.Net.SASL.TokenProvider, _ = oidc.NewOIDCfileTokenProvider( context.Background(), saramaSASLOIDCFILEConfig.ClientID, "/etc/hosts", "http://127.0.0.1:3000/oidc", []string{"mock-scope"}, time.Duration(0)*time.Second, url.Values{}, oauth2.AuthStyleAutoDetect) diff --git a/internal/kafka/oidc/oidc_client.go b/internal/kafka/oidc/oidc_client.go index 9960aa1d0417c..e73c69edac0c3 100644 --- a/internal/kafka/oidc/oidc_client.go +++ b/internal/kafka/oidc/oidc_client.go @@ -42,7 +42,9 @@ type OIDCfileTokenProvider struct { func NewOIDCfileTokenProvider(ctx context.Context, clientID, clientSecretFilePath, tokenURL string, scopes []string, refreshAhead time.Duration, endPointParams url.Values, authStyle oauth2.AuthStyle, -) sarama.AccessTokenProvider { +) (sarama.AccessTokenProvider, context.CancelFunc) { + ctx, cancel := context.WithCancel(ctx) + prov := &OIDCfileTokenProvider{ Ctx: ctx, ClientID: clientID, @@ -58,7 +60,7 @@ func NewOIDCfileTokenProvider(ctx context.Context, clientID, clientSecretFilePat prov.startBackgroundRefresher() } - return prov + return prov, cancel } func (p *OIDCfileTokenProvider) Token() (*sarama.AccessToken, error) { @@ -158,12 +160,21 @@ func (p *OIDCfileTokenProvider) startBackgroundRefresher() { p.mu.RUnlock() if sleepDuration > 0 { - time.Sleep(sleepDuration) - continue + select { + case <-p.Ctx.Done(): + return + case <-time.After(sleepDuration): + continue + } } - if _, err := p.updateToken(); err != nil { - log.Printf("background token refresh failed: %v\n", err) + select { + case <-p.Ctx.Done(): + return + default: + if _, err := p.updateToken(); err != nil { + log.Printf("background token refresh failed: %v\n", err) + } } } }() diff --git a/internal/kafka/oidc/oidc_client_test.go b/internal/kafka/oidc/oidc_client_test.go index e051b09d641ee..fdd8be925cfe1 100644 --- a/internal/kafka/oidc/oidc_client_test.go +++ b/internal/kafka/oidc/oidc_client_test.go @@ -53,8 +53,9 @@ func TestOIDCProvider_GetToken_Success(t *testing.T) { port := <-portCh tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) - oidcProvider := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, + oidcProvider, cancel := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) + defer cancel() saramaToken, err := oidcProvider.Token() require.NoError(t, err) @@ -99,8 +100,9 @@ func TestOIDCProvider_GetToken_Error(t *testing.T) { port := <-portCh tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) - oidcProvider := NewOIDCfileTokenProvider(context.Background(), "wrong-client-id", secretFile, + oidcProvider, cancel := NewOIDCfileTokenProvider(context.Background(), "wrong-client-id", secretFile, tokenURL, []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) + defer cancel() saramaToken, err := oidcProvider.Token() require.Error(t, err) @@ -129,7 +131,8 @@ func TestOIDCProvider_TokenCaching(t *testing.T) { port := <-portCh tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) - oidcProvider := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) + oidcProvider, cancel := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) + defer cancel() token1, err1 := oidcProvider.Token() assert.NoError(t, err1) @@ -163,7 +166,8 @@ func TestOIDCProvider_TokenExpired(t *testing.T) { port := <-portCh tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) - oidcProvider := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) + oidcProvider, cancel := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) + defer cancel() token1, err1 := oidcProvider.Token() assert.NoError(t, err1) @@ -199,8 +203,9 @@ func TestOIDCProvider_RefreshAhead(t *testing.T) { port := <-portCh tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) - oidcProvider := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, + oidcProvider, cancel := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, []string{testScope}, 2*time.Second, url.Values{}, oauth2.AuthStyleAutoDetect) + defer cancel() parser := jwt.NewParser(jwt.WithoutClaimsValidation()) From 9d39786d4a47049938f1d5b80573cd74df2aba3f Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Fri, 8 Aug 2025 14:42:48 -0600 Subject: [PATCH 25/38] Add initial draft of change log notes. Signed-off-by: Rich Scott --- .chloggen/feature_kafka-oidc-auth.yaml | 28 ++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .chloggen/feature_kafka-oidc-auth.yaml diff --git a/.chloggen/feature_kafka-oidc-auth.yaml b/.chloggen/feature_kafka-oidc-auth.yaml new file mode 100644 index 0000000000000..9ae097eee9ec9 --- /dev/null +++ b/.chloggen/feature_kafka-oidc-auth.yaml @@ -0,0 +1,28 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: kafka + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: This change adds support for authentication via OIDC to the Kafka client. It provides an implementation +of the sarama.AccessTokenProvider interface, supporting the 'client_credentials' Grant Type, and a background token refresh. + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [] From b07483cdbbda427232e6f7caf789e5a43830a607 Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Fri, 8 Aug 2025 15:11:30 -0600 Subject: [PATCH 26/38] Finish change log entries. Signed-off-by: Rich Scott --- .chloggen/feature_kafka-oidc-auth.yaml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.chloggen/feature_kafka-oidc-auth.yaml b/.chloggen/feature_kafka-oidc-auth.yaml index 9ae097eee9ec9..74b3db6b897b7 100644 --- a/.chloggen/feature_kafka-oidc-auth.yaml +++ b/.chloggen/feature_kafka-oidc-auth.yaml @@ -7,16 +7,17 @@ change_type: enhancement component: kafka # A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). -note: This change adds support for authentication via OIDC to the Kafka client. It provides an implementation -of the sarama.AccessTokenProvider interface, supporting the 'client_credentials' Grant Type, and a background token refresh. - +note: This change adds support for authentication via OIDC to the Kafka client. + # Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. -issues: [] +issues: [41872] # (Optional) One or more lines of additional information to render under the primary note. # These lines will be padded with 2 spaces and then inserted directly into the document. # Use pipe (|) for multiline entries. -subtext: +subtext: | + It provides an implementation of the sarama.AccessTokenProvider interface, supporting the + client_credentials Grant Type, and a background token refresh. # If your change doesn't affect end users or the exported elements of any package, # you should instead start your pull request title with [chore] or use the "Skip Changelog" label. From 4009f94975d67f8b52b05923c7da4379cce4c26b Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Fri, 8 Aug 2025 15:16:08 -0600 Subject: [PATCH 27/38] Remove debugging messages. Signed-off-by: Rich Scott --- internal/kafka/oidc/oidc_client.go | 12 ------------ internal/kafka/oidc/oidc_client_test.go | 4 ---- 2 files changed, 16 deletions(-) diff --git a/internal/kafka/oidc/oidc_client.go b/internal/kafka/oidc/oidc_client.go index e73c69edac0c3..3f35cc65ff43b 100644 --- a/internal/kafka/oidc/oidc_client.go +++ b/internal/kafka/oidc/oidc_client.go @@ -79,8 +79,6 @@ func (p *OIDCfileTokenProvider) updateToken() (*oauth2.Token, error) { now := time.Now() if now.Sub(p.lastRefreshTime) < p.refreshCooldown { // Someone just refreshed - skip - log.Printf("Skipping token refresh for %s, within the quiet window of %s", - p.ClientID, p.refreshCooldown) return p.cachedToken, nil } @@ -116,8 +114,6 @@ func (p *OIDCfileTokenProvider) updateToken() (*oauth2.Token, error) { p.cachedToken = oauthTok p.tokenExpiry = now.Add(time.Duration(expiresIn) * time.Second) p.lastRefreshTime = now - // log.Printf("Token refreshed for %s, will expire after %s at %s", p.ClientID, - // expiresIn.String(), p.tokenExpiry.String()) return oauthTok, nil } @@ -146,8 +142,6 @@ func (p *OIDCfileTokenProvider) GetToken() (*oauth2.Token, error) { } func (p *OIDCfileTokenProvider) startBackgroundRefresher() { - log.Printf("Will refresh access token for client %s and scope %s, %s before expiry", - p.ClientID, p.Scopes[0], p.refreshAhead.String()) p.backgroundOnce.Do(func() { go func() { for { @@ -180,9 +174,3 @@ func (p *OIDCfileTokenProvider) startBackgroundRefresher() { }() }) } - -func DumpOauth2Token(tok *oauth2.Token) string { - return fmt.Sprintf("\nAccessToken: %s...%s\nTokenType: %s\nRefreshToken: %s\nExpiry: %s\nExpiresIn: %d\n", - tok.AccessToken[0:8], tok.AccessToken[len(tok.AccessToken)-8:], tok.TokenType, - tok.RefreshToken, tok.Expiry.Format(time.RFC3339), tok.ExpiresIn) -} diff --git a/internal/kafka/oidc/oidc_client_test.go b/internal/kafka/oidc/oidc_client_test.go index fdd8be925cfe1..1204a499b4cdf 100644 --- a/internal/kafka/oidc/oidc_client_test.go +++ b/internal/kafka/oidc/oidc_client_test.go @@ -388,8 +388,6 @@ func NewTokenHandler(expireSecs int) func(w http.ResponseWriter, r *http.Request return } - // log.Printf("tokenHandler(): expireSecs = %v", expireSecs) - response := oauth2.Token{ AccessToken: token, TokenType: "Bearer", @@ -398,8 +396,6 @@ func NewTokenHandler(expireSecs int) func(w http.ResponseWriter, r *http.Request ExpiresIn: int64(expireSecs), } - // log.Printf("SERVER tokenHandler(): response token = %s\n", DumpOauth2Token(&response)) - w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) err = json.NewEncoder(w).Encode(response) From 2ab594c477b328bf27697c2b44e8434ee7bd4074 Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Fri, 15 Aug 2025 14:11:10 -0600 Subject: [PATCH 28/38] Fix formatting warning from golangci-lint. Signed-off-by: Rich Scott --- internal/kafka/authentication_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/kafka/authentication_test.go b/internal/kafka/authentication_test.go index b917347f6f2e9..ba82e26f96fff 100644 --- a/internal/kafka/authentication_test.go +++ b/internal/kafka/authentication_test.go @@ -176,14 +176,14 @@ func TestAuthentication(t *testing.T) { // equalizes SCRAMClientGeneratorFunc to do assertion with the same reference. config.Net.SASL.SCRAMClientGeneratorFunc = test.saramaConfig.Net.SASL.SCRAMClientGeneratorFunc - + // For OIDC token provider, we need to compare fields individually since the context // contains non-deterministic channels that will cause the comparison to fail if config.Net.SASL.TokenProvider != nil && test.saramaConfig.Net.SASL.TokenProvider != nil { // Set them to the same reference for comparison config.Net.SASL.TokenProvider = test.saramaConfig.Net.SASL.TokenProvider } - + assert.Equal(t, test.saramaConfig, config) }) } From bd388cecaea4cf871b4badae93fba86cb44d5910 Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Tue, 19 Aug 2025 15:37:40 -0600 Subject: [PATCH 29/38] Set and use refreshCoolDown on OIDC token provider. Also check if our context is done before calling updateToken(). Signed-off-by: Rich Scott --- internal/kafka/oidc/oidc_client.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/internal/kafka/oidc/oidc_client.go b/internal/kafka/oidc/oidc_client.go index 3f35cc65ff43b..8a60ebce57a8e 100644 --- a/internal/kafka/oidc/oidc_client.go +++ b/internal/kafka/oidc/oidc_client.go @@ -52,6 +52,7 @@ func NewOIDCfileTokenProvider(ctx context.Context, clientID, clientSecretFilePat TokenURL: tokenURL, Scopes: scopes, refreshAhead: refreshAhead, + refreshCooldown: 1 * time.Second, EndpointParams: endPointParams, AuthStyle: authStyle, } @@ -86,7 +87,7 @@ func (p *OIDCfileTokenProvider) updateToken() (*oauth2.Token, error) { // as it may have changed in the meantime. clientSecret, err := os.ReadFile(p.ClientSecretFilePath) if err != nil { - return nil, fmt.Errorf("failed to read client secret: %w", err) + return nil, fmt.Errorf("failed to read client secret from %s: %w", p.ClientSecretFilePath, err) } oauthTok, err := (&clientcredentials.Config{ @@ -145,7 +146,7 @@ func (p *OIDCfileTokenProvider) startBackgroundRefresher() { p.backgroundOnce.Do(func() { go func() { for { - sleepDuration := 0 * time.Minute + sleepDuration := p.refreshCooldown p.mu.RLock() if p.cachedToken != nil && p.cachedToken.AccessToken != "" { @@ -166,8 +167,14 @@ func (p *OIDCfileTokenProvider) startBackgroundRefresher() { case <-p.Ctx.Done(): return default: - if _, err := p.updateToken(); err != nil { - log.Printf("background token refresh failed: %v\n", err) + // Check if context is done before trying to update token + select { + case <-p.Ctx.Done(): + return + default: + if _, err := p.updateToken(); err != nil { + log.Printf("background token refresh failed: %v\n", err) + } } } } From bbd3f72bf81114a368d62d204bcc9bba1a4ef341 Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Tue, 19 Aug 2025 16:48:44 -0600 Subject: [PATCH 30/38] Fix go.mod for kafkareceiver Signed-off-by: Rich Scott --- receiver/kafkareceiver/go.mod | 6 +----- receiver/kafkareceiver/go.sum | 9 ++------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/receiver/kafkareceiver/go.mod b/receiver/kafkareceiver/go.mod index df83178a46162..78fe85f8c82a2 100644 --- a/receiver/kafkareceiver/go.mod +++ b/receiver/kafkareceiver/go.mod @@ -126,12 +126,8 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.39.0 // indirect golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect -<<<<<<< HEAD - golang.org/x/net v0.40.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect -======= golang.org/x/net v0.41.0 // indirect ->>>>>>> main + golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.33.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect google.golang.org/grpc v1.75.0 // indirect diff --git a/receiver/kafkareceiver/go.sum b/receiver/kafkareceiver/go.sum index 91bba312b9dde..59b18b5de6fcb 100644 --- a/receiver/kafkareceiver/go.sum +++ b/receiver/kafkareceiver/go.sum @@ -303,15 +303,10 @@ golang.org/x/net v0.0.0-20220725212005-46097bf591d3/go.mod h1:AaygXjzTFtRAg2ttMY golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -<<<<<<< HEAD -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -======= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= ->>>>>>> main +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= From 694ab485da81858538e8c5aee95f329cca57a426 Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Fri, 12 Sep 2025 16:55:59 -0600 Subject: [PATCH 31/38] Rewrite and simplify OIDC token background refresher logic. Signed-off-by: Rich Scott --- internal/kafka/oidc/oidc_client.go | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/internal/kafka/oidc/oidc_client.go b/internal/kafka/oidc/oidc_client.go index 8a60ebce57a8e..19804b8e3e6cc 100644 --- a/internal/kafka/oidc/oidc_client.go +++ b/internal/kafka/oidc/oidc_client.go @@ -146,35 +146,21 @@ func (p *OIDCfileTokenProvider) startBackgroundRefresher() { p.backgroundOnce.Do(func() { go func() { for { - sleepDuration := p.refreshCooldown - p.mu.RLock() - if p.cachedToken != nil && p.cachedToken.AccessToken != "" { - sleepDuration = time.Until(p.tokenExpiry.Add(-p.refreshAhead)) - } - p.mu.RUnlock() + tokenExpiry := p.tokenExpiry - if sleepDuration > 0 { - select { - case <-p.Ctx.Done(): - return - case <-time.After(sleepDuration): - continue - } + sleepDuration := time.Until(tokenExpiry.Add(-p.refreshAhead)) + if sleepDuration <= 0 { + sleepDuration = p.refreshCooldown } + p.mu.RUnlock() select { case <-p.Ctx.Done(): return - default: - // Check if context is done before trying to update token - select { - case <-p.Ctx.Done(): - return - default: - if _, err := p.updateToken(); err != nil { - log.Printf("background token refresh failed: %v\n", err) - } + case <-time.After(sleepDuration): + if _, err := p.updateToken(); err != nil { + log.Printf("Error: background refresher - token refresh failed: %v", err) } } } From 50a4bd7fa4a829d2caaac3af04ab5c55fc7a91a8 Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Mon, 15 Sep 2025 11:28:11 -0600 Subject: [PATCH 32/38] Use t.Context() in kafka OIDC client tests. Signed-off-by: Rich Scott --- internal/kafka/oidc/oidc_client_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/kafka/oidc/oidc_client_test.go b/internal/kafka/oidc/oidc_client_test.go index 1204a499b4cdf..541d5fa7666ab 100644 --- a/internal/kafka/oidc/oidc_client_test.go +++ b/internal/kafka/oidc/oidc_client_test.go @@ -53,7 +53,7 @@ func TestOIDCProvider_GetToken_Success(t *testing.T) { port := <-portCh tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) - oidcProvider, cancel := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, + oidcProvider, cancel := NewOIDCfileTokenProvider(t.Context(), testClientID, secretFile, tokenURL, []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) defer cancel() @@ -100,7 +100,7 @@ func TestOIDCProvider_GetToken_Error(t *testing.T) { port := <-portCh tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) - oidcProvider, cancel := NewOIDCfileTokenProvider(context.Background(), "wrong-client-id", secretFile, + oidcProvider, cancel := NewOIDCfileTokenProvider(t.Context(), "wrong-client-id", secretFile, tokenURL, []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) defer cancel() @@ -131,7 +131,7 @@ func TestOIDCProvider_TokenCaching(t *testing.T) { port := <-portCh tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) - oidcProvider, cancel := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) + oidcProvider, cancel := NewOIDCfileTokenProvider(t.Context(), testClientID, secretFile, tokenURL, []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) defer cancel() token1, err1 := oidcProvider.Token() @@ -166,7 +166,7 @@ func TestOIDCProvider_TokenExpired(t *testing.T) { port := <-portCh tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) - oidcProvider, cancel := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) + oidcProvider, cancel := NewOIDCfileTokenProvider(t.Context(), testClientID, secretFile, tokenURL, []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) defer cancel() token1, err1 := oidcProvider.Token() @@ -203,7 +203,7 @@ func TestOIDCProvider_RefreshAhead(t *testing.T) { port := <-portCh tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) - oidcProvider, cancel := NewOIDCfileTokenProvider(context.Background(), testClientID, secretFile, tokenURL, + oidcProvider, cancel := NewOIDCfileTokenProvider(t.Context(), testClientID, secretFile, tokenURL, []string{testScope}, 2*time.Second, url.Values{}, oauth2.AuthStyleAutoDetect) defer cancel() From f13d0a5f4252857e560b758b8213de905d667131 Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Tue, 14 Oct 2025 11:10:42 -0600 Subject: [PATCH 33/38] Migrate OIDC file-based token auth to use franz client Signed-off-by: Rich Scott --- internal/kafka/authentication.go | 2 +- internal/kafka/authentication_test.go | 2 +- internal/kafka/franz_client.go | 18 ++++++++- internal/kafka/oidc/oidc_client.go | 51 +++++++++++++------------ internal/kafka/oidc/oidc_client_test.go | 44 +++++++++++---------- 5 files changed, 68 insertions(+), 49 deletions(-) diff --git a/internal/kafka/authentication.go b/internal/kafka/authentication.go index 2e4c1dba4085b..ad250e011680f 100644 --- a/internal/kafka/authentication.go +++ b/internal/kafka/authentication.go @@ -61,7 +61,7 @@ func configureSASL(ctx context.Context, config configkafka.SASLConfig, saramaCon saramaConfig.Net.SASL.TokenProvider = &awsMSKTokenProvider{ctx: ctx, region: config.AWSMSK.Region} case OIDCFILE: saramaConfig.Net.SASL.Mechanism = sarama.SASLTypeOAuth - saramaConfig.Net.SASL.TokenProvider, _ = oidc.NewOIDCfileTokenProvider(ctx, saramaConfig.ClientID, + saramaConfig.Net.SASL.TokenProvider, _ = oidc.NewOIDCTokenProvider(ctx, saramaConfig.ClientID, config.OIDCFILE.ClientSecretFilePath, config.OIDCFILE.TokenURL, config.OIDCFILE.Scopes, time.Duration(config.OIDCFILE.RefreshAheadSecs)*time.Second, config.OIDCFILE.EndPointParams, config.OIDCFILE.AuthStyle) diff --git a/internal/kafka/authentication_test.go b/internal/kafka/authentication_test.go index ba82e26f96fff..2ca478bb31d3b 100644 --- a/internal/kafka/authentication_test.go +++ b/internal/kafka/authentication_test.go @@ -61,7 +61,7 @@ func TestAuthentication(t *testing.T) { // Specify 0 seconds for the RefreshAhead, as we don't want to have it launch // the background refresher goroutine - we are just verifying authentication // configuration setup here. - saramaSASLOIDCFILEConfig.Net.SASL.TokenProvider, _ = oidc.NewOIDCfileTokenProvider( + saramaSASLOIDCFILEConfig.Net.SASL.TokenProvider, _ = oidc.NewOIDCTokenProvider( t.Context(), saramaSASLOIDCFILEConfig.ClientID, "/etc/hosts", "http://127.0.0.1:3000/oidc", []string{"mock-scope"}, time.Duration(0)*time.Second, url.Values{}, oauth2.AuthStyleAutoDetect) diff --git a/internal/kafka/franz_client.go b/internal/kafka/franz_client.go index 918d5f0817a11..e9d1c272224fa 100644 --- a/internal/kafka/franz_client.go +++ b/internal/kafka/franz_client.go @@ -30,6 +30,7 @@ import ( "go.opentelemetry.io/collector/config/configcompression" "go.uber.org/zap" + "github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka/oidc" "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/configkafka" ) @@ -260,7 +261,7 @@ func commonOpts(ctx context.Context, clientCfg configkafka.ClientConfig, opts = append(opts, kgo.SASL(auth.AsMechanism())) } if clientCfg.Authentication.SASL != nil { - saslOpt, err := configureKgoSASL(clientCfg.Authentication.SASL) + saslOpt, err := configureKgoSASL(clientCfg.Authentication.SASL, clientCfg.ClientID) if err != nil { return nil, fmt.Errorf("failed to configure SASL: %w", err) } @@ -303,7 +304,7 @@ func commonOpts(ctx context.Context, clientCfg configkafka.ClientConfig, return opts, nil } -func configureKgoSASL(cfg *configkafka.SASLConfig) (kgo.Opt, error) { +func configureKgoSASL(cfg *configkafka.SASLConfig, clientID string) (kgo.Opt, error) { var m sasl.Mechanism switch cfg.Mechanism { case PLAIN: @@ -317,6 +318,19 @@ func configureKgoSASL(cfg *configkafka.SASLConfig) (kgo.Opt, error) { token, _, err := signer.GenerateAuthToken(ctx, cfg.AWSMSK.Region) return oauth.Auth{Token: token}, err }) + case OIDCFILE: + m = oauth.Oauth(func(ctx context.Context) (oauth.Auth, error) { + tokenProvider, cancel := oidc.NewOIDCTokenProvider(ctx, clientID, + cfg.OIDCFILE.ClientSecretFilePath, cfg.OIDCFILE.TokenURL, + cfg.OIDCFILE.Scopes, time.Duration(cfg.OIDCFILE.RefreshAheadSecs)*time.Second, + cfg.OIDCFILE.EndPointParams, cfg.OIDCFILE.AuthStyle) + _ = cancel // Store cancel function for cleanup if needed + token, err := tokenProvider.GetToken() + if err != nil { + return oauth.Auth{}, err + } + return oauth.Auth{Token: token.AccessToken}, nil + }) default: return nil, fmt.Errorf("unsupported SASL mechanism: %s", cfg.Mechanism) } diff --git a/internal/kafka/oidc/oidc_client.go b/internal/kafka/oidc/oidc_client.go index 19804b8e3e6cc..ec4dd4ea4d7fe 100644 --- a/internal/kafka/oidc/oidc_client.go +++ b/internal/kafka/oidc/oidc_client.go @@ -40,30 +40,6 @@ type OIDCfileTokenProvider struct { AuthStyle oauth2.AuthStyle } -func NewOIDCfileTokenProvider(ctx context.Context, clientID, clientSecretFilePath, tokenURL string, - scopes []string, refreshAhead time.Duration, endPointParams url.Values, authStyle oauth2.AuthStyle, -) (sarama.AccessTokenProvider, context.CancelFunc) { - ctx, cancel := context.WithCancel(ctx) - - prov := &OIDCfileTokenProvider{ - Ctx: ctx, - ClientID: clientID, - ClientSecretFilePath: clientSecretFilePath, - TokenURL: tokenURL, - Scopes: scopes, - refreshAhead: refreshAhead, - refreshCooldown: 1 * time.Second, - EndpointParams: endPointParams, - AuthStyle: authStyle, - } - - if refreshAhead.Milliseconds() > 0 { - prov.startBackgroundRefresher() - } - - return prov, cancel -} - func (p *OIDCfileTokenProvider) Token() (*sarama.AccessToken, error) { oauthTok, err := p.GetToken() if err != nil { @@ -167,3 +143,30 @@ func (p *OIDCfileTokenProvider) startBackgroundRefresher() { }() }) } + +// NewOIDCTokenProvider creates a new OIDC token provider for Franz-go clients. +// This provides the same functionality as NewOIDCfileTokenProvider but returns +// a provider that can be used directly with Franz-go's oauth.Oauth mechanism. +func NewOIDCTokenProvider(ctx context.Context, clientID, clientSecretFilePath, tokenURL string, + scopes []string, refreshAhead time.Duration, endPointParams url.Values, authStyle oauth2.AuthStyle, +) (*OIDCfileTokenProvider, context.CancelFunc) { + ctx, cancel := context.WithCancel(ctx) + + prov := &OIDCfileTokenProvider{ + Ctx: ctx, + ClientID: clientID, + ClientSecretFilePath: clientSecretFilePath, + TokenURL: tokenURL, + Scopes: scopes, + refreshAhead: refreshAhead, + refreshCooldown: 1 * time.Second, + EndpointParams: endPointParams, + AuthStyle: authStyle, + } + + if refreshAhead.Milliseconds() > 0 { + prov.startBackgroundRefresher() + } + + return prov, cancel +} diff --git a/internal/kafka/oidc/oidc_client_test.go b/internal/kafka/oidc/oidc_client_test.go index 541d5fa7666ab..206da985be502 100644 --- a/internal/kafka/oidc/oidc_client_test.go +++ b/internal/kafka/oidc/oidc_client_test.go @@ -53,17 +53,17 @@ func TestOIDCProvider_GetToken_Success(t *testing.T) { port := <-portCh tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) - oidcProvider, cancel := NewOIDCfileTokenProvider(t.Context(), testClientID, secretFile, tokenURL, + oidcProvider, cancel := NewOIDCTokenProvider(t.Context(), testClientID, secretFile, tokenURL, []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) defer cancel() - saramaToken, err := oidcProvider.Token() + oauthToken, err := oidcProvider.GetToken() require.NoError(t, err) - assert.NotNil(t, saramaToken) - assert.NotEmpty(t, saramaToken.Token) + assert.NotNil(t, oauthToken) + assert.NotEmpty(t, oauthToken.AccessToken) parser := jwt.NewParser(jwt.WithoutClaimsValidation()) - tokenObj, err := parser.Parse(saramaToken.Token, func(_ *jwt.Token) (any, error) { + tokenObj, err := parser.Parse(oauthToken.AccessToken, func(_ *jwt.Token) (any, error) { return publicKey, nil }) assert.NoError(t, err) @@ -100,13 +100,13 @@ func TestOIDCProvider_GetToken_Error(t *testing.T) { port := <-portCh tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) - oidcProvider, cancel := NewOIDCfileTokenProvider(t.Context(), "wrong-client-id", secretFile, + oidcProvider, cancel := NewOIDCTokenProvider(t.Context(), "wrong-client-id", secretFile, tokenURL, []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) defer cancel() - saramaToken, err := oidcProvider.Token() + oauthToken, err := oidcProvider.GetToken() require.Error(t, err) - assert.Nil(t, saramaToken) + assert.Nil(t, oauthToken) } func TestOIDCProvider_TokenCaching(t *testing.T) { @@ -131,14 +131,15 @@ func TestOIDCProvider_TokenCaching(t *testing.T) { port := <-portCh tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) - oidcProvider, cancel := NewOIDCfileTokenProvider(t.Context(), testClientID, secretFile, tokenURL, []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) + oidcProvider, cancel := NewOIDCTokenProvider(t.Context(), testClientID, secretFile, tokenURL, + []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) defer cancel() - token1, err1 := oidcProvider.Token() + token1, err1 := oidcProvider.GetToken() assert.NoError(t, err1) assert.NotNil(t, token1) - token2, err2 := oidcProvider.Token() + token2, err2 := oidcProvider.GetToken() assert.NoError(t, err2) assert.NotNil(t, token2) assert.Equal(t, token1, token2) @@ -166,16 +167,17 @@ func TestOIDCProvider_TokenExpired(t *testing.T) { port := <-portCh tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) - oidcProvider, cancel := NewOIDCfileTokenProvider(t.Context(), testClientID, secretFile, tokenURL, []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) + oidcProvider, cancel := NewOIDCTokenProvider(t.Context(), testClientID, secretFile, tokenURL, + []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) defer cancel() - token1, err1 := oidcProvider.Token() + token1, err1 := oidcProvider.GetToken() assert.NoError(t, err1) assert.NotNil(t, token1) time.Sleep(time.Duration((tokenTTLsecs*1000)+500) * time.Millisecond) - token2, err2 := oidcProvider.Token() + token2, err2 := oidcProvider.GetToken() assert.NoError(t, err2) assert.NotNil(t, token2) assert.NotEqual(t, token1, token2) @@ -203,25 +205,25 @@ func TestOIDCProvider_RefreshAhead(t *testing.T) { port := <-portCh tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) - oidcProvider, cancel := NewOIDCfileTokenProvider(t.Context(), testClientID, secretFile, tokenURL, + parser := jwt.NewParser(jwt.WithoutClaimsValidation()) + + oidcProvider, cancel := NewOIDCTokenProvider(t.Context(), testClientID, secretFile, tokenURL, []string{testScope}, 2*time.Second, url.Values{}, oauth2.AuthStyleAutoDetect) defer cancel() - parser := jwt.NewParser(jwt.WithoutClaimsValidation()) - - token1, err1 := oidcProvider.Token() + token1, err1 := oidcProvider.GetToken() assert.NoError(t, err1) assert.NotNil(t, token1) time.Sleep(time.Duration((tokenTTLsecs*1000)+500) * time.Millisecond) - token2, err2 := oidcProvider.Token() + token2, err2 := oidcProvider.GetToken() assert.NoError(t, err2) assert.NotNil(t, token2) assert.NotEqual(t, token1, token2) // Verify second token is different and issued after the first - token1obj, err := parser.Parse(token1.Token, func(_ *jwt.Token) (any, error) { + token1obj, err := parser.Parse(token1.AccessToken, func(_ *jwt.Token) (any, error) { return publicKey, nil }) assert.NoError(t, err) @@ -232,7 +234,7 @@ func TestOIDCProvider_RefreshAhead(t *testing.T) { tok1IssuedAt := time.Unix(int64(claims1["iat"].(float64)), 0) tok1ExpAt := time.Unix(int64(claims1["exp"].(float64)), 0) - token2obj, err := parser.Parse(token2.Token, func(_ *jwt.Token) (any, error) { + token2obj, err := parser.Parse(token2.AccessToken, func(_ *jwt.Token) (any, error) { return publicKey, nil }) assert.NoError(t, err) From 4063b859c2c79da9ca9d83057f38c5ff67db0de4 Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Thu, 16 Oct 2025 10:06:55 -0600 Subject: [PATCH 34/38] Rework if/else to switch to fix lint error. Signed-off-by: Rich Scott --- internal/kafka/client.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/kafka/client.go b/internal/kafka/client.go index dffe964d0690e..41e292e7787c5 100644 --- a/internal/kafka/client.go +++ b/internal/kafka/client.go @@ -143,17 +143,18 @@ func newSaramaClientConfig(ctx context.Context, config configkafka.ClientConfig) if tlsConfig == nil { tlsConfig = config.Authentication.TLS } - if tlsConfig != nil { + switch { + case tlsConfig != nil: if tlsConfig, err := tlsConfig.LoadTLSConfig(ctx); err != nil { return nil, err } else if tlsConfig != nil { saramaConfig.Net.TLS.Config = tlsConfig saramaConfig.Net.TLS.Enable = true } - } else if config.Authentication.SASL != nil && config.Authentication.SASL.Mechanism == "AWS_MSK_IAM_OAUTHBEARER" { + case config.Authentication.SASL != nil && config.Authentication.SASL.Mechanism == "AWS_MSK_IAM_OAUTHBEARER": saramaConfig.Net.TLS.Config = &tls.Config{} saramaConfig.Net.TLS.Enable = true - } else if config.Authentication.SASL != nil && config.Authentication.SASL.Mechanism == OIDCFILE { + case config.Authentication.SASL != nil && config.Authentication.SASL.Mechanism == OIDCFILE: saramaConfig.Net.TLS.Enable = true } configureSaramaAuthentication(ctx, config.Authentication, saramaConfig) From 2ede506ad0a329d3f59e8a89b92a70b0a5ffcd5f Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Thu, 16 Oct 2025 14:41:52 -0600 Subject: [PATCH 35/38] Replace logic in internal/kafka/oidc/* with existing oauth2 extension Signed-off-by: Rich Scott --- .../clientcredentialsconfig.go | 34 +++- internal/kafka/authentication.go | 5 +- internal/kafka/authentication_test.go | 3 +- internal/kafka/franz_client.go | 4 +- internal/kafka/go.mod | 31 ++- internal/kafka/go.sum | 57 +++++- internal/kafka/oidc/oidc_client.go | 180 ++++-------------- internal/kafka/oidc/oidc_client_test.go | 13 +- pkg/kafka/configkafka/config.go | 1 + 9 files changed, 164 insertions(+), 164 deletions(-) diff --git a/extension/oauth2clientauthextension/clientcredentialsconfig.go b/extension/oauth2clientauthextension/clientcredentialsconfig.go index 8d40eec74553d..269ac82d9b6c0 100644 --- a/extension/oauth2clientauthextension/clientcredentialsconfig.go +++ b/extension/oauth2clientauthextension/clientcredentialsconfig.go @@ -15,7 +15,7 @@ import ( "golang.org/x/oauth2/clientcredentials" ) -// clientCredentialsConfig is a clientcredentials.Config wrapper to allow +// ClientCredentialsConfig is a clientcredentials.Config wrapper to allow // values read from files in the ClientID and ClientSecret fields. // // Values from files can be retrieved by populating the ClientIDFile or @@ -25,13 +25,21 @@ import ( // // Example - Retrieve secret from file: // -// cfg := clientCredentialsConfig{ -// Config: clientcredentials.Config{ +// cfg := ClientCredentialsConfig{ +// Config: Config{ // ClientID: "clientId", // ... // }, // ClientSecretFile: "/path/to/client/secret", // } +type ClientCredentialsConfig struct { + Config Config + + AuthStyle oauth2.AuthStyle + ExpiryBuffer int +} + +// clientCredentialsConfig is an internal version that embeds clientcredentials.Config type clientCredentialsConfig struct { clientcredentials.Config @@ -91,7 +99,25 @@ func (c *clientCredentialsConfig) createConfig() (*clientcredentials.Config, err }, nil } -func (c *clientCredentialsConfig) TokenSource(ctx context.Context) oauth2.TokenSource { +// TokenSource creates an oauth2.TokenSource from the exported ClientCredentialsConfig +func (c *ClientCredentialsConfig) TokenSource(ctx context.Context) oauth2.TokenSource { + internalConfig := &clientCredentialsConfig{ + Config: clientcredentials.Config{ + ClientID: c.Config.ClientID, + ClientSecret: string(c.Config.ClientSecret), + TokenURL: c.Config.TokenURL, + Scopes: c.Config.Scopes, + EndpointParams: c.Config.EndpointParams, + AuthStyle: c.AuthStyle, + }, + ClientIDFile: c.Config.ClientIDFile, + ClientSecretFile: c.Config.ClientSecretFile, + ExpiryBuffer: time.Duration(c.ExpiryBuffer) * time.Second, + } + return internalConfig.tokenSource(ctx) +} + +func (c *clientCredentialsConfig) tokenSource(ctx context.Context) oauth2.TokenSource { return oauth2.ReuseTokenSourceWithExpiry(nil, clientCredentialsTokenSource{ctx: ctx, config: c}, c.ExpiryBuffer) } diff --git a/internal/kafka/authentication.go b/internal/kafka/authentication.go index ad250e011680f..69f998a2cdd4f 100644 --- a/internal/kafka/authentication.go +++ b/internal/kafka/authentication.go @@ -7,7 +7,6 @@ import ( "context" "crypto/sha256" "crypto/sha512" - "time" "github.com/IBM/sarama" "github.com/aws/aws-msk-iam-sasl-signer-go/signer" @@ -63,8 +62,8 @@ func configureSASL(ctx context.Context, config configkafka.SASLConfig, saramaCon saramaConfig.Net.SASL.Mechanism = sarama.SASLTypeOAuth saramaConfig.Net.SASL.TokenProvider, _ = oidc.NewOIDCTokenProvider(ctx, saramaConfig.ClientID, config.OIDCFILE.ClientSecretFilePath, config.OIDCFILE.TokenURL, - config.OIDCFILE.Scopes, time.Duration(config.OIDCFILE.RefreshAheadSecs)*time.Second, - config.OIDCFILE.EndPointParams, config.OIDCFILE.AuthStyle) + config.OIDCFILE.Scopes, config.OIDCFILE.EndPointParams, + config.OIDCFILE.AuthStyle, config.OIDCFILE.ExpiryBuffer) } } diff --git a/internal/kafka/authentication_test.go b/internal/kafka/authentication_test.go index 2ca478bb31d3b..67eff6ac42c98 100644 --- a/internal/kafka/authentication_test.go +++ b/internal/kafka/authentication_test.go @@ -6,7 +6,6 @@ package kafka import ( "net/url" "testing" - "time" "github.com/IBM/sarama" "github.com/stretchr/testify/assert" @@ -63,7 +62,7 @@ func TestAuthentication(t *testing.T) { // configuration setup here. saramaSASLOIDCFILEConfig.Net.SASL.TokenProvider, _ = oidc.NewOIDCTokenProvider( t.Context(), saramaSASLOIDCFILEConfig.ClientID, "/etc/hosts", "http://127.0.0.1:3000/oidc", - []string{"mock-scope"}, time.Duration(0)*time.Second, url.Values{}, oauth2.AuthStyleAutoDetect) + []string{"mock-scope"}, url.Values{}, oauth2.AuthStyleAutoDetect, 0) saramaKerberosCfg := &sarama.Config{} saramaKerberosCfg.Net.SASL.Mechanism = sarama.SASLTypeGSSAPI diff --git a/internal/kafka/franz_client.go b/internal/kafka/franz_client.go index e9d1c272224fa..549647d43bed6 100644 --- a/internal/kafka/franz_client.go +++ b/internal/kafka/franz_client.go @@ -322,8 +322,8 @@ func configureKgoSASL(cfg *configkafka.SASLConfig, clientID string) (kgo.Opt, er m = oauth.Oauth(func(ctx context.Context) (oauth.Auth, error) { tokenProvider, cancel := oidc.NewOIDCTokenProvider(ctx, clientID, cfg.OIDCFILE.ClientSecretFilePath, cfg.OIDCFILE.TokenURL, - cfg.OIDCFILE.Scopes, time.Duration(cfg.OIDCFILE.RefreshAheadSecs)*time.Second, - cfg.OIDCFILE.EndPointParams, cfg.OIDCFILE.AuthStyle) + cfg.OIDCFILE.Scopes, cfg.OIDCFILE.EndPointParams, + cfg.OIDCFILE.AuthStyle, cfg.OIDCFILE.ExpiryBuffer) _ = cancel // Store cancel function for cleanup if needed token, err := tokenProvider.GetToken() if err != nil { diff --git a/internal/kafka/go.mod b/internal/kafka/go.mod index dc539c02b6215..dbfb98e0e2d0c 100644 --- a/internal/kafka/go.mod +++ b/internal/kafka/go.mod @@ -7,6 +7,7 @@ require ( github.com/aws/aws-msk-iam-sasl-signer-go v1.0.4 github.com/golang-jwt/jwt/v5 v5.2.0 github.com/google/uuid v1.6.0 + github.com/open-telemetry/opentelemetry-collector-contrib/extension/oauth2clientauthextension v0.137.0 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/configkafka v0.137.0 github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 github.com/stretchr/testify v1.11.1 @@ -17,7 +18,32 @@ require ( go.opentelemetry.io/collector/config/configopaque v1.43.1-0.20251013162618-a96eab114ea4 go.opentelemetry.io/collector/config/configtls v1.43.1-0.20251013162618-a96eab114ea4 go.uber.org/goleak v1.3.0 - golang.org/x/oauth2 v0.30.0 + golang.org/x/oauth2 v0.31.0 +) + +require ( + cloud.google.com/go/compute/metadata v0.7.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/collector/component v1.43.1-0.20251013162618-a96eab114ea4 // indirect + go.opentelemetry.io/collector/extension v1.43.1-0.20251013162618-a96eab114ea4 // indirect + go.opentelemetry.io/collector/extension/extensionauth v1.43.1-0.20251013162618-a96eab114ea4 // indirect + go.opentelemetry.io/collector/internal/telemetry v0.137.1-0.20251013162618-a96eab114ea4 // indirect + go.opentelemetry.io/collector/pdata v1.43.1-0.20251013162618-a96eab114ea4 // indirect + go.opentelemetry.io/contrib/bridges/otelzap v0.13.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/log v0.14.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect + google.golang.org/grpc v1.76.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect ) require ( @@ -59,7 +85,6 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/twmb/franz-go v1.19.5 github.com/twmb/franz-go/pkg/kadm v1.16.1 github.com/twmb/franz-go/pkg/kmsg v1.12.0 @@ -78,4 +103,6 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect ) +replace github.com/open-telemetry/opentelemetry-collector-contrib/extension/oauth2clientauthextension => ../../extension/oauth2clientauthextension + replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/configkafka => ../../pkg/kafka/configkafka diff --git a/internal/kafka/go.sum b/internal/kafka/go.sum index 503ffef490bd3..49aec5bee7a54 100644 --- a/internal/kafka/go.sum +++ b/internal/kafka/go.sum @@ -1,3 +1,5 @@ +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= github.com/IBM/sarama v1.46.2 h1:65JJmZpxKUWe/7HEHmc56upTfAvgoxuyu4Ek+TcevDE= github.com/IBM/sarama v1.46.2/go.mod h1:PDOGmVeKmW744c/0d4CZ0MfrzmcIYtpmS5+KIWs1zHQ= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.4 h1:2jAwFwA0Xgcx94dUId+K24yFabsKYDtAhCgyMit6OqE= @@ -46,6 +48,7 @@ github.com/foxboron/swtpm_test v0.0.0-20230726224112-46aaafdf7006 h1:50sW4r0Pcvl github.com/foxboron/swtpm_test v0.0.0-20230726224112-46aaafdf7006/go.mod h1:eIXCMsMYCaqq9m1KSSxXwQG11krpuNPGP3k0uaWrbas= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -58,12 +61,17 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-tpm v0.9.6 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA= github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98= github.com/google/go-tpm-tools v0.4.4/go.mod h1:T8jXkp2s+eltnCDIsXR84/MTcVU9Ja7bh3Mit0pa4AY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= @@ -88,6 +96,8 @@ github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZ github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= @@ -105,8 +115,10 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= @@ -122,6 +134,7 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -148,11 +161,15 @@ github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/collector/component v1.43.1-0.20251013162618-a96eab114ea4 h1:w1VjKgmFktbRwdt5L1C4j6jL60sLhThh9dRRBqArUiA= go.opentelemetry.io/collector/component v1.43.1-0.20251013162618-a96eab114ea4/go.mod h1:LJ8w25mRyV1axguFAwX6NxKzh0sXK4pYVOn3dJvfVuk= +go.opentelemetry.io/collector/component/componenttest v0.137.1-0.20251013162618-a96eab114ea4 h1:GDUlSEmbp4yCEhOym78zazGX/5Cp8/OGfFNtk7M+T1w= +go.opentelemetry.io/collector/component/componenttest v0.137.1-0.20251013162618-a96eab114ea4/go.mod h1:08xR/WnVzcz8dz4TfPidNfQ6GsZ//mp9g6RvXgBMO/Q= go.opentelemetry.io/collector/config/configcompression v1.43.1-0.20251013162618-a96eab114ea4 h1:G+6htJtXnwqkOvoLbWZUDBSIOJw/AW31PilJLoYJKOQ= go.opentelemetry.io/collector/config/configcompression v1.43.1-0.20251013162618-a96eab114ea4/go.mod h1:ZlnKaXFYL3HVMUNWVAo/YOLYoxNZo7h8SrQp3l7GV00= go.opentelemetry.io/collector/config/configopaque v1.43.1-0.20251013162618-a96eab114ea4 h1:HtYVRIZuBE0vwbwqAGWDIdpbUTNmuP0DBlFZMt/MziY= @@ -163,24 +180,42 @@ go.opentelemetry.io/collector/confmap v1.43.1-0.20251013162618-a96eab114ea4 h1:3 go.opentelemetry.io/collector/confmap v1.43.1-0.20251013162618-a96eab114ea4/go.mod h1:N5GZpFCmwD1GynDu3IWaZW5Ycfc/7YxSU0q1/E3vLdg= go.opentelemetry.io/collector/confmap/xconfmap v0.137.1-0.20251013162618-a96eab114ea4 h1:yA2m7hdE+LELLWJ8BQWLV6PWrxqlI6Ni/u72A4Abp/E= go.opentelemetry.io/collector/confmap/xconfmap v0.137.1-0.20251013162618-a96eab114ea4/go.mod h1:psXdQr13pVrCqNPdoER2QZZorvONAR5ZUEHURe4POh4= +go.opentelemetry.io/collector/extension v1.43.1-0.20251013162618-a96eab114ea4 h1:ydWUlclX7E9H3AGvrayXMnplL7DZXcJShKbCp0fweEY= +go.opentelemetry.io/collector/extension v1.43.1-0.20251013162618-a96eab114ea4/go.mod h1:GIzXTwB+7I3TpTx2Zppp/2884BNg1LaWe0zjumgV82Q= +go.opentelemetry.io/collector/extension/extensionauth v1.43.1-0.20251013162618-a96eab114ea4 h1:6BbCvOb/86AnIGn5J1vcNauoZ2cbloU4WscZJMjbkso= +go.opentelemetry.io/collector/extension/extensionauth v1.43.1-0.20251013162618-a96eab114ea4/go.mod h1:6Sh0hqPfPqpg0ErCoNPO/ky2NdfGmUX+G5wekPx7A7U= +go.opentelemetry.io/collector/extension/extensiontest v0.137.1-0.20251013162618-a96eab114ea4 h1:DBMKb2WJVCjqa6hkDgVI2mgnzv/x/mx9JUA35U0k1YA= +go.opentelemetry.io/collector/extension/extensiontest v0.137.1-0.20251013162618-a96eab114ea4/go.mod h1:2wyfqLdE/kVOdVOnWxPBQlEnI2NV0bqGXJFRPSrtqy0= go.opentelemetry.io/collector/featuregate v1.43.1-0.20251013162618-a96eab114ea4 h1:IXK7EGifr3Lic3mnMlkVXFb1HBlkBoYfoy/AYzsevko= go.opentelemetry.io/collector/featuregate v1.43.1-0.20251013162618-a96eab114ea4/go.mod h1:d0tiRzVYrytB6LkcYgz2ESFTv7OktRPQe0QEQcPt1L4= go.opentelemetry.io/collector/internal/telemetry v0.137.1-0.20251013162618-a96eab114ea4 h1:E94fKydbFNkR6Zfhix/alvOftJx6e6srYQ8PIr9xYRw= go.opentelemetry.io/collector/internal/telemetry v0.137.1-0.20251013162618-a96eab114ea4/go.mod h1:ui3HnaeyvIe6tpjUFcL70Ev3aw5UxQnzoBGhuXfbbfs= go.opentelemetry.io/collector/pdata v1.43.1-0.20251013162618-a96eab114ea4 h1:lmaRPm+HIMu/Tz4Ht9/NcGCd7FVOUXBmC3fXDZYN6+E= go.opentelemetry.io/collector/pdata v1.43.1-0.20251013162618-a96eab114ea4/go.mod h1:rhhv1vy8COsKFpXiBtLG8GTRDRjg2DL7JPq4E+xOD5Q= +go.opentelemetry.io/collector/pipeline v1.43.0 h1:IJjdqE5UCQlyVvFUUzlhSWhP4WIwpH6UyJQ9iWXpyww= +go.opentelemetry.io/collector/pipeline v1.43.0/go.mod h1:xUrAqiebzYbrgxyoXSkk6/Y3oi5Sy3im2iCA51LwUAI= go.opentelemetry.io/contrib/bridges/otelzap v0.13.0 h1:aBKdhLVieqvwWe9A79UHI/0vgp2t/s2euY8X59pGRlw= go.opentelemetry.io/contrib/bridges/otelzap v0.13.0/go.mod h1:SYqtxLQE7iINgh6WFuVi2AI70148B8EI35DSk0Wr8m4= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM= go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno= +go.opentelemetry.io/otel/log/logtest v0.14.0 h1:BGTqNeluJDK2uIHAY8lRqxjVAYfqgcaTbVk1n3MWe5A= +go.opentelemetry.io/otel/log/logtest v0.14.0/go.mod h1:IuguGt8XVP4XA4d2oEEDMVDBBCesMg8/tSGWDjuKfoA= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/slim/otlp v1.8.0 h1:afcLwp2XOeCbGrjufT1qWyruFt+6C9g5SOuymrSPUXQ= +go.opentelemetry.io/proto/slim/otlp v1.8.0/go.mod h1:Yaa5fjYm1SMCq0hG0x/87wV1MP9H5xDuG/1+AhvBcsI= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.1.0 h1:Uc+elixz922LHx5colXGi1ORbsW8DTIGM+gg+D9V7HE= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.1.0/go.mod h1:VyU6dTWBWv6h9w/+DYgSZAPMabWbPTFTuxp25sM8+s0= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.1.0 h1:i8YpvWGm/Uq1koL//bnbJ/26eV3OrKWm09+rDYo7keU= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.1.0/go.mod h1:pQ70xHY/ZVxNUBPn+qUWPl8nwai87eWdqL3M37lNi9A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -190,15 +225,22 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -208,13 +250,17 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= +golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -237,8 +283,15 @@ golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8= google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= diff --git a/internal/kafka/oidc/oidc_client.go b/internal/kafka/oidc/oidc_client.go index ec4dd4ea4d7fe..c29d0e52bfec3 100644 --- a/internal/kafka/oidc/oidc_client.go +++ b/internal/kafka/oidc/oidc_client.go @@ -5,43 +5,21 @@ package oidc // import "github.com/open-telemetry/opentelemetry-collector-contri import ( "context" - "errors" - "fmt" - "log" - "net/url" - "os" - "sync" - "time" "github.com/IBM/sarama" "golang.org/x/oauth2" - "golang.org/x/oauth2/clientcredentials" -) - -type OIDCfileTokenProvider struct { - Ctx context.Context - ClientID string - ClientSecretFilePath string - TokenURL string - Scopes []string - - mu sync.RWMutex - backgroundOnce sync.Once - - cachedToken *oauth2.Token - - tokenExpiry time.Time - lastRefreshTime time.Time - refreshAhead time.Duration - refreshCooldown time.Duration + "github.com/open-telemetry/opentelemetry-collector-contrib/extension/oauth2clientauthextension" +) - EndpointParams url.Values - AuthStyle oauth2.AuthStyle +// OIDCTokenProvider wraps oauth2clientauthextension to provide tokens for Kafka SASL/OAUTHBEARER +type OIDCTokenProvider struct { + tokenSource oauth2.TokenSource } -func (p *OIDCfileTokenProvider) Token() (*sarama.AccessToken, error) { - oauthTok, err := p.GetToken() +// Token implements sarama.AccessTokenProvider for Sarama-based Kafka clients +func (p *OIDCTokenProvider) Token() (*sarama.AccessToken, error) { + oauthTok, err := p.tokenSource.Token() if err != nil { return nil, err } @@ -49,124 +27,40 @@ func (p *OIDCfileTokenProvider) Token() (*sarama.AccessToken, error) { return &sarama.AccessToken{Token: oauthTok.AccessToken}, nil } -func (p *OIDCfileTokenProvider) updateToken() (*oauth2.Token, error) { - p.mu.Lock() - defer p.mu.Unlock() - - now := time.Now() - if now.Sub(p.lastRefreshTime) < p.refreshCooldown { - // Someone just refreshed - skip - return p.cachedToken, nil - } - - // Read the client secret every time we get a new token, - // as it may have changed in the meantime. - clientSecret, err := os.ReadFile(p.ClientSecretFilePath) - if err != nil { - return nil, fmt.Errorf("failed to read client secret from %s: %w", p.ClientSecretFilePath, err) - } - - oauthTok, err := (&clientcredentials.Config{ - ClientID: p.ClientID, - ClientSecret: string(clientSecret), - TokenURL: p.TokenURL, - Scopes: p.Scopes, - EndpointParams: p.EndpointParams, - AuthStyle: p.AuthStyle, - }).Token(p.Ctx) - - if err != nil || oauthTok == nil || oauthTok.AccessToken == "" { - return nil, fmt.Errorf("failed to refresh token: %w", err) - } - - // oauth2.Token() in golang.org/x/oauth2 v0.30.0 appears not to populate the `ExpiresIn` field(?) from - // the server response. The Expiry/`expiry` field is not standard in the OIDC or Oauth2 specs. - var expiresIn int64 - if oauthTok.ExpiresIn != 0 { - expiresIn = oauthTok.ExpiresIn - } else { - expiresIn = (oauthTok.Expiry.UnixMilli() - time.Now().UnixMilli()) / 1000 - } - - p.cachedToken = oauthTok - p.tokenExpiry = now.Add(time.Duration(expiresIn) * time.Second) - p.lastRefreshTime = now - - return oauthTok, nil -} - -func (p *OIDCfileTokenProvider) GetToken() (*oauth2.Token, error) { - p.mu.RLock() - token := p.cachedToken - expires := p.tokenExpiry - hasToken := token != nil && token.AccessToken != "" - p.mu.RUnlock() - - if hasToken && expires.After(time.Now()) { - return token, nil - } - - // No valid cached token - do a blocking refresh - newToken, err := p.updateToken() - if err != nil { - return nil, err - } - - if newToken == nil || newToken.AccessToken == "" { - return nil, errors.New("token blank after fetch") - } - return newToken, nil -} - -func (p *OIDCfileTokenProvider) startBackgroundRefresher() { - p.backgroundOnce.Do(func() { - go func() { - for { - p.mu.RLock() - tokenExpiry := p.tokenExpiry - - sleepDuration := time.Until(tokenExpiry.Add(-p.refreshAhead)) - if sleepDuration <= 0 { - sleepDuration = p.refreshCooldown - } - p.mu.RUnlock() - - select { - case <-p.Ctx.Done(): - return - case <-time.After(sleepDuration): - if _, err := p.updateToken(); err != nil { - log.Printf("Error: background refresher - token refresh failed: %v", err) - } - } - } - }() - }) +// GetToken returns the oauth2.Token directly for franz-go based clients +func (p *OIDCTokenProvider) GetToken() (*oauth2.Token, error) { + return p.tokenSource.Token() } -// NewOIDCTokenProvider creates a new OIDC token provider for Franz-go clients. -// This provides the same functionality as NewOIDCfileTokenProvider but returns -// a provider that can be used directly with Franz-go's oauth.Oauth mechanism. -func NewOIDCTokenProvider(ctx context.Context, clientID, clientSecretFilePath, tokenURL string, - scopes []string, refreshAhead time.Duration, endPointParams url.Values, authStyle oauth2.AuthStyle, -) (*OIDCfileTokenProvider, context.CancelFunc) { +// NewOIDCTokenProvider creates a new OIDC token provider using oauth2clientauthextension. +// This provides token functionality for both Sarama and Franz-go Kafka clients. +func NewOIDCTokenProvider( + ctx context.Context, + clientID string, + clientSecretFilePath string, + tokenURL string, + scopes []string, + endpointParams map[string][]string, + authStyle oauth2.AuthStyle, + expiryBuffer int, +) (*OIDCTokenProvider, context.CancelFunc) { ctx, cancel := context.WithCancel(ctx) - prov := &OIDCfileTokenProvider{ - Ctx: ctx, - ClientID: clientID, - ClientSecretFilePath: clientSecretFilePath, - TokenURL: tokenURL, - Scopes: scopes, - refreshAhead: refreshAhead, - refreshCooldown: 1 * time.Second, - EndpointParams: endPointParams, - AuthStyle: authStyle, + config := &oauth2clientauthextension.ClientCredentialsConfig{ + Config: oauth2clientauthextension.Config{ + ClientID: clientID, + ClientSecretFile: clientSecretFilePath, + TokenURL: tokenURL, + Scopes: scopes, + EndpointParams: endpointParams, + }, + AuthStyle: authStyle, + ExpiryBuffer: expiryBuffer, } - if refreshAhead.Milliseconds() > 0 { - prov.startBackgroundRefresher() - } + tokenSource := config.TokenSource(ctx) - return prov, cancel + return &OIDCTokenProvider{ + tokenSource: tokenSource, + }, cancel } diff --git a/internal/kafka/oidc/oidc_client_test.go b/internal/kafka/oidc/oidc_client_test.go index 206da985be502..f9b982ca047ec 100644 --- a/internal/kafka/oidc/oidc_client_test.go +++ b/internal/kafka/oidc/oidc_client_test.go @@ -54,7 +54,7 @@ func TestOIDCProvider_GetToken_Success(t *testing.T) { tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) oidcProvider, cancel := NewOIDCTokenProvider(t.Context(), testClientID, secretFile, tokenURL, - []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) + []string{testScope}, url.Values{}, oauth2.AuthStyleAutoDetect, 0) defer cancel() oauthToken, err := oidcProvider.GetToken() @@ -101,7 +101,7 @@ func TestOIDCProvider_GetToken_Error(t *testing.T) { tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) oidcProvider, cancel := NewOIDCTokenProvider(t.Context(), "wrong-client-id", secretFile, - tokenURL, []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) + tokenURL, []string{testScope}, url.Values{}, oauth2.AuthStyleAutoDetect, 0) defer cancel() oauthToken, err := oidcProvider.GetToken() @@ -132,7 +132,7 @@ func TestOIDCProvider_TokenCaching(t *testing.T) { tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) oidcProvider, cancel := NewOIDCTokenProvider(t.Context(), testClientID, secretFile, tokenURL, - []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) + []string{testScope}, url.Values{}, oauth2.AuthStyleAutoDetect, 0) defer cancel() token1, err1 := oidcProvider.GetToken() @@ -142,7 +142,8 @@ func TestOIDCProvider_TokenCaching(t *testing.T) { token2, err2 := oidcProvider.GetToken() assert.NoError(t, err2) assert.NotNil(t, token2) - assert.Equal(t, token1, token2) + // Check that the same access token is returned (caching is working) + assert.Equal(t, token1.AccessToken, token2.AccessToken) } func TestOIDCProvider_TokenExpired(t *testing.T) { @@ -168,7 +169,7 @@ func TestOIDCProvider_TokenExpired(t *testing.T) { tokenURL := fmt.Sprintf("http://127.0.0.1:%d/token", port) oidcProvider, cancel := NewOIDCTokenProvider(t.Context(), testClientID, secretFile, tokenURL, - []string{testScope}, 0, url.Values{}, oauth2.AuthStyleAutoDetect) + []string{testScope}, url.Values{}, oauth2.AuthStyleAutoDetect, 0) defer cancel() token1, err1 := oidcProvider.GetToken() @@ -208,7 +209,7 @@ func TestOIDCProvider_RefreshAhead(t *testing.T) { parser := jwt.NewParser(jwt.WithoutClaimsValidation()) oidcProvider, cancel := NewOIDCTokenProvider(t.Context(), testClientID, secretFile, tokenURL, - []string{testScope}, 2*time.Second, url.Values{}, oauth2.AuthStyleAutoDetect) + []string{testScope}, url.Values{}, oauth2.AuthStyleAutoDetect, 2) defer cancel() token1, err1 := oidcProvider.GetToken() diff --git a/pkg/kafka/configkafka/config.go b/pkg/kafka/configkafka/config.go index dc8297819a4e1..f56263e6d4f57 100644 --- a/pkg/kafka/configkafka/config.go +++ b/pkg/kafka/configkafka/config.go @@ -411,6 +411,7 @@ type OIDCFileConfig struct { RefreshAheadSecs int `mapstructure:"refresh_ahead_secs"` EndPointParams url.Values `mapstructure:"endpoint_params"` AuthStyle oauth2.AuthStyle `mapstructure:"auth_style"` + ExpiryBuffer int `mapstructure:"expiry_buffer"` } // KerberosConfig defines kerberos configuration. From e366d037e2ed04be06f2b87dcb16969d358f58ba Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Fri, 17 Oct 2025 11:18:19 -0600 Subject: [PATCH 36/38] Update description of NewOIDCTokenProvider func. Signed-off-by: Rich Scott --- internal/kafka/oidc/oidc_client.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/kafka/oidc/oidc_client.go b/internal/kafka/oidc/oidc_client.go index c29d0e52bfec3..52175f743f4f0 100644 --- a/internal/kafka/oidc/oidc_client.go +++ b/internal/kafka/oidc/oidc_client.go @@ -17,14 +17,15 @@ type OIDCTokenProvider struct { tokenSource oauth2.TokenSource } -// Token implements sarama.AccessTokenProvider for Sarama-based Kafka clients +// Token returns a sarama.AccessToken for Sarama-based Kafka clients func (p *OIDCTokenProvider) Token() (*sarama.AccessToken, error) { - oauthTok, err := p.tokenSource.Token() + token, err := p.tokenSource.Token() if err != nil { return nil, err } - - return &sarama.AccessToken{Token: oauthTok.AccessToken}, nil + return &sarama.AccessToken{ + Token: token.AccessToken, + }, nil } // GetToken returns the oauth2.Token directly for franz-go based clients @@ -33,7 +34,7 @@ func (p *OIDCTokenProvider) GetToken() (*oauth2.Token, error) { } // NewOIDCTokenProvider creates a new OIDC token provider using oauth2clientauthextension. -// This provides token functionality for both Sarama and Franz-go Kafka clients. +// This provides token functionality for Franz-go Kafka clients. func NewOIDCTokenProvider( ctx context.Context, clientID string, From 7c5a5aead9872947697dac0c9d096a11a1633416 Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Fri, 17 Oct 2025 16:38:57 -0600 Subject: [PATCH 37/38] Move internal/kafka/oidc/* to internal/kafka There's only a single program file and its test file in the former sub-dir, and adding a go.mod to the dir just to import the oauth2authclientextension package seemed more complicated than just having both oidc_client*.go reside in internal/kafka directly. Also added a 'replace' directive in the kafkareceiver/go.mod, so oidc_client can get the latest Config change in oauth2authclientextension before all this is published. Unit tests in receiver/kafkareceiver and internal/kafka are all successful (as well as the rest of the repo). Signed-off-by: Rich Scott --- internal/kafka/authentication.go | 3 +-- internal/kafka/authentication_test.go | 3 +-- internal/kafka/franz_client.go | 3 +-- internal/kafka/{oidc => }/oidc_client.go | 2 +- internal/kafka/{oidc => }/oidc_client_test.go | 2 +- receiver/kafkareceiver/go.mod | 7 ++++++- receiver/kafkareceiver/go.sum | 10 ++++++++-- 7 files changed, 19 insertions(+), 11 deletions(-) rename internal/kafka/{oidc => }/oidc_client.go (94%) rename internal/kafka/{oidc => }/oidc_client_test.go (99%) diff --git a/internal/kafka/authentication.go b/internal/kafka/authentication.go index 69f998a2cdd4f..1b462379161d9 100644 --- a/internal/kafka/authentication.go +++ b/internal/kafka/authentication.go @@ -11,7 +11,6 @@ import ( "github.com/IBM/sarama" "github.com/aws/aws-msk-iam-sasl-signer-go/signer" - "github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka/oidc" "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/configkafka" ) @@ -60,7 +59,7 @@ func configureSASL(ctx context.Context, config configkafka.SASLConfig, saramaCon saramaConfig.Net.SASL.TokenProvider = &awsMSKTokenProvider{ctx: ctx, region: config.AWSMSK.Region} case OIDCFILE: saramaConfig.Net.SASL.Mechanism = sarama.SASLTypeOAuth - saramaConfig.Net.SASL.TokenProvider, _ = oidc.NewOIDCTokenProvider(ctx, saramaConfig.ClientID, + saramaConfig.Net.SASL.TokenProvider, _ = NewOIDCTokenProvider(ctx, saramaConfig.ClientID, config.OIDCFILE.ClientSecretFilePath, config.OIDCFILE.TokenURL, config.OIDCFILE.Scopes, config.OIDCFILE.EndPointParams, config.OIDCFILE.AuthStyle, config.OIDCFILE.ExpiryBuffer) diff --git a/internal/kafka/authentication_test.go b/internal/kafka/authentication_test.go index 67eff6ac42c98..2c60f86ca4793 100644 --- a/internal/kafka/authentication_test.go +++ b/internal/kafka/authentication_test.go @@ -11,7 +11,6 @@ import ( "github.com/stretchr/testify/assert" "golang.org/x/oauth2" - "github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka/oidc" "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/configkafka" ) @@ -60,7 +59,7 @@ func TestAuthentication(t *testing.T) { // Specify 0 seconds for the RefreshAhead, as we don't want to have it launch // the background refresher goroutine - we are just verifying authentication // configuration setup here. - saramaSASLOIDCFILEConfig.Net.SASL.TokenProvider, _ = oidc.NewOIDCTokenProvider( + saramaSASLOIDCFILEConfig.Net.SASL.TokenProvider, _ = NewOIDCTokenProvider( t.Context(), saramaSASLOIDCFILEConfig.ClientID, "/etc/hosts", "http://127.0.0.1:3000/oidc", []string{"mock-scope"}, url.Values{}, oauth2.AuthStyleAutoDetect, 0) diff --git a/internal/kafka/franz_client.go b/internal/kafka/franz_client.go index 549647d43bed6..7eee544be76e6 100644 --- a/internal/kafka/franz_client.go +++ b/internal/kafka/franz_client.go @@ -30,7 +30,6 @@ import ( "go.opentelemetry.io/collector/config/configcompression" "go.uber.org/zap" - "github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka/oidc" "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/configkafka" ) @@ -320,7 +319,7 @@ func configureKgoSASL(cfg *configkafka.SASLConfig, clientID string) (kgo.Opt, er }) case OIDCFILE: m = oauth.Oauth(func(ctx context.Context) (oauth.Auth, error) { - tokenProvider, cancel := oidc.NewOIDCTokenProvider(ctx, clientID, + tokenProvider, cancel := NewOIDCTokenProvider(ctx, clientID, cfg.OIDCFILE.ClientSecretFilePath, cfg.OIDCFILE.TokenURL, cfg.OIDCFILE.Scopes, cfg.OIDCFILE.EndPointParams, cfg.OIDCFILE.AuthStyle, cfg.OIDCFILE.ExpiryBuffer) diff --git a/internal/kafka/oidc/oidc_client.go b/internal/kafka/oidc_client.go similarity index 94% rename from internal/kafka/oidc/oidc_client.go rename to internal/kafka/oidc_client.go index 52175f743f4f0..2ae103014f67c 100644 --- a/internal/kafka/oidc/oidc_client.go +++ b/internal/kafka/oidc_client.go @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package oidc // import "github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka/oidc" +package kafka // import "github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka" import ( "context" diff --git a/internal/kafka/oidc/oidc_client_test.go b/internal/kafka/oidc_client_test.go similarity index 99% rename from internal/kafka/oidc/oidc_client_test.go rename to internal/kafka/oidc_client_test.go index f9b982ca047ec..f3d966b8ffaa8 100644 --- a/internal/kafka/oidc/oidc_client_test.go +++ b/internal/kafka/oidc_client_test.go @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package oidc // import "github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka" +package kafka // import "github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka" import ( "context" diff --git a/receiver/kafkareceiver/go.mod b/receiver/kafkareceiver/go.mod index 1915ca60dfd5d..92b6fa4f7f5f5 100644 --- a/receiver/kafkareceiver/go.mod +++ b/receiver/kafkareceiver/go.mod @@ -51,6 +51,7 @@ require ( ) require ( + cloud.google.com/go/compute/metadata v0.7.0 // indirect github.com/apache/thrift v0.22.0 // indirect github.com/aws/aws-msk-iam-sasl-signer-go v1.0.4 // indirect github.com/aws/aws-sdk-go-v2 v1.36.4 // indirect @@ -96,6 +97,7 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/open-telemetry/opentelemetry-collector-contrib/extension/oauth2clientauthextension v0.137.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/pkg/core/xidutils v0.137.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.137.0 // indirect github.com/openzipkin/zipkin-go v0.4.3 // indirect @@ -114,6 +116,7 @@ require ( go.opentelemetry.io/collector/config/configoptional v1.43.1-0.20251013162618-a96eab114ea4 // indirect go.opentelemetry.io/collector/exporter v1.43.1-0.20251013162618-a96eab114ea4 // indirect go.opentelemetry.io/collector/extension v1.43.1-0.20251013162618-a96eab114ea4 // indirect + go.opentelemetry.io/collector/extension/extensionauth v1.43.1-0.20251013162618-a96eab114ea4 // indirect go.opentelemetry.io/collector/extension/xextension v0.137.1-0.20251013162618-a96eab114ea4 // indirect go.opentelemetry.io/collector/internal/telemetry v0.137.1-0.20251013162618-a96eab114ea4 // indirect go.opentelemetry.io/collector/pdata/xpdata v0.137.1-0.20251013162618-a96eab114ea4 // indirect @@ -126,7 +129,7 @@ require ( golang.org/x/crypto v0.43.0 // indirect golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect golang.org/x/net v0.46.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/oauth2 v0.31.0 // indirect golang.org/x/sys v0.37.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect google.golang.org/grpc v1.76.0 // indirect @@ -159,3 +162,5 @@ replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden => replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/azure => ../../pkg/translator/azure replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/configkafka => ../../pkg/kafka/configkafka + +replace github.com/open-telemetry/opentelemetry-collector-contrib/extension/oauth2clientauthextension => ../../extension/oauth2clientauthextension diff --git a/receiver/kafkareceiver/go.sum b/receiver/kafkareceiver/go.sum index 1137bf60dd95e..f528564426228 100644 --- a/receiver/kafkareceiver/go.sum +++ b/receiver/kafkareceiver/go.sum @@ -1,3 +1,5 @@ +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= github.com/IBM/sarama v1.46.2 h1:65JJmZpxKUWe/7HEHmc56upTfAvgoxuyu4Ek+TcevDE= github.com/IBM/sarama v1.46.2/go.mod h1:PDOGmVeKmW744c/0d4CZ0MfrzmcIYtpmS5+KIWs1zHQ= github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc= @@ -129,6 +131,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/open-telemetry/opentelemetry-collector-contrib/extension/oauth2clientauthextension v0.137.0 h1:cKeFgcZf0mI7NL4NLB6c4nsQVZ5Gv14cg6wyB5BFsns= +github.com/open-telemetry/opentelemetry-collector-contrib/extension/oauth2clientauthextension v0.137.0/go.mod h1:GLGpfbWdN6n/I0WA+JwGdokRO9WcHDzCPojxmjK4yEI= github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= @@ -218,6 +222,8 @@ go.opentelemetry.io/collector/exporter/xexporter v0.137.0 h1:2fSmBDB+tuFoYKJSHbR go.opentelemetry.io/collector/exporter/xexporter v0.137.0/go.mod h1:9gudRad3ijkbzcnTLE0y+CzUDtC4TaPyZQDUKB2yzVs= go.opentelemetry.io/collector/extension v1.43.1-0.20251013162618-a96eab114ea4 h1:ydWUlclX7E9H3AGvrayXMnplL7DZXcJShKbCp0fweEY= go.opentelemetry.io/collector/extension v1.43.1-0.20251013162618-a96eab114ea4/go.mod h1:GIzXTwB+7I3TpTx2Zppp/2884BNg1LaWe0zjumgV82Q= +go.opentelemetry.io/collector/extension/extensionauth v1.43.1-0.20251013162618-a96eab114ea4 h1:6BbCvOb/86AnIGn5J1vcNauoZ2cbloU4WscZJMjbkso= +go.opentelemetry.io/collector/extension/extensionauth v1.43.1-0.20251013162618-a96eab114ea4/go.mod h1:6Sh0hqPfPqpg0ErCoNPO/ky2NdfGmUX+G5wekPx7A7U= go.opentelemetry.io/collector/extension/extensiontest v0.137.0 h1:gnPF3HIOKqNk93XObt2x0WFvVfPtm76VggWe7LxgcaY= go.opentelemetry.io/collector/extension/extensiontest v0.137.0/go.mod h1:vVmKojdITYka9+iAi3aarxeMrO6kdlywKuf3d3c6lcI= go.opentelemetry.io/collector/extension/xextension v0.137.1-0.20251013162618-a96eab114ea4 h1:0DbV+ob2o19r03p7ZbacTeMqFi1B/ivpcmNBPGLkA+k= @@ -302,8 +308,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= +golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= From d91ca1eba63262613198011ce136ee18f9b1f6aa Mon Sep 17 00:00:00 2001 From: Rich Scott Date: Fri, 17 Oct 2025 18:06:17 -0600 Subject: [PATCH 38/38] go.mod fixes for each kafka component Add a 'replace' rule so that each kafka component's go.mod uses the direct source in extension/oauth2clientauthextension, so it has the latest (e.g. this PR branch) changes available, so the OIDC client logic available to each kafka component can find the Config definition in oauth2clientauthextension. Signed-off-by: Rich Scott --- exporter/kafkaexporter/go.mod | 7 ++++++- exporter/kafkaexporter/go.sum | 10 ++++++++-- extension/observer/kafkatopicsobserver/go.mod | 7 ++++++- extension/observer/kafkatopicsobserver/go.sum | 10 ++++++++-- receiver/kafkametricsreceiver/go.mod | 8 +++++++- receiver/kafkametricsreceiver/go.sum | 12 ++++++++++-- 6 files changed, 45 insertions(+), 9 deletions(-) diff --git a/exporter/kafkaexporter/go.mod b/exporter/kafkaexporter/go.mod index e083965482fc2..91e4ec7a74d6b 100644 --- a/exporter/kafkaexporter/go.mod +++ b/exporter/kafkaexporter/go.mod @@ -46,6 +46,7 @@ require ( ) require ( + cloud.google.com/go/compute/metadata v0.7.0 // indirect github.com/apache/thrift v0.22.0 // indirect github.com/aws/aws-msk-iam-sasl-signer-go v1.0.4 // indirect github.com/aws/aws-sdk-go-v2 v1.36.4 // indirect @@ -92,6 +93,7 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/open-telemetry/opentelemetry-collector-contrib/extension/oauth2clientauthextension v0.137.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/pkg/core/xidutils v0.137.0 // indirect github.com/openzipkin/zipkin-go v0.4.3 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect @@ -112,6 +114,7 @@ require ( go.opentelemetry.io/collector/consumer/consumertest v0.137.1-0.20251013162618-a96eab114ea4 // indirect go.opentelemetry.io/collector/consumer/xconsumer v0.137.1-0.20251013162618-a96eab114ea4 // indirect go.opentelemetry.io/collector/extension v1.43.1-0.20251013162618-a96eab114ea4 // indirect + go.opentelemetry.io/collector/extension/extensionauth v1.43.1-0.20251013162618-a96eab114ea4 // indirect go.opentelemetry.io/collector/extension/xextension v0.137.1-0.20251013162618-a96eab114ea4 // indirect go.opentelemetry.io/collector/internal/telemetry v0.137.1-0.20251013162618-a96eab114ea4 // indirect go.opentelemetry.io/collector/pdata/xpdata v0.137.1-0.20251013162618-a96eab114ea4 // indirect @@ -126,7 +129,7 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.43.0 // indirect golang.org/x/net v0.46.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/oauth2 v0.31.0 // indirect golang.org/x/sys v0.37.0 // indirect golang.org/x/text v0.30.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect @@ -162,3 +165,5 @@ replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden => ../../pkg/golden replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/configkafka => ../../pkg/kafka/configkafka + +replace github.com/open-telemetry/opentelemetry-collector-contrib/extension/oauth2clientauthextension => ../../extension/oauth2clientauthextension diff --git a/exporter/kafkaexporter/go.sum b/exporter/kafkaexporter/go.sum index 4e03f3a452e22..1e9d3941f619b 100644 --- a/exporter/kafkaexporter/go.sum +++ b/exporter/kafkaexporter/go.sum @@ -1,3 +1,5 @@ +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= github.com/IBM/sarama v1.46.2 h1:65JJmZpxKUWe/7HEHmc56upTfAvgoxuyu4Ek+TcevDE= github.com/IBM/sarama v1.46.2/go.mod h1:PDOGmVeKmW744c/0d4CZ0MfrzmcIYtpmS5+KIWs1zHQ= github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc= @@ -127,6 +129,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/open-telemetry/opentelemetry-collector-contrib/extension/oauth2clientauthextension v0.137.0 h1:cKeFgcZf0mI7NL4NLB6c4nsQVZ5Gv14cg6wyB5BFsns= +github.com/open-telemetry/opentelemetry-collector-contrib/extension/oauth2clientauthextension v0.137.0/go.mod h1:GLGpfbWdN6n/I0WA+JwGdokRO9WcHDzCPojxmjK4yEI= github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= @@ -216,6 +220,8 @@ go.opentelemetry.io/collector/exporter/xexporter v0.137.1-0.20251013162618-a96ea go.opentelemetry.io/collector/exporter/xexporter v0.137.1-0.20251013162618-a96eab114ea4/go.mod h1:VVH3xbkykgHV+TIfyzRA+1ZLsMjb0FLQZmEphtPI63A= go.opentelemetry.io/collector/extension v1.43.1-0.20251013162618-a96eab114ea4 h1:ydWUlclX7E9H3AGvrayXMnplL7DZXcJShKbCp0fweEY= go.opentelemetry.io/collector/extension v1.43.1-0.20251013162618-a96eab114ea4/go.mod h1:GIzXTwB+7I3TpTx2Zppp/2884BNg1LaWe0zjumgV82Q= +go.opentelemetry.io/collector/extension/extensionauth v1.43.1-0.20251013162618-a96eab114ea4 h1:6BbCvOb/86AnIGn5J1vcNauoZ2cbloU4WscZJMjbkso= +go.opentelemetry.io/collector/extension/extensionauth v1.43.1-0.20251013162618-a96eab114ea4/go.mod h1:6Sh0hqPfPqpg0ErCoNPO/ky2NdfGmUX+G5wekPx7A7U= go.opentelemetry.io/collector/extension/extensiontest v0.137.0 h1:gnPF3HIOKqNk93XObt2x0WFvVfPtm76VggWe7LxgcaY= go.opentelemetry.io/collector/extension/extensiontest v0.137.0/go.mod h1:vVmKojdITYka9+iAi3aarxeMrO6kdlywKuf3d3c6lcI= go.opentelemetry.io/collector/extension/xextension v0.137.1-0.20251013162618-a96eab114ea4 h1:0DbV+ob2o19r03p7ZbacTeMqFi1B/ivpcmNBPGLkA+k= @@ -298,8 +304,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= +golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/extension/observer/kafkatopicsobserver/go.mod b/extension/observer/kafkatopicsobserver/go.mod index 550876c41e38b..5da41f91f97fd 100644 --- a/extension/observer/kafkatopicsobserver/go.mod +++ b/extension/observer/kafkatopicsobserver/go.mod @@ -19,6 +19,7 @@ require ( ) require ( + cloud.google.com/go/compute/metadata v0.7.0 // indirect github.com/aws/aws-msk-iam-sasl-signer-go v1.0.4 // indirect github.com/aws/aws-sdk-go-v2 v1.36.4 // indirect github.com/aws/aws-sdk-go-v2/config v1.29.16 // indirect @@ -63,6 +64,7 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/open-telemetry/opentelemetry-collector-contrib/extension/oauth2clientauthextension v0.137.0 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect @@ -79,6 +81,7 @@ require ( go.opentelemetry.io/collector/config/configcompression v1.43.1-0.20251013162618-a96eab114ea4 // indirect go.opentelemetry.io/collector/config/configopaque v1.43.1-0.20251013162618-a96eab114ea4 // indirect go.opentelemetry.io/collector/config/configtls v1.43.1-0.20251013162618-a96eab114ea4 // indirect + go.opentelemetry.io/collector/extension/extensionauth v1.43.1-0.20251013162618-a96eab114ea4 // indirect go.opentelemetry.io/collector/featuregate v1.43.1-0.20251013162618-a96eab114ea4 // indirect go.opentelemetry.io/collector/internal/telemetry v0.137.1-0.20251013162618-a96eab114ea4 // indirect go.opentelemetry.io/collector/pdata v1.43.1-0.20251013162618-a96eab114ea4 // indirect @@ -92,7 +95,7 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.43.0 // indirect golang.org/x/net v0.46.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/oauth2 v0.31.0 // indirect golang.org/x/sys v0.37.0 // indirect golang.org/x/text v0.30.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect @@ -106,3 +109,5 @@ replace github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/configkafka => ../../../pkg/kafka/configkafka replace github.com/open-telemetry/opentelemetry-collector-contrib/extension/observer => ../ + +replace github.com/open-telemetry/opentelemetry-collector-contrib/extension/oauth2clientauthextension => ../../../extension/oauth2clientauthextension diff --git a/extension/observer/kafkatopicsobserver/go.sum b/extension/observer/kafkatopicsobserver/go.sum index a39afdd9906ff..b863a375b2e4d 100644 --- a/extension/observer/kafkatopicsobserver/go.sum +++ b/extension/observer/kafkatopicsobserver/go.sum @@ -1,3 +1,5 @@ +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= github.com/IBM/sarama v1.46.2 h1:65JJmZpxKUWe/7HEHmc56upTfAvgoxuyu4Ek+TcevDE= github.com/IBM/sarama v1.46.2/go.mod h1:PDOGmVeKmW744c/0d4CZ0MfrzmcIYtpmS5+KIWs1zHQ= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.4 h1:2jAwFwA0Xgcx94dUId+K24yFabsKYDtAhCgyMit6OqE= @@ -182,6 +184,10 @@ go.opentelemetry.io/collector/confmap/xconfmap v0.137.1-0.20251013162618-a96eab1 go.opentelemetry.io/collector/confmap/xconfmap v0.137.1-0.20251013162618-a96eab114ea4/go.mod h1:psXdQr13pVrCqNPdoER2QZZorvONAR5ZUEHURe4POh4= go.opentelemetry.io/collector/extension v1.43.1-0.20251013162618-a96eab114ea4 h1:ydWUlclX7E9H3AGvrayXMnplL7DZXcJShKbCp0fweEY= go.opentelemetry.io/collector/extension v1.43.1-0.20251013162618-a96eab114ea4/go.mod h1:GIzXTwB+7I3TpTx2Zppp/2884BNg1LaWe0zjumgV82Q= +go.opentelemetry.io/collector/extension/extensionauth v1.43.1-0.20251013162618-a96eab114ea4 h1:6BbCvOb/86AnIGn5J1vcNauoZ2cbloU4WscZJMjbkso= +go.opentelemetry.io/collector/extension/extensionauth v1.43.1-0.20251013162618-a96eab114ea4/go.mod h1:6Sh0hqPfPqpg0ErCoNPO/ky2NdfGmUX+G5wekPx7A7U= +go.opentelemetry.io/collector/extension/extensiontest v0.137.1-0.20251013162618-a96eab114ea4 h1:DBMKb2WJVCjqa6hkDgVI2mgnzv/x/mx9JUA35U0k1YA= +go.opentelemetry.io/collector/extension/extensiontest v0.137.1-0.20251013162618-a96eab114ea4/go.mod h1:2wyfqLdE/kVOdVOnWxPBQlEnI2NV0bqGXJFRPSrtqy0= go.opentelemetry.io/collector/featuregate v1.43.1-0.20251013162618-a96eab114ea4 h1:IXK7EGifr3Lic3mnMlkVXFb1HBlkBoYfoy/AYzsevko= go.opentelemetry.io/collector/featuregate v1.43.1-0.20251013162618-a96eab114ea4/go.mod h1:d0tiRzVYrytB6LkcYgz2ESFTv7OktRPQe0QEQcPt1L4= go.opentelemetry.io/collector/internal/telemetry v0.137.1-0.20251013162618-a96eab114ea4 h1:E94fKydbFNkR6Zfhix/alvOftJx6e6srYQ8PIr9xYRw= @@ -246,8 +252,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= +golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/receiver/kafkametricsreceiver/go.mod b/receiver/kafkametricsreceiver/go.mod index 43a7bafa4a44c..e83907f65919e 100644 --- a/receiver/kafkametricsreceiver/go.mod +++ b/receiver/kafkametricsreceiver/go.mod @@ -29,6 +29,7 @@ require ( ) require ( + cloud.google.com/go/compute/metadata v0.7.0 // indirect github.com/aws/aws-msk-iam-sasl-signer-go v1.0.4 // indirect github.com/aws/aws-sdk-go-v2 v1.36.4 // indirect github.com/aws/aws-sdk-go-v2/config v1.29.16 // indirect @@ -73,6 +74,7 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/open-telemetry/opentelemetry-collector-contrib/extension/oauth2clientauthextension v0.137.0 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect @@ -89,6 +91,8 @@ require ( go.opentelemetry.io/collector/config/configtls v1.43.1-0.20251013162618-a96eab114ea4 // indirect go.opentelemetry.io/collector/consumer/consumererror v0.137.1-0.20251013162618-a96eab114ea4 // indirect go.opentelemetry.io/collector/consumer/xconsumer v0.137.1-0.20251013162618-a96eab114ea4 // indirect + go.opentelemetry.io/collector/extension v1.43.1-0.20251013162618-a96eab114ea4 // indirect + go.opentelemetry.io/collector/extension/extensionauth v1.43.1-0.20251013162618-a96eab114ea4 // indirect go.opentelemetry.io/collector/internal/telemetry v0.137.1-0.20251013162618-a96eab114ea4 // indirect go.opentelemetry.io/collector/pdata/pprofile v0.137.1-0.20251013162618-a96eab114ea4 // indirect go.opentelemetry.io/collector/pipeline v1.43.1-0.20251013162618-a96eab114ea4 // indirect @@ -104,7 +108,7 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.43.0 // indirect golang.org/x/net v0.46.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/oauth2 v0.31.0 // indirect golang.org/x/sys v0.37.0 // indirect golang.org/x/text v0.30.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect @@ -117,6 +121,8 @@ replace github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/configkafka => ../../pkg/kafka/configkafka +replace github.com/open-telemetry/opentelemetry-collector-contrib/extension/oauth2clientauthextension => ../../extension/oauth2clientauthextension + // see https://github.com/distribution/distribution/issues/3590 exclude github.com/docker/distribution v2.8.0+incompatible diff --git a/receiver/kafkametricsreceiver/go.sum b/receiver/kafkametricsreceiver/go.sum index b50e696468c31..caaa2e67c3c9f 100644 --- a/receiver/kafkametricsreceiver/go.sum +++ b/receiver/kafkametricsreceiver/go.sum @@ -1,3 +1,5 @@ +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= github.com/IBM/sarama v1.46.2 h1:65JJmZpxKUWe/7HEHmc56upTfAvgoxuyu4Ek+TcevDE= github.com/IBM/sarama v1.46.2/go.mod h1:PDOGmVeKmW744c/0d4CZ0MfrzmcIYtpmS5+KIWs1zHQ= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.4 h1:2jAwFwA0Xgcx94dUId+K24yFabsKYDtAhCgyMit6OqE= @@ -188,6 +190,12 @@ go.opentelemetry.io/collector/consumer/consumertest v0.137.1-0.20251013162618-a9 go.opentelemetry.io/collector/consumer/consumertest v0.137.1-0.20251013162618-a96eab114ea4/go.mod h1:Yz1Mo4ibkgutZRUOf/odYxyrJCnqWPiQ3s9XsESkyZA= go.opentelemetry.io/collector/consumer/xconsumer v0.137.1-0.20251013162618-a96eab114ea4 h1:t3bFdGBL/gCTXVeKRNd/n9dhl1mKj+upajY6Y5pL1yM= go.opentelemetry.io/collector/consumer/xconsumer v0.137.1-0.20251013162618-a96eab114ea4/go.mod h1:RsSFgyg2HiQHSE0yBFxG1GKm+x/sKtLYNkhwUqQ6ABg= +go.opentelemetry.io/collector/extension v1.43.1-0.20251013162618-a96eab114ea4 h1:ydWUlclX7E9H3AGvrayXMnplL7DZXcJShKbCp0fweEY= +go.opentelemetry.io/collector/extension v1.43.1-0.20251013162618-a96eab114ea4/go.mod h1:GIzXTwB+7I3TpTx2Zppp/2884BNg1LaWe0zjumgV82Q= +go.opentelemetry.io/collector/extension/extensionauth v1.43.1-0.20251013162618-a96eab114ea4 h1:6BbCvOb/86AnIGn5J1vcNauoZ2cbloU4WscZJMjbkso= +go.opentelemetry.io/collector/extension/extensionauth v1.43.1-0.20251013162618-a96eab114ea4/go.mod h1:6Sh0hqPfPqpg0ErCoNPO/ky2NdfGmUX+G5wekPx7A7U= +go.opentelemetry.io/collector/extension/extensiontest v0.137.1-0.20251013162618-a96eab114ea4 h1:DBMKb2WJVCjqa6hkDgVI2mgnzv/x/mx9JUA35U0k1YA= +go.opentelemetry.io/collector/extension/extensiontest v0.137.1-0.20251013162618-a96eab114ea4/go.mod h1:2wyfqLdE/kVOdVOnWxPBQlEnI2NV0bqGXJFRPSrtqy0= go.opentelemetry.io/collector/featuregate v1.43.1-0.20251013162618-a96eab114ea4 h1:IXK7EGifr3Lic3mnMlkVXFb1HBlkBoYfoy/AYzsevko= go.opentelemetry.io/collector/featuregate v1.43.1-0.20251013162618-a96eab114ea4/go.mod h1:d0tiRzVYrytB6LkcYgz2ESFTv7OktRPQe0QEQcPt1L4= go.opentelemetry.io/collector/filter v0.137.1-0.20251013162618-a96eab114ea4 h1:Njejdb5z+0bGP41bty5HeDZtslEwk5pNceRmeYCLys0= @@ -270,8 +278,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= +golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=