Ctrl K
ComunidadGuías…

internal/kafka/oauth.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package kafka

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"time"

	"cloud.google.com/go/compute/metadata"
	"github.com/Shopify/sarama"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
)

// gcpTokenProvider implements sarama.AccessTokenProvider for SASL/OAUTHBEARER
// against GCP Managed Service for Apache Kafka (GMK). It pulls credentials from
// Application Default Credentials, which resolves to:
//   - the GKE metadata server when running with Workload Identity, or
//   - gcloud user credentials when running locally.
//
// IMPORTANT: GMK does NOT accept a bare OAuth2 access token over OAUTHBEARER.
// The broker expects a Google-specific token: three base64url segments joined
// with "." — a JWT-style header {"typ":"JWT","alg":"GOOG_OAUTH2_TOKEN"}, a
// payload {"exp","iss":"Google","iat","sub":<principal email>}, and the raw
// access token. Sending the plain access token yields:
//
//	kafka server: SASL Authentication failed: ... invalid credentials with
//	SASL mechanism OAUTHBEARER
//
// This format is defined by Google's reference implementation:
// https://github.com/googleapis/managedkafka/blob/main/kafka-auth-local-server/kafka_gcp_credentials_server.py
//
// oauth2.ReuseTokenSource caches the underlying access token until expiry, so we
// don't hit the metadata server on every broker connect.
type gcpTokenProvider struct {
	src oauth2.TokenSource
	// principal is the service-account email used as the JWT `sub`. When empty
	// it is resolved lazily from the metadata server on first Token() call.
	principal string
}

// gmkJWTHeader is the fixed OAUTHBEARER header GMK requires. The alg value is a
// Google sentinel, not a real signing algorithm — the third segment carries the
// already-minted access token rather than a signature.
var gmkJWTHeader = mustB64JSON(map[string]string{"typ": "JWT", "alg": "GOOG_OAUTH2_TOKEN"})

// NewGCPTokenProvider builds a sarama.AccessTokenProvider that emits GMK-format
// OAUTHBEARER tokens (see gcpTokenProvider). Exported so the separate
// internal/pubsub/kafka client can reuse the exact same token logic.
func NewGCPTokenProvider(ctx context.Context, scopes []string) (sarama.AccessTokenProvider, error) {
	return newGCPTokenProvider(ctx, scopes)
}

func newGCPTokenProvider(ctx context.Context, scopes []string) (*gcpTokenProvider, error) {
	if len(scopes) == 0 {
		scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
	}
	src, err := google.DefaultTokenSource(ctx, scopes...)
	if err != nil {
		return nil, fmt.Errorf("kafka oauthbearer: resolve GCP default token source: %w", err)
	}
	// Allow an explicit principal override (matches the Google reference's
	// GOOGLE_MANAGED_KAFKA_AUTH_PRINCIPAL env var). Otherwise it is resolved
	// from the metadata server on first use.
	principal := os.Getenv("GOOGLE_MANAGED_KAFKA_AUTH_PRINCIPAL")
	return &gcpTokenProvider{
		src:       oauth2.ReuseTokenSource(nil, src),
		principal: principal,
	}, nil
}

func (p *gcpTokenProvider) Token() (*sarama.AccessToken, error) {
	tok, err := p.src.Token()
	if err != nil {
		return nil, fmt.Errorf("kafka oauthbearer: fetch GCP access token: %w", err)
	}

	principal, err := p.resolvePrincipal()
	if err != nil {
		return nil, fmt.Errorf("kafka oauthbearer: resolve principal email: %w", err)
	}

	payload := mustB64JSON(map[string]any{
		"exp": tok.Expiry.UTC().Unix(),
		"iss": "Google",
		"iat": time.Now().UTC().Unix(),
		"sub": principal,
	})
	encodedToken := base64.RawURLEncoding.EncodeToString([]byte(tok.AccessToken))

	gmkToken := strings.Join([]string{gmkJWTHeader, payload, encodedToken}, ".")
	return &sarama.AccessToken{Token: gmkToken}, nil
}

// resolvePrincipal returns the service-account email for the JWT `sub`. It is
// cached after the first successful lookup.
func (p *gcpTokenProvider) resolvePrincipal() (string, error) {
	if p.principal != "" {
		return p.principal, nil
	}
	// Under Workload Identity the access token does not carry the SA email, so
	// fetch it from the metadata server (the impersonated GSA's default SA).
	email, err := metadata.Email("default")
	if err != nil {
		return "", fmt.Errorf("query metadata server for SA email (set GOOGLE_MANAGED_KAFKA_AUTH_PRINCIPAL to override): %w", err)
	}
	p.principal = email
	return email, nil
}

func mustB64JSON(v any) string {
	b, err := json.Marshal(v)
	if err != nil {
		// The inputs are fixed maps of primitives; marshaling cannot fail.
		panic(fmt.Sprintf("kafka oauthbearer: marshal token segment: %v", err))
	}
	return base64.RawURLEncoding.EncodeToString(b)
}