package grpcserver import ( "context" "crypto/subtle" "fmt" "strings" "git3.techno-world.net/lrosales/broad-announce/internal/pipeline" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" ) // authenticate validates the API key from gRPC metadata and returns the // corresponding SourceConfig. It is the gRPC equivalent of the HTTP // X-BA-Signature HMAC check. // // Metadata key: "authorization" → "Bearer ::" // // Returns codes.Unauthenticated on any failure. func authenticate(ctx context.Context, sources map[string]pipeline.SourceConfig) (*pipeline.SourceConfig, error) { md, ok := metadata.FromIncomingContext(ctx) if !ok { return nil, status.Error(codes.Unauthenticated, "missing metadata") } // Support both canonical "authorization" and " Authorization". authVals := md.Get("authorization") if len(authVals) == 0 { authVals = md.Get("Authorization") } if len(authVals) == 0 || authVals[0] == "" { return nil, status.Error(codes.Unauthenticated, "missing authorization header") } raw := strings.TrimPrefix(authVals[0], "Bearer ") if raw == authVals[0] { return nil, status.Error(codes.Unauthenticated, "authorization must use Bearer scheme") } key := parseAPIKey(raw) if key == nil { return nil, status.Error(codes.Unauthenticated, "malformed API key") } src, ok := sources[key.SourceKey()] if !ok { return nil, status.Error(codes.Unauthenticated, fmt.Sprintf("no such source %s/%s", key.CompanyID, key.SourceID)) } // Verify the secret. The sources map stores the full raw API key as the // secret for env-based auth. Constant-time compare to avoid timing leaks. want := key.CompanyID + ":" + key.SourceID + ":" + string(src.HMACSecret) if subtle.ConstantTimeCompare([]byte(raw), []byte(want)) != 1 { return nil, status.Error(codes.Unauthenticated, "invalid API key") } return &src, nil } // apiKey represents a parsed API key in "::" form. type apiKey struct { CompanyID string SourceID string Secret string } func parseAPIKey(raw string) *apiKey { parts := strings.SplitN(raw, ":", 3) if len(parts) != 3 { return nil } return &apiKey{CompanyID: parts[0], SourceID: parts[1], Secret: parts[2]} } func (k *apiKey) SourceKey() string { return k.CompanyID + ":" + k.SourceID }