summaryrefslogtreecommitdiffstats
path: root/internal/auth/README.md
blob: df0de6acd5638fc077253acaea37e462357152f6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# 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 <base64-encoded-event>
   ```

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.)