package grpcmw
import (
"context"
"time"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
type ctxKey string
const principalKey ctxKey = "principal"
func Principal(ctx context.Context) (string, bool) {
v := ctx.Value(principalKey)
s, ok := v.(string)
return s, ok
}
func UnaryAuth(log *zap.Logger, validate func(token string) (string, error)) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
start := time.Now()
md, _ := metadata.FromIncomingContext(ctx)
token := ""
if vals := md.Get("authorization"); len(vals) > 0 {
token = vals[0]
}
principal, err := validate(token)
if err != nil {
return nil, status.Error(codes.Unauthenticated, "unauthenticated")
}
ctx = context.WithValue(ctx, principalKey, principal)
resp, hErr := handler(ctx, req)
log.Info("grpc.unary", zap.String("method", info.FullMethod), zap.Duration("duration", time.Since(start)), zap.Error(hErr))
return resp, hErr
}
}
Interceptors are the cleanest way to standardize cross-cutting behavior in gRPC. I use a unary interceptor to extract authorization from metadata, validate it, attach the principal to context.Context, and log the method name and duration. This keeps service methods focused: they read principal from context and do work. The subtle benefit is that all endpoints share the same auth policy and logging format, so your logs are consistent and greppable. I also keep the error surface tight: unauthenticated requests return codes.Unauthenticated and we don't leak whether the token was missing vs invalid. Combined with metrics and tracing interceptors, this becomes a strong baseline for production-grade gRPC services.