From 756c325223ef744b476ade565cb1970c7717d053 Mon Sep 17 00:00:00 2001 From: bndw Date: Sat, 14 Feb 2026 08:39:37 -0800 Subject: feat: implement NIP-98 HTTP auth for gRPC Add comprehensive NIP-98 authentication support following the standard gRPC credentials.PerRPCCredentials pattern. Client-side: - NostrCredentials implements PerRPCCredentials interface - Automatically signs each request with kind 27235 event - Drop-in replacement for OAuth2/JWT in gRPC clients Server-side: - Unary and stream interceptors for validation - Extracts and validates NIP-98 events from Authorization headers - Configurable options (timestamp window, whitelists, skip methods) - Adds authenticated pubkey to request context Security features: - Replay protection via timestamp validation - Optional payload hash verification - Signature verification using schnorr - TLS requirement option Includes comprehensive test coverage and detailed README with usage examples and security considerations. --- internal/auth/README.md | 181 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 internal/auth/README.md (limited to 'internal/auth/README.md') diff --git a/internal/auth/README.md b/internal/auth/README.md new file mode 100644 index 0000000..adfe260 --- /dev/null +++ b/internal/auth/README.md @@ -0,0 +1,181 @@ +# 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.) -- cgit v1.2.3