# 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. ## 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) - **`AllowedPubkeys`**: Optional whitelist of allowed pubkeys (nil = allow all) ### 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. **Per-request auth**: Each request is independently authenticated 4. **Nostr compatible**: Works with existing Nostr identities and tools 5. **Standard pattern**: Uses industry-standard gRPC credentials interface 6. **Key rotation**: Easy to rotate keys without server-side updates ## 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.)