summaryrefslogtreecommitdiffstats
path: root/internal/auth/interceptor.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/auth/interceptor.go')
-rw-r--r--internal/auth/interceptor.go118
1 files changed, 72 insertions, 46 deletions
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 (
21 21
22// InterceptorOptions configures the authentication interceptor behavior. 22// InterceptorOptions configures the authentication interceptor behavior.
23type InterceptorOptions struct { 23type InterceptorOptions struct {
24 // Read configures authentication for read operations (Subscribe, QueryEvents, etc.)
25 Read OperationAuthConfig
26
27 // Write configures authentication for write operations (PublishEvent, PublishBatch)
28 Write OperationAuthConfig
29
24 // TimestampWindow is the maximum age of auth events in seconds. 30 // TimestampWindow is the maximum age of auth events in seconds.
25 // Events older than this are rejected to prevent replay attacks. 31 // Events older than this are rejected to prevent replay attacks.
26 // Default: 60 seconds 32 // Default: 60 seconds
27 TimestampWindow int64 33 TimestampWindow int64
28 34
29 // Required determines whether authentication is mandatory.
30 // If true, requests without valid auth are rejected.
31 // If false, unauthenticated requests are allowed (pubkey will be empty).
32 // Default: false
33 Required bool
34
35 // ValidatePayload checks the payload hash tag if present. 35 // ValidatePayload checks the payload hash tag if present.
36 // Default: false 36 // Default: false
37 ValidatePayload bool 37 ValidatePayload bool
38 38
39 // AllowedNpubsRead is an optional whitelist of allowed pubkeys for read operations (hex format).
40 // Config accepts npub format only, normalized to hex at load time.
41 // If nil or empty, all valid signatures are accepted for reads.
42 // Default: nil (allow all)
43 AllowedNpubsRead []string
44
45 // AllowedNpubsWrite is an optional whitelist of allowed pubkeys for write operations (hex format).
46 // Config accepts npub format only, normalized to hex at load time.
47 // If nil or empty, all valid signatures are accepted for writes.
48 // Default: nil (allow all)
49 AllowedNpubsWrite []string
50
51 // SkipMethods is a list of gRPC methods that bypass authentication. 39 // SkipMethods is a list of gRPC methods that bypass authentication.
52 // Useful for public endpoints like health checks or relay info. 40 // Useful for public endpoints like health checks or relay info.
53 // Example: []string{"/nostr.v1.NostrRelay/QueryEvents"} 41 // Example: []string{"/grpc.health.v1.Health/Check"}
54 // Default: nil (authenticate all methods) 42 // Default: nil (authenticate all methods)
55 SkipMethods []string 43 SkipMethods []string
56} 44}
57 45
46// OperationAuthConfig configures auth for a specific operation type.
47type OperationAuthConfig struct {
48 // Enabled determines if auth is required.
49 // false = no auth, allow all
50 // true = auth required
51 Enabled bool
52
53 // AllowedNpubs is an optional whitelist (hex format, normalized from npub at config load).
54 // If Enabled=true && AllowedNpubs=[]: any valid signature accepted
55 // If Enabled=true && AllowedNpubs=[...]: only whitelisted npubs accepted
56 AllowedNpubs []string
57}
58
58// DefaultInterceptorOptions returns the default configuration. 59// DefaultInterceptorOptions returns the default configuration.
59func DefaultInterceptorOptions() *InterceptorOptions { 60func DefaultInterceptorOptions() *InterceptorOptions {
60 return &InterceptorOptions{ 61 return &InterceptorOptions{
61 TimestampWindow: 60, 62 Read: OperationAuthConfig{
62 Required: false, 63 Enabled: false,
63 ValidatePayload: false, 64 AllowedNpubs: nil,
64 AllowedNpubsRead: nil, 65 },
65 AllowedNpubsWrite: nil, 66 Write: OperationAuthConfig{
66 SkipMethods: nil, 67 Enabled: false,
68 AllowedNpubs: nil,
69 },
70 TimestampWindow: 60,
71 ValidatePayload: false,
72 SkipMethods: nil,
67 } 73 }
68} 74}
69 75
@@ -79,14 +85,23 @@ func NostrUnaryInterceptor(opts *InterceptorOptions) grpc.UnaryServerInterceptor
79 return handler(ctx, req) 85 return handler(ctx, req)
80 } 86 }
81 87
88 // Check if auth is required for this operation type
89 var authRequired bool
90 if isWriteMethod(info.FullMethod) {
91 authRequired = opts.Write.Enabled
92 } else {
93 authRequired = opts.Read.Enabled
94 }
95
96 // If auth not required, skip validation
97 if !authRequired {
98 return handler(ctx, req)
99 }
100
82 // Extract and validate auth 101 // Extract and validate auth
83 pubkey, err := validateAuthFromContext(ctx, info.FullMethod, opts) 102 pubkey, err := validateAuthFromContext(ctx, info.FullMethod, opts)
84 if err != nil { 103 if err != nil {
85 if opts.Required { 104 return nil, status.Error(codes.Unauthenticated, err.Error())
86 return nil, status.Error(codes.Unauthenticated, err.Error())
87 }
88 // Auth not required, continue without pubkey
89 return handler(ctx, req)
90 } 105 }
91 106
92 // Add pubkey to context for handlers 107 // Add pubkey to context for handlers
@@ -108,15 +123,24 @@ func NostrStreamInterceptor(opts *InterceptorOptions) grpc.StreamServerIntercept
108 return handler(srv, ss) 123 return handler(srv, ss)
109 } 124 }
110 125
126 // Check if auth is required for this operation type
127 var authRequired bool
128 if isWriteMethod(info.FullMethod) {
129 authRequired = opts.Write.Enabled
130 } else {
131 authRequired = opts.Read.Enabled
132 }
133
134 // If auth not required, skip validation
135 if !authRequired {
136 return handler(srv, ss)
137 }
138
111 // Extract and validate auth 139 // Extract and validate auth
112 ctx := ss.Context() 140 ctx := ss.Context()
113 pubkey, err := validateAuthFromContext(ctx, info.FullMethod, opts) 141 pubkey, err := validateAuthFromContext(ctx, info.FullMethod, opts)
114 if err != nil { 142 if err != nil {
115 if opts.Required { 143 return status.Error(codes.Unauthenticated, err.Error())
116 return status.Error(codes.Unauthenticated, err.Error())
117 }
118 // Auth not required, continue without pubkey
119 return handler(srv, ss)
120 } 144 }
121 145
122 // Wrap stream with authenticated context 146 // Wrap stream with authenticated context
@@ -177,23 +201,25 @@ func validateAuthFromContext(ctx context.Context, method string, opts *Intercept
177 // Extract pubkey 201 // Extract pubkey
178 pubkey := ExtractPubkey(event) 202 pubkey := ExtractPubkey(event)
179 203
180 // Check whitelist based on operation type (all values are already normalized to hex) 204 // Get the operation config based on method type
205 var opConfig OperationAuthConfig
181 if isWriteMethod(method) { 206 if isWriteMethod(method) {
182 // Write operation - check write allowlist 207 opConfig = opts.Write
183 if len(opts.AllowedNpubsWrite) > 0 {
184 if !contains(opts.AllowedNpubsWrite, pubkey) {
185 return "", fmt.Errorf("pubkey not authorized for write operations")
186 }
187 }
188 } else { 208 } else {
189 // Read operation - check read allowlist 209 opConfig = opts.Read
190 if len(opts.AllowedNpubsRead) > 0 { 210 }
191 if !contains(opts.AllowedNpubsRead, pubkey) { 211
192 return "", fmt.Errorf("pubkey not authorized for read operations") 212 // Check whitelist if configured
213 if len(opConfig.AllowedNpubs) > 0 {
214 if !contains(opConfig.AllowedNpubs, pubkey) {
215 if isWriteMethod(method) {
216 return "", fmt.Errorf("pubkey not authorized for write operations")
193 } 217 }
218 return "", fmt.Errorf("pubkey not authorized for read operations")
194 } 219 }
195 } 220 }
196 221
222 // No whitelist or pubkey in whitelist - allow
197 return pubkey, nil 223 return pubkey, nil
198} 224}
199 225