summaryrefslogtreecommitdiffstats
path: root/internal/auth
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-02-14 12:03:21 -0800
committerbndw <ben@bdw.to>2026-02-14 12:03:21 -0800
commit4fc493e6d8cc20137f920f8647e39fc5051bb245 (patch)
tree69055d7da89ca909e33c66de7a883fdbe2ccbb97 /internal/auth
parent3e0ddc90c8f4ae7658cc07fb183aa0a7ecc338b7 (diff)
refactor: remove frivolous comments from auth validation/credentials
Also removed internal/nostr package - now using northwest.io/nostr library.
Diffstat (limited to 'internal/auth')
-rw-r--r--internal/auth/auth_test.go2
-rw-r--r--internal/auth/credentials.go35
-rw-r--r--internal/auth/validation.go33
3 files changed, 9 insertions, 61 deletions
diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go
index bcbb4a3..7b0fa13 100644
--- a/internal/auth/auth_test.go
+++ b/internal/auth/auth_test.go
@@ -8,7 +8,7 @@ import (
8 "time" 8 "time"
9 9
10 "google.golang.org/grpc/metadata" 10 "google.golang.org/grpc/metadata"
11 "northwest.io/muxstr/internal/nostr" 11 "northwest.io/nostr"
12) 12)
13 13
14func TestNostrCredentials(t *testing.T) { 14func TestNostrCredentials(t *testing.T) {
diff --git a/internal/auth/credentials.go b/internal/auth/credentials.go
index c558653..8e8aae8 100644
--- a/internal/auth/credentials.go
+++ b/internal/auth/credentials.go
@@ -8,20 +8,15 @@ import (
8 "fmt" 8 "fmt"
9 "time" 9 "time"
10 10
11 "northwest.io/muxstr/internal/nostr" 11 "northwest.io/nostr"
12) 12)
13 13
14// NostrCredentials implements credentials.PerRPCCredentials for NIP-98 authentication. 14// NostrCredentials implements credentials.PerRPCCredentials for NIP-98 auth.
15// It automatically signs each gRPC request with a Nostr event (kind 27235) and
16// attaches it to the Authorization header.
17type NostrCredentials struct { 15type NostrCredentials struct {
18 key *nostr.Key 16 key *nostr.Key
19 includePayload bool 17 includePayload bool
20} 18}
21 19
22// NewNostrCredentials creates credentials using the provided key.
23// Each RPC call will be authenticated with a freshly signed NIP-98 event.
24// The key must have a private key (CanSign() must return true).
25func NewNostrCredentials(key *nostr.Key) *NostrCredentials { 20func NewNostrCredentials(key *nostr.Key) *NostrCredentials {
26 return &NostrCredentials{ 21 return &NostrCredentials{
27 key: key, 22 key: key,
@@ -29,8 +24,6 @@ func NewNostrCredentials(key *nostr.Key) *NostrCredentials {
29 } 24 }
30} 25}
31 26
32// NewNostrCredentialsWithPayload creates credentials that include payload hashes.
33// When enabled, a SHA256 hash of the request body is included in the auth event.
34func NewNostrCredentialsWithPayload(key *nostr.Key) *NostrCredentials { 27func NewNostrCredentialsWithPayload(key *nostr.Key) *NostrCredentials {
35 return &NostrCredentials{ 28 return &NostrCredentials{
36 key: key, 29 key: key,
@@ -38,39 +31,26 @@ func NewNostrCredentialsWithPayload(key *nostr.Key) *NostrCredentials {
38 } 31 }
39} 32}
40 33
41// GetRequestMetadata implements credentials.PerRPCCredentials.
42// It creates and signs a NIP-98 auth event for each request.
43func (n *NostrCredentials) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { 34func (n *NostrCredentials) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
44 if len(uri) == 0 { 35 if len(uri) == 0 {
45 return nil, fmt.Errorf("no URI provided") 36 return nil, fmt.Errorf("no URI provided")
46 } 37 }
47 38
48 // Create kind 27235 event (NIP-98 HTTP Auth)
49 event := &nostr.Event{ 39 event := &nostr.Event{
50 PubKey: n.key.Public(), 40 PubKey: n.key.Public(),
51 CreatedAt: time.Now().Unix(), 41 CreatedAt: time.Now().Unix(),
52 Kind: 27235, // NIP-98 HTTP Auth 42 Kind: 27235,
53 Tags: nostr.Tags{}, 43 Tags: nostr.Tags{},
54 Content: "", 44 Content: "",
55 } 45 }
56 46
57 // Add URL tag
58 event.Tags = append(event.Tags, nostr.Tag{"u", uri[0]}) 47 event.Tags = append(event.Tags, nostr.Tag{"u", uri[0]})
59
60 // Add method tag - default to POST for gRPC
61 // The URI contains the method name, e.g., /nostr.v1.NostrRelay/PublishEvent
62 event.Tags = append(event.Tags, nostr.Tag{"method", "POST"}) 48 event.Tags = append(event.Tags, nostr.Tag{"method", "POST"})
63 49
64 // TODO: Add payload hash if includePayload is true
65 // This requires access to the request body, which isn't available in GetRequestMetadata
66 // We could use a context key to pass the payload hash from the application
67
68 // Sign the event
69 if err := n.key.Sign(event); err != nil { 50 if err := n.key.Sign(event); err != nil {
70 return nil, fmt.Errorf("failed to sign auth event: %w", err) 51 return nil, fmt.Errorf("failed to sign auth event: %w", err)
71 } 52 }
72 53
73 // Encode event as base64 JSON
74 eventJSON, err := json.Marshal(event) 54 eventJSON, err := json.Marshal(event)
75 if err != nil { 55 if err != nil {
76 return nil, fmt.Errorf("failed to marshal auth event: %w", err) 56 return nil, fmt.Errorf("failed to marshal auth event: %w", err)
@@ -83,33 +63,24 @@ func (n *NostrCredentials) GetRequestMetadata(ctx context.Context, uri ...string
83 }, nil 63 }, nil
84} 64}
85 65
86// RequireTransportSecurity implements credentials.PerRPCCredentials.
87// Returns false to allow usage over insecure connections (for development).
88// In production, use TLS and set this to true.
89func (n *NostrCredentials) RequireTransportSecurity() bool { 66func (n *NostrCredentials) RequireTransportSecurity() bool {
90 return false 67 return false
91} 68}
92 69
93// SetRequireTLS configures whether TLS is required.
94// When true, the credentials will only work over TLS connections.
95type NostrCredentialsWithTLS struct { 70type NostrCredentialsWithTLS struct {
96 *NostrCredentials 71 *NostrCredentials
97} 72}
98 73
99// NewNostrCredentialsWithTLS creates credentials that require TLS.
100func NewNostrCredentialsWithTLS(key *nostr.Key) *NostrCredentialsWithTLS { 74func NewNostrCredentialsWithTLS(key *nostr.Key) *NostrCredentialsWithTLS {
101 return &NostrCredentialsWithTLS{ 75 return &NostrCredentialsWithTLS{
102 NostrCredentials: NewNostrCredentials(key), 76 NostrCredentials: NewNostrCredentials(key),
103 } 77 }
104} 78}
105 79
106// RequireTransportSecurity returns true to enforce TLS.
107func (n *NostrCredentialsWithTLS) RequireTransportSecurity() bool { 80func (n *NostrCredentialsWithTLS) RequireTransportSecurity() bool {
108 return true 81 return true
109} 82}
110 83
111// HashPayload creates a SHA256 hash of the payload for inclusion in auth events.
112// This can be used to verify request integrity.
113func HashPayload(payload []byte) string { 84func HashPayload(payload []byte) string {
114 hash := sha256.Sum256(payload) 85 hash := sha256.Sum256(payload)
115 return fmt.Sprintf("%x", hash) 86 return fmt.Sprintf("%x", hash)
diff --git a/internal/auth/validation.go b/internal/auth/validation.go
index 11435ee..8b9d8a1 100644
--- a/internal/auth/validation.go
+++ b/internal/auth/validation.go
@@ -7,52 +7,37 @@ import (
7 "strings" 7 "strings"
8 "time" 8 "time"
9 9
10 "northwest.io/muxstr/internal/nostr" 10 "northwest.io/nostr"
11) 11)
12 12
13// ValidationOptions configures how NIP-98 events are validated.
14type ValidationOptions struct { 13type ValidationOptions struct {
15 // TimestampWindow is the maximum age of events in seconds
16 TimestampWindow int64 14 TimestampWindow int64
17
18 // ValidatePayload checks the payload hash if present
19 ValidatePayload bool 15 ValidatePayload bool
20 16 ExpectedURI string
21 // ExpectedURI is the URI that should match the 'u' tag 17 ExpectedMethod string
22 ExpectedURI string 18 PayloadHash string
23
24 // ExpectedMethod is the method that should match the 'method' tag
25 ExpectedMethod string
26
27 // PayloadHash is the expected payload hash (if ValidatePayload is true)
28 PayloadHash string
29} 19}
30 20
31// ParseAuthHeader extracts and decodes a NIP-98 event from an Authorization header. 21// ParseAuthHeader parses "Nostr <base64-encoded-event-json>" format.
32// Expected format: "Nostr <base64-encoded-event-json>"
33func ParseAuthHeader(header string) (*nostr.Event, error) { 22func ParseAuthHeader(header string) (*nostr.Event, error) {
34 if header == "" { 23 if header == "" {
35 return nil, fmt.Errorf("empty authorization header") 24 return nil, fmt.Errorf("empty authorization header")
36 } 25 }
37 26
38 // Check for "Nostr " prefix
39 if !strings.HasPrefix(header, "Nostr ") { 27 if !strings.HasPrefix(header, "Nostr ") {
40 return nil, fmt.Errorf("invalid authorization header: must start with 'Nostr '") 28 return nil, fmt.Errorf("invalid authorization header: must start with 'Nostr '")
41 } 29 }
42 30
43 // Extract base64 part
44 encoded := strings.TrimPrefix(header, "Nostr ") 31 encoded := strings.TrimPrefix(header, "Nostr ")
45 if encoded == "" { 32 if encoded == "" {
46 return nil, fmt.Errorf("empty authorization token") 33 return nil, fmt.Errorf("empty authorization token")
47 } 34 }
48 35
49 // Decode base64
50 decoded, err := base64.StdEncoding.DecodeString(encoded) 36 decoded, err := base64.StdEncoding.DecodeString(encoded)
51 if err != nil { 37 if err != nil {
52 return nil, fmt.Errorf("invalid base64 encoding: %w", err) 38 return nil, fmt.Errorf("invalid base64 encoding: %w", err)
53 } 39 }
54 40
55 // Unmarshal event
56 var event nostr.Event 41 var event nostr.Event
57 if err := json.Unmarshal(decoded, &event); err != nil { 42 if err := json.Unmarshal(decoded, &event); err != nil {
58 return nil, fmt.Errorf("invalid event JSON: %w", err) 43 return nil, fmt.Errorf("invalid event JSON: %w", err)
@@ -61,19 +46,15 @@ func ParseAuthHeader(header string) (*nostr.Event, error) {
61 return &event, nil 46 return &event, nil
62} 47}
63 48
64// ValidateAuthEvent validates a NIP-98 auth event according to the spec.
65func ValidateAuthEvent(event *nostr.Event, opts ValidationOptions) error { 49func ValidateAuthEvent(event *nostr.Event, opts ValidationOptions) error {
66 // Check event kind
67 if event.Kind != 27235 { 50 if event.Kind != 27235 {
68 return fmt.Errorf("invalid event kind: expected 27235, got %d", event.Kind) 51 return fmt.Errorf("invalid event kind: expected 27235, got %d", event.Kind)
69 } 52 }
70 53
71 // Verify signature
72 if !event.Verify() { 54 if !event.Verify() {
73 return fmt.Errorf("invalid event signature") 55 return fmt.Errorf("invalid event signature")
74 } 56 }
75 57
76 // Check timestamp (prevent replay attacks)
77 now := time.Now().Unix() 58 now := time.Now().Unix()
78 age := now - event.CreatedAt 59 age := now - event.CreatedAt
79 60
@@ -85,7 +66,6 @@ func ValidateAuthEvent(event *nostr.Event, opts ValidationOptions) error {
85 return fmt.Errorf("event too old: %d seconds (max %d)", age, opts.TimestampWindow) 66 return fmt.Errorf("event too old: %d seconds (max %d)", age, opts.TimestampWindow)
86 } 67 }
87 68
88 // Validate 'u' tag (URL)
89 if opts.ExpectedURI != "" { 69 if opts.ExpectedURI != "" {
90 uTag := event.Tags.Find("u") 70 uTag := event.Tags.Find("u")
91 if uTag == nil { 71 if uTag == nil {
@@ -98,7 +78,6 @@ func ValidateAuthEvent(event *nostr.Event, opts ValidationOptions) error {
98 } 78 }
99 } 79 }
100 80
101 // Validate 'method' tag
102 if opts.ExpectedMethod != "" { 81 if opts.ExpectedMethod != "" {
103 methodTag := event.Tags.Find("method") 82 methodTag := event.Tags.Find("method")
104 if methodTag == nil { 83 if methodTag == nil {
@@ -111,7 +90,6 @@ func ValidateAuthEvent(event *nostr.Event, opts ValidationOptions) error {
111 } 90 }
112 } 91 }
113 92
114 // Validate payload hash if requested
115 if opts.ValidatePayload && opts.PayloadHash != "" { 93 if opts.ValidatePayload && opts.PayloadHash != "" {
116 payloadTag := event.Tags.Find("payload") 94 payloadTag := event.Tags.Find("payload")
117 if payloadTag == nil { 95 if payloadTag == nil {
@@ -127,7 +105,6 @@ func ValidateAuthEvent(event *nostr.Event, opts ValidationOptions) error {
127 return nil 105 return nil
128} 106}
129 107
130// ExtractPubkey returns the pubkey from a validated auth event.
131func ExtractPubkey(event *nostr.Event) string { 108func ExtractPubkey(event *nostr.Event) string {
132 return event.PubKey 109 return event.PubKey
133} 110}