package auth import ( "context" "fmt" "strings" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" ) type contextKey string const ( pubkeyContextKey contextKey = "nostr-pubkey" ) type InterceptorOptions struct { Read AuthOperationConfig Write AuthOperationConfig TimestampWindow int64 ValidatePayload bool SkipMethods []string } type AuthOperationConfig struct { Enabled bool `yaml:"enabled"` AllowedNpubs []string `yaml:"allowed_npubs"` } func DefaultInterceptorOptions() *InterceptorOptions { return &InterceptorOptions{ Read: AuthOperationConfig{ Enabled: false, AllowedNpubs: nil, }, Write: AuthOperationConfig{ Enabled: false, AllowedNpubs: nil, }, TimestampWindow: 60, ValidatePayload: false, SkipMethods: nil, } } func NostrUnaryInterceptor(opts *InterceptorOptions) grpc.UnaryServerInterceptor { if opts == nil { opts = DefaultInterceptorOptions() } return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { if shouldSkipAuth(info.FullMethod, opts.SkipMethods) { return handler(ctx, req) } var authRequired bool if isWriteMethod(info.FullMethod) { authRequired = opts.Write.Enabled } else { authRequired = opts.Read.Enabled } if !authRequired { return handler(ctx, req) } pubkey, err := validateAuthFromContext(ctx, info.FullMethod, opts) if err != nil { return nil, status.Error(codes.Unauthenticated, err.Error()) } ctx = context.WithValue(ctx, pubkeyContextKey, pubkey) return handler(ctx, req) } } func NostrStreamInterceptor(opts *InterceptorOptions) grpc.StreamServerInterceptor { if opts == nil { opts = DefaultInterceptorOptions() } return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { if shouldSkipAuth(info.FullMethod, opts.SkipMethods) { return handler(srv, ss) } var authRequired bool if isWriteMethod(info.FullMethod) { authRequired = opts.Write.Enabled } else { authRequired = opts.Read.Enabled } if !authRequired { return handler(srv, ss) } ctx := ss.Context() pubkey, err := validateAuthFromContext(ctx, info.FullMethod, opts) if err != nil { return status.Error(codes.Unauthenticated, err.Error()) } wrappedStream := &authenticatedStream{ ServerStream: ss, ctx: context.WithValue(ctx, pubkeyContextKey, pubkey), } return handler(srv, wrappedStream) } } type authenticatedStream struct{ grpc.ServerStream ctx context.Context } func (s *authenticatedStream) Context() context.Context { return s.ctx } func validateAuthFromContext(ctx context.Context, method string, opts *InterceptorOptions) (string, error) { md, ok := metadata.FromIncomingContext(ctx) if !ok { return "", fmt.Errorf("missing metadata") } authHeaders := md.Get("authorization") if len(authHeaders) == 0 { return "", fmt.Errorf("missing authorization header") } event, err := ParseAuthHeader(authHeaders[0]) if err != nil { return "", fmt.Errorf("invalid auth header: %w", err) } validationOpts := ValidationOptions{ TimestampWindow: opts.TimestampWindow, ValidatePayload: opts.ValidatePayload, ExpectedMethod: "POST", } if err := ValidateAuthEvent(event, validationOpts); err != nil { return "", fmt.Errorf("invalid auth event: %w", err) } pubkey := ExtractPubkey(event) var opConfig AuthOperationConfig if isWriteMethod(method) { opConfig = opts.Write } else { opConfig = opts.Read } if len(opConfig.AllowedNpubs) > 0 { if !contains(opConfig.AllowedNpubs, pubkey) { if isWriteMethod(method) { return "", fmt.Errorf("pubkey not authorized for write operations") } return "", fmt.Errorf("pubkey not authorized for read operations") } } return pubkey, nil } func isWriteMethod(method string) bool { return strings.Contains(method, "/PublishEvent") || strings.Contains(method, "/PublishBatch") } func shouldSkipAuth(method string, skipMethods []string) bool { for _, skip := range skipMethods { if skip == method { return true } } return false } func contains(slice []string, item string) bool { for _, s := range slice { if s == item { return true } } return false } func PubkeyFromContext(ctx context.Context) (string, bool) { pubkey, ok := ctx.Value(pubkeyContextKey).(string) return pubkey, ok } func RequireAuth(ctx context.Context) (string, error) { pubkey, ok := PubkeyFromContext(ctx) if !ok || pubkey == "" { return "", status.Error(codes.Unauthenticated, "authentication required") } return pubkey, nil }