# Nostr HTTP Authentication (NIP-98) This package implements [NIP-98](https://github.com/nostr-protocol/nips/blob/master/98.md) authentication for gRPC using the standard `credentials.PerRPCCredentials` interface. ## Overview NIP-98 provides HTTP authentication using Nostr event signatures instead of bearer tokens or OAuth2. It uses cryptographic signatures to prove the request came from a specific public key, without requiring passwords or centralized identity providers. ## Relationship to NIP-42 This implementation serves as **NIP-42 for gRPC**, with some important distinctions: ### For Reads (Queries/Subscriptions) **NIP-42 (WebSocket)**: - Authenticates the **connection** once via challenge-response - All subsequent REQ messages use the authenticated connection - Enables access control for private/restricted events **NIP-98 (gRPC - this implementation)**: - Authenticates **streaming RPCs** once at stream establishment - All events streamed over that authenticated connection - Same access control pattern as NIP-42 **Result**: Functionally identical! Both authenticate once and stream many events. ``` WebSocket + NIP-42: AUTH challenge → Subscribe → 1000 events (no re-auth) gRPC + NIP-98: Subscribe with auth → 1000 events (no re-auth) ``` ### For Writes (Publishing Events) **NIP-42 (WebSocket)**: - Not applicable - events are self-authenticating via `event.sig` - No connection-level auth for EVENT messages - Relay only verifies the event signature **NIP-98 (gRPC - this implementation)**: - Adds **relay access control** on top of event signatures - Proves who is **submitting** (NIP-98) vs who **created** (event.sig) the event - Enables use cases like: - Rate limiting per user - Allow-lists for relay access - Preventing spam/abuse (submitting scraped events) - Verifying submitter matches event author **Result**: Standardizes a relay access control pattern beyond base Nostr's self-authenticating events. ### Summary | Use Case | WebSocket (NIP-42) | gRPC (NIP-98) | |----------|-------------------|---------------| | **Read auth** | Challenge-response, once per connection | Per-RPC auth, once per stream | | **Read pattern** | ✅ Same: authenticate once, stream many | ✅ Same: authenticate once, stream many | | **Write auth** | ❌ N/A (events self-auth) | ✅ Optional relay access control | | **Overhead** | None after initial handshake | None for streams; minimal for unary calls | This implementation gives you NIP-42's read authentication pattern plus standardized relay access control for writes. ## How It Works ### Authentication Flow 1. **Client** creates a special kind 27235 event with: - `u` tag: Full request URI - `method` tag: HTTP method (GET, POST, etc.) or gRPC method name - `payload` tag (optional): SHA256 hash of the request body - `created_at`: Current Unix timestamp 2. **Client** signs the event with their private key 3. **Client** base64-encodes the event JSON and sends it in the `Authorization` header: ``` Authorization: Nostr ``` 4. **Server** validates the event: - Verifies the signature matches the pubkey - Checks the timestamp is recent (prevents replay attacks) - Verifies the `u` and `method` tags match the actual request - Optionally validates the payload hash 5. **Server** adds the validated pubkey to the request context for use by handlers ### Example Event ```json { "id": "9e1b6471f...", "pubkey": "79be667ef9dc...", "created_at": 1682327852, "kind": 27235, "tags": [ ["u", "https://api.example.com/nostr.v1.NostrRelay/PublishEvent"], ["method", "POST"], ["payload", "5c9e3a4d..."] ], "content": "", "sig": "d2d6e9f0..." } ``` ## Usage ### Client Side Use the `NostrCredentials` type with standard gRPC dial options: ```go import ( "google.golang.org/grpc" "northwest.io/muxstr/internal/auth" "northwest.io/muxstr/internal/nostr" ) // Generate or load your private key key, _ := nostr.GenerateKey() // Create credentials creds := auth.NewNostrCredentials(key) // Use with gRPC client conn, err := grpc.NewClient( "localhost:50051", grpc.WithPerRPCCredentials(creds), grpc.WithTransportCredentials(insecure.NewCredentials()), ) ``` The credentials automatically sign each request with a fresh NIP-98 event. ### Server Side Use the interceptors to validate incoming requests: ```go import ( "google.golang.org/grpc" "northwest.io/muxstr/internal/auth" ) // Create auth options authOpts := &auth.InterceptorOptions{ TimestampWindow: 60, // Accept events within 60 seconds Required: true, // Reject unauthenticated requests } // Create gRPC server with interceptors server := grpc.NewServer( grpc.UnaryInterceptor(auth.NostrUnaryInterceptor(authOpts)), grpc.StreamInterceptor(auth.NostrStreamInterceptor(authOpts)), ) ``` ### Accessing the Authenticated Pubkey In your handlers, retrieve the authenticated pubkey from the context: ```go func (s *Server) PublishEvent(ctx context.Context, req *pb.PublishEventRequest) (*pb.PublishEventResponse, error) { pubkey, ok := auth.PubkeyFromContext(ctx) if !ok { return nil, status.Error(codes.Unauthenticated, "no authentication") } // Verify the event was signed by the authenticated user if req.Event.Pubkey != pubkey { return nil, status.Error(codes.PermissionDenied, "event pubkey doesn't match auth") } // Process the event... } ``` ## Security Considerations ### Replay Protection Events include a `created_at` timestamp. The server validates that events are recent (within the configured `TimestampWindow`). This prevents replay attacks where an attacker intercepts and re-sends a valid auth event. ### Transport Security While NIP-98 provides authentication (proving who you are), it doesn't provide encryption. Use TLS/SSL to encrypt the connection and prevent eavesdropping. ```go // Client with TLS creds := credentials.NewClientTLSFromCert(nil, "") conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(nostrCreds), ) ``` ### Payload Validation The `payload` tag is optional but recommended for POST/PUT requests. When present, the server can verify the request body hasn't been tampered with: ```go authOpts := &auth.InterceptorOptions{ ValidatePayload: true, // Verify payload hash if present } ``` ## Configuration Options ### InterceptorOptions - **`TimestampWindow`**: Maximum age of events in seconds (default: 60) - **`Required`**: Whether to reject unauthenticated requests (default: false) - **`ValidatePayload`**: Whether to verify payload hash when present (default: false) - **`AllowedNpubs`**: Optional whitelist of allowed pubkeys (nil = allow all) - Config accepts npub format only (human-readable bech32) - Automatically normalized to hex format (computer-readable) at config load time ### NostrCredentials Options - **`IncludePayload`**: Whether to include payload hash in auth events (default: false) ## Benefits Over Traditional Auth 1. **No passwords**: Uses public key cryptography 2. **Decentralized**: No central identity provider 3. **Nostr ecosystem compatible**: - Same authentication as NIP-42 for WebSocket relays - Works with existing Nostr identities (npub/nsec) - Compatible with Nostr clients and tools 4. **Efficient for streaming**: Authenticate once per stream (like NIP-42 for WebSocket) 5. **Standard gRPC pattern**: Drop-in replacement for OAuth2/JWT using `credentials.PerRPCCredentials` 6. **Flexible access control**: Enables relay-level permissions beyond event signatures 7. **Key rotation**: Easy to rotate keys without server-side session management ## Compatibility This implementation follows the gRPC `credentials.PerRPCCredentials` interface, making it a drop-in replacement for OAuth2, JWT, or other auth mechanisms. It works with: - Standard gRPC clients (Go, Python, JS, etc.) - gRPC-Web and Connect protocol - All gRPC features (unary, streaming, metadata, etc.)