From 606e0a3329a3534a00889eee19c25e7d432f7d2d Mon Sep 17 00:00:00 2001 From: bndw Date: Sat, 14 Feb 2026 10:11:16 -0800 Subject: refactor: restructure auth config for better UX Changed from flat structure to hierarchical read/write config: Before: auth: enabled: bool required: bool allowed_npubs_read: [] allowed_npubs_write: [] After: auth: read: enabled: bool allowed_npubs: [] write: enabled: bool allowed_npubs: [] Three states per operation: - enabled=false: no auth, allow all - enabled=true, allowed_npubs=[]: auth required, any valid signature - enabled=true, allowed_npubs=[...]: auth required, whitelist only Much clearer semantics and easier to reason about. --- internal/auth/interceptor.go | 118 ++++++++++++++++++++++++++----------------- 1 file changed, 72 insertions(+), 46 deletions(-) (limited to 'internal/auth/interceptor.go') diff --git a/internal/auth/interceptor.go b/internal/auth/interceptor.go index 149cc01..d394102 100644 --- a/internal/auth/interceptor.go +++ b/internal/auth/interceptor.go @@ -21,49 +21,55 @@ const ( // InterceptorOptions configures the authentication interceptor behavior. type InterceptorOptions struct { + // Read configures authentication for read operations (Subscribe, QueryEvents, etc.) + Read OperationAuthConfig + + // Write configures authentication for write operations (PublishEvent, PublishBatch) + Write OperationAuthConfig + // TimestampWindow is the maximum age of auth events in seconds. // Events older than this are rejected to prevent replay attacks. // Default: 60 seconds TimestampWindow int64 - // Required determines whether authentication is mandatory. - // If true, requests without valid auth are rejected. - // If false, unauthenticated requests are allowed (pubkey will be empty). - // Default: false - Required bool - // ValidatePayload checks the payload hash tag if present. // Default: false ValidatePayload bool - // AllowedNpubsRead is an optional whitelist of allowed pubkeys for read operations (hex format). - // Config accepts npub format only, normalized to hex at load time. - // If nil or empty, all valid signatures are accepted for reads. - // Default: nil (allow all) - AllowedNpubsRead []string - - // AllowedNpubsWrite is an optional whitelist of allowed pubkeys for write operations (hex format). - // Config accepts npub format only, normalized to hex at load time. - // If nil or empty, all valid signatures are accepted for writes. - // Default: nil (allow all) - AllowedNpubsWrite []string - // SkipMethods is a list of gRPC methods that bypass authentication. // Useful for public endpoints like health checks or relay info. - // Example: []string{"/nostr.v1.NostrRelay/QueryEvents"} + // Example: []string{"/grpc.health.v1.Health/Check"} // Default: nil (authenticate all methods) SkipMethods []string } +// OperationAuthConfig configures auth for a specific operation type. +type OperationAuthConfig struct { + // Enabled determines if auth is required. + // false = no auth, allow all + // true = auth required + Enabled bool + + // AllowedNpubs is an optional whitelist (hex format, normalized from npub at config load). + // If Enabled=true && AllowedNpubs=[]: any valid signature accepted + // If Enabled=true && AllowedNpubs=[...]: only whitelisted npubs accepted + AllowedNpubs []string +} + // DefaultInterceptorOptions returns the default configuration. func DefaultInterceptorOptions() *InterceptorOptions { return &InterceptorOptions{ - TimestampWindow: 60, - Required: false, - ValidatePayload: false, - AllowedNpubsRead: nil, - AllowedNpubsWrite: nil, - SkipMethods: nil, + Read: OperationAuthConfig{ + Enabled: false, + AllowedNpubs: nil, + }, + Write: OperationAuthConfig{ + Enabled: false, + AllowedNpubs: nil, + }, + TimestampWindow: 60, + ValidatePayload: false, + SkipMethods: nil, } } @@ -79,14 +85,23 @@ func NostrUnaryInterceptor(opts *InterceptorOptions) grpc.UnaryServerInterceptor return handler(ctx, req) } + // Check if auth is required for this operation type + var authRequired bool + if isWriteMethod(info.FullMethod) { + authRequired = opts.Write.Enabled + } else { + authRequired = opts.Read.Enabled + } + + // If auth not required, skip validation + if !authRequired { + return handler(ctx, req) + } + // Extract and validate auth pubkey, err := validateAuthFromContext(ctx, info.FullMethod, opts) if err != nil { - if opts.Required { - return nil, status.Error(codes.Unauthenticated, err.Error()) - } - // Auth not required, continue without pubkey - return handler(ctx, req) + return nil, status.Error(codes.Unauthenticated, err.Error()) } // Add pubkey to context for handlers @@ -108,15 +123,24 @@ func NostrStreamInterceptor(opts *InterceptorOptions) grpc.StreamServerIntercept return handler(srv, ss) } + // Check if auth is required for this operation type + var authRequired bool + if isWriteMethod(info.FullMethod) { + authRequired = opts.Write.Enabled + } else { + authRequired = opts.Read.Enabled + } + + // If auth not required, skip validation + if !authRequired { + return handler(srv, ss) + } + // Extract and validate auth ctx := ss.Context() pubkey, err := validateAuthFromContext(ctx, info.FullMethod, opts) if err != nil { - if opts.Required { - return status.Error(codes.Unauthenticated, err.Error()) - } - // Auth not required, continue without pubkey - return handler(srv, ss) + return status.Error(codes.Unauthenticated, err.Error()) } // Wrap stream with authenticated context @@ -177,23 +201,25 @@ func validateAuthFromContext(ctx context.Context, method string, opts *Intercept // Extract pubkey pubkey := ExtractPubkey(event) - // Check whitelist based on operation type (all values are already normalized to hex) + // Get the operation config based on method type + var opConfig OperationAuthConfig if isWriteMethod(method) { - // Write operation - check write allowlist - if len(opts.AllowedNpubsWrite) > 0 { - if !contains(opts.AllowedNpubsWrite, pubkey) { - return "", fmt.Errorf("pubkey not authorized for write operations") - } - } + opConfig = opts.Write } else { - // Read operation - check read allowlist - if len(opts.AllowedNpubsRead) > 0 { - if !contains(opts.AllowedNpubsRead, pubkey) { - return "", fmt.Errorf("pubkey not authorized for read operations") + opConfig = opts.Read + } + + // Check whitelist if configured + 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") } } + // No whitelist or pubkey in whitelist - allow return pubkey, nil } -- cgit v1.2.3