summaryrefslogtreecommitdiffstats
path: root/internal/auth
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-02-14 10:17:54 -0800
committerbndw <ben@bdw.to>2026-02-14 10:17:54 -0800
commit702fa6c37b9f74e75404a0ea8e6f9023841143de (patch)
treea3d686b0252c0c889696dc745b37810af73e46b7 /internal/auth
parent606e0a3329a3534a00889eee19c25e7d432f7d2d (diff)
refactor: remove frivolous comments from auth and config
Removed ~100 lines of obvious comments that just repeated what the code does. Kept only comments that add clarity or valuable detail.
Diffstat (limited to 'internal/auth')
-rw-r--r--internal/auth/interceptor.go78
1 files changed, 10 insertions, 68 deletions
diff --git a/internal/auth/interceptor.go b/internal/auth/interceptor.go
index d394102..42c2688 100644
--- a/internal/auth/interceptor.go
+++ b/internal/auth/interceptor.go
@@ -11,52 +11,28 @@ import (
11 "google.golang.org/grpc/status" 11 "google.golang.org/grpc/status"
12) 12)
13 13
14// contextKey is a custom type for context keys to avoid collisions.
15type contextKey string 14type contextKey string
16 15
17const ( 16const (
18 // pubkeyContextKey is the key for storing the authenticated pubkey in context.
19 pubkeyContextKey contextKey = "nostr-pubkey" 17 pubkeyContextKey contextKey = "nostr-pubkey"
20) 18)
21 19
22// InterceptorOptions configures the authentication interceptor behavior.
23type InterceptorOptions struct { 20type InterceptorOptions struct {
24 // Read configures authentication for read operations (Subscribe, QueryEvents, etc.) 21 Read OperationAuthConfig
25 Read OperationAuthConfig 22 Write OperationAuthConfig
26
27 // Write configures authentication for write operations (PublishEvent, PublishBatch)
28 Write OperationAuthConfig
29
30 // TimestampWindow is the maximum age of auth events in seconds.
31 // Events older than this are rejected to prevent replay attacks.
32 // Default: 60 seconds
33 TimestampWindow int64 23 TimestampWindow int64
34
35 // ValidatePayload checks the payload hash tag if present.
36 // Default: false
37 ValidatePayload bool 24 ValidatePayload bool
38 25 SkipMethods []string
39 // SkipMethods is a list of gRPC methods that bypass authentication.
40 // Useful for public endpoints like health checks or relay info.
41 // Example: []string{"/grpc.health.v1.Health/Check"}
42 // Default: nil (authenticate all methods)
43 SkipMethods []string
44} 26}
45 27
46// OperationAuthConfig configures auth for a specific operation type. 28// OperationAuthConfig configures auth for read or write operations.
29// Three states: disabled (allow all), enabled with empty list (require auth),
30// enabled with npubs (whitelist only). Npubs normalized to hex at load time.
47type OperationAuthConfig struct { 31type OperationAuthConfig struct {
48 // Enabled determines if auth is required. 32 Enabled bool
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 33 AllowedNpubs []string
57} 34}
58 35
59// DefaultInterceptorOptions returns the default configuration.
60func DefaultInterceptorOptions() *InterceptorOptions { 36func DefaultInterceptorOptions() *InterceptorOptions {
61 return &InterceptorOptions{ 37 return &InterceptorOptions{
62 Read: OperationAuthConfig{ 38 Read: OperationAuthConfig{
@@ -73,19 +49,16 @@ func DefaultInterceptorOptions() *InterceptorOptions {
73 } 49 }
74} 50}
75 51
76// NostrUnaryInterceptor creates a gRPC unary interceptor for NIP-98 authentication.
77func NostrUnaryInterceptor(opts *InterceptorOptions) grpc.UnaryServerInterceptor { 52func NostrUnaryInterceptor(opts *InterceptorOptions) grpc.UnaryServerInterceptor {
78 if opts == nil { 53 if opts == nil {
79 opts = DefaultInterceptorOptions() 54 opts = DefaultInterceptorOptions()
80 } 55 }
81 56
82 return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 57 return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
83 // Check if this method should skip auth
84 if shouldSkipAuth(info.FullMethod, opts.SkipMethods) { 58 if shouldSkipAuth(info.FullMethod, opts.SkipMethods) {
85 return handler(ctx, req) 59 return handler(ctx, req)
86 } 60 }
87 61
88 // Check if auth is required for this operation type
89 var authRequired bool 62 var authRequired bool
90 if isWriteMethod(info.FullMethod) { 63 if isWriteMethod(info.FullMethod) {
91 authRequired = opts.Write.Enabled 64 authRequired = opts.Write.Enabled
@@ -93,37 +66,31 @@ func NostrUnaryInterceptor(opts *InterceptorOptions) grpc.UnaryServerInterceptor
93 authRequired = opts.Read.Enabled 66 authRequired = opts.Read.Enabled
94 } 67 }
95 68
96 // If auth not required, skip validation
97 if !authRequired { 69 if !authRequired {
98 return handler(ctx, req) 70 return handler(ctx, req)
99 } 71 }
100 72
101 // Extract and validate auth
102 pubkey, err := validateAuthFromContext(ctx, info.FullMethod, opts) 73 pubkey, err := validateAuthFromContext(ctx, info.FullMethod, opts)
103 if err != nil { 74 if err != nil {
104 return nil, status.Error(codes.Unauthenticated, err.Error()) 75 return nil, status.Error(codes.Unauthenticated, err.Error())
105 } 76 }
106 77
107 // Add pubkey to context for handlers
108 ctx = context.WithValue(ctx, pubkeyContextKey, pubkey) 78 ctx = context.WithValue(ctx, pubkeyContextKey, pubkey)
109 79
110 return handler(ctx, req) 80 return handler(ctx, req)
111 } 81 }
112} 82}
113 83
114// NostrStreamInterceptor creates a gRPC stream interceptor for NIP-98 authentication.
115func NostrStreamInterceptor(opts *InterceptorOptions) grpc.StreamServerInterceptor { 84func NostrStreamInterceptor(opts *InterceptorOptions) grpc.StreamServerInterceptor {
116 if opts == nil { 85 if opts == nil {
117 opts = DefaultInterceptorOptions() 86 opts = DefaultInterceptorOptions()
118 } 87 }
119 88
120 return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { 89 return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
121 // Check if this method should skip auth
122 if shouldSkipAuth(info.FullMethod, opts.SkipMethods) { 90 if shouldSkipAuth(info.FullMethod, opts.SkipMethods) {
123 return handler(srv, ss) 91 return handler(srv, ss)
124 } 92 }
125 93
126 // Check if auth is required for this operation type
127 var authRequired bool 94 var authRequired bool
128 if isWriteMethod(info.FullMethod) { 95 if isWriteMethod(info.FullMethod) {
129 authRequired = opts.Write.Enabled 96 authRequired = opts.Write.Enabled
@@ -131,19 +98,16 @@ func NostrStreamInterceptor(opts *InterceptorOptions) grpc.StreamServerIntercept
131 authRequired = opts.Read.Enabled 98 authRequired = opts.Read.Enabled
132 } 99 }
133 100
134 // If auth not required, skip validation
135 if !authRequired { 101 if !authRequired {
136 return handler(srv, ss) 102 return handler(srv, ss)
137 } 103 }
138 104
139 // Extract and validate auth
140 ctx := ss.Context() 105 ctx := ss.Context()
141 pubkey, err := validateAuthFromContext(ctx, info.FullMethod, opts) 106 pubkey, err := validateAuthFromContext(ctx, info.FullMethod, opts)
142 if err != nil { 107 if err != nil {
143 return status.Error(codes.Unauthenticated, err.Error()) 108 return status.Error(codes.Unauthenticated, err.Error())
144 } 109 }
145 110
146 // Wrap stream with authenticated context
147 wrappedStream := &authenticatedStream{ 111 wrappedStream := &authenticatedStream{
148 ServerStream: ss, 112 ServerStream: ss,
149 ctx: context.WithValue(ctx, pubkeyContextKey, pubkey), 113 ctx: context.WithValue(ctx, pubkeyContextKey, pubkey),
@@ -153,8 +117,7 @@ func NostrStreamInterceptor(opts *InterceptorOptions) grpc.StreamServerIntercept
153 } 117 }
154} 118}
155 119
156// authenticatedStream wraps a ServerStream with an authenticated context. 120type authenticatedStream struct{
157type authenticatedStream struct {
158 grpc.ServerStream 121 grpc.ServerStream
159 ctx context.Context 122 ctx context.Context
160} 123}
@@ -163,45 +126,34 @@ func (s *authenticatedStream) Context() context.Context {
163 return s.ctx 126 return s.ctx
164} 127}
165 128
166// validateAuthFromContext extracts and validates the NIP-98 auth event from the context.
167func validateAuthFromContext(ctx context.Context, method string, opts *InterceptorOptions) (string, error) { 129func validateAuthFromContext(ctx context.Context, method string, opts *InterceptorOptions) (string, error) {
168 // Extract metadata from context
169 md, ok := metadata.FromIncomingContext(ctx) 130 md, ok := metadata.FromIncomingContext(ctx)
170 if !ok { 131 if !ok {
171 return "", fmt.Errorf("missing metadata") 132 return "", fmt.Errorf("missing metadata")
172 } 133 }
173 134
174 // Get authorization header
175 authHeaders := md.Get("authorization") 135 authHeaders := md.Get("authorization")
176 if len(authHeaders) == 0 { 136 if len(authHeaders) == 0 {
177 return "", fmt.Errorf("missing authorization header") 137 return "", fmt.Errorf("missing authorization header")
178 } 138 }
179 139
180 authHeader := authHeaders[0] 140 event, err := ParseAuthHeader(authHeaders[0])
181
182 // Parse the auth event
183 event, err := ParseAuthHeader(authHeader)
184 if err != nil { 141 if err != nil {
185 return "", fmt.Errorf("invalid auth header: %w", err) 142 return "", fmt.Errorf("invalid auth header: %w", err)
186 } 143 }
187 144
188 // Validate the event
189 validationOpts := ValidationOptions{ 145 validationOpts := ValidationOptions{
190 TimestampWindow: opts.TimestampWindow, 146 TimestampWindow: opts.TimestampWindow,
191 ValidatePayload: opts.ValidatePayload, 147 ValidatePayload: opts.ValidatePayload,
192 ExpectedMethod: "POST", // gRPC always uses POST 148 ExpectedMethod: "POST",
193 // Note: We don't validate URI here because the full URI isn't easily
194 // available in the interceptor context. The method name is validated instead.
195 } 149 }
196 150
197 if err := ValidateAuthEvent(event, validationOpts); err != nil { 151 if err := ValidateAuthEvent(event, validationOpts); err != nil {
198 return "", fmt.Errorf("invalid auth event: %w", err) 152 return "", fmt.Errorf("invalid auth event: %w", err)
199 } 153 }
200 154
201 // Extract pubkey
202 pubkey := ExtractPubkey(event) 155 pubkey := ExtractPubkey(event)
203 156
204 // Get the operation config based on method type
205 var opConfig OperationAuthConfig 157 var opConfig OperationAuthConfig
206 if isWriteMethod(method) { 158 if isWriteMethod(method) {
207 opConfig = opts.Write 159 opConfig = opts.Write
@@ -209,7 +161,6 @@ func validateAuthFromContext(ctx context.Context, method string, opts *Intercept
209 opConfig = opts.Read 161 opConfig = opts.Read
210 } 162 }
211 163
212 // Check whitelist if configured
213 if len(opConfig.AllowedNpubs) > 0 { 164 if len(opConfig.AllowedNpubs) > 0 {
214 if !contains(opConfig.AllowedNpubs, pubkey) { 165 if !contains(opConfig.AllowedNpubs, pubkey) {
215 if isWriteMethod(method) { 166 if isWriteMethod(method) {
@@ -219,18 +170,13 @@ func validateAuthFromContext(ctx context.Context, method string, opts *Intercept
219 } 170 }
220 } 171 }
221 172
222 // No whitelist or pubkey in whitelist - allow
223 return pubkey, nil 173 return pubkey, nil
224} 174}
225 175
226// isWriteMethod determines if a gRPC method is a write operation.
227// Write: PublishEvent, PublishBatch
228// Read: Subscribe, Unsubscribe, QueryEvents, CountEvents
229func isWriteMethod(method string) bool { 176func isWriteMethod(method string) bool {
230 return strings.Contains(method, "/PublishEvent") || strings.Contains(method, "/PublishBatch") 177 return strings.Contains(method, "/PublishEvent") || strings.Contains(method, "/PublishBatch")
231} 178}
232 179
233// shouldSkipAuth checks if a method should bypass authentication.
234func shouldSkipAuth(method string, skipMethods []string) bool { 180func shouldSkipAuth(method string, skipMethods []string) bool {
235 for _, skip := range skipMethods { 181 for _, skip := range skipMethods {
236 if skip == method { 182 if skip == method {
@@ -240,7 +186,6 @@ func shouldSkipAuth(method string, skipMethods []string) bool {
240 return false 186 return false
241} 187}
242 188
243// contains checks if a slice contains a string.
244func contains(slice []string, item string) bool { 189func contains(slice []string, item string) bool {
245 for _, s := range slice { 190 for _, s := range slice {
246 if s == item { 191 if s == item {
@@ -250,14 +195,11 @@ func contains(slice []string, item string) bool {
250 return false 195 return false
251} 196}
252 197
253// PubkeyFromContext retrieves the authenticated pubkey from the context.
254// Returns the pubkey and true if authentication was successful, or empty string and false otherwise.
255func PubkeyFromContext(ctx context.Context) (string, bool) { 198func PubkeyFromContext(ctx context.Context) (string, bool) {
256 pubkey, ok := ctx.Value(pubkeyContextKey).(string) 199 pubkey, ok := ctx.Value(pubkeyContextKey).(string)
257 return pubkey, ok 200 return pubkey, ok
258} 201}
259 202
260// RequireAuth is a helper that extracts the pubkey and returns an error if not authenticated.
261func RequireAuth(ctx context.Context) (string, error) { 203func RequireAuth(ctx context.Context) (string, error) {
262 pubkey, ok := PubkeyFromContext(ctx) 204 pubkey, ok := PubkeyFromContext(ctx)
263 if !ok || pubkey == "" { 205 if !ok || pubkey == "" {