summaryrefslogtreecommitdiffstats
path: root/internal/auth/validation.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/auth/validation.go')
-rw-r--r--internal/auth/validation.go133
1 files changed, 133 insertions, 0 deletions
diff --git a/internal/auth/validation.go b/internal/auth/validation.go
new file mode 100644
index 0000000..11435ee
--- /dev/null
+++ b/internal/auth/validation.go
@@ -0,0 +1,133 @@
1package auth
2
3import (
4 "encoding/base64"
5 "encoding/json"
6 "fmt"
7 "strings"
8 "time"
9
10 "northwest.io/muxstr/internal/nostr"
11)
12
13// ValidationOptions configures how NIP-98 events are validated.
14type ValidationOptions struct {
15 // TimestampWindow is the maximum age of events in seconds
16 TimestampWindow int64
17
18 // ValidatePayload checks the payload hash if present
19 ValidatePayload bool
20
21 // ExpectedURI is the URI that should match the 'u' tag
22 ExpectedURI 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}
30
31// ParseAuthHeader extracts and decodes a NIP-98 event from an Authorization header.
32// Expected format: "Nostr <base64-encoded-event-json>"
33func ParseAuthHeader(header string) (*nostr.Event, error) {
34 if header == "" {
35 return nil, fmt.Errorf("empty authorization header")
36 }
37
38 // Check for "Nostr " prefix
39 if !strings.HasPrefix(header, "Nostr ") {
40 return nil, fmt.Errorf("invalid authorization header: must start with 'Nostr '")
41 }
42
43 // Extract base64 part
44 encoded := strings.TrimPrefix(header, "Nostr ")
45 if encoded == "" {
46 return nil, fmt.Errorf("empty authorization token")
47 }
48
49 // Decode base64
50 decoded, err := base64.StdEncoding.DecodeString(encoded)
51 if err != nil {
52 return nil, fmt.Errorf("invalid base64 encoding: %w", err)
53 }
54
55 // Unmarshal event
56 var event nostr.Event
57 if err := json.Unmarshal(decoded, &event); err != nil {
58 return nil, fmt.Errorf("invalid event JSON: %w", err)
59 }
60
61 return &event, nil
62}
63
64// ValidateAuthEvent validates a NIP-98 auth event according to the spec.
65func ValidateAuthEvent(event *nostr.Event, opts ValidationOptions) error {
66 // Check event kind
67 if event.Kind != 27235 {
68 return fmt.Errorf("invalid event kind: expected 27235, got %d", event.Kind)
69 }
70
71 // Verify signature
72 if !event.Verify() {
73 return fmt.Errorf("invalid event signature")
74 }
75
76 // Check timestamp (prevent replay attacks)
77 now := time.Now().Unix()
78 age := now - event.CreatedAt
79
80 if age < 0 {
81 return fmt.Errorf("event timestamp is in the future")
82 }
83
84 if opts.TimestampWindow > 0 && age > opts.TimestampWindow {
85 return fmt.Errorf("event too old: %d seconds (max %d)", age, opts.TimestampWindow)
86 }
87
88 // Validate 'u' tag (URL)
89 if opts.ExpectedURI != "" {
90 uTag := event.Tags.Find("u")
91 if uTag == nil {
92 return fmt.Errorf("missing 'u' tag in auth event")
93 }
94
95 eventURI := uTag.Value()
96 if eventURI != opts.ExpectedURI {
97 return fmt.Errorf("URI mismatch: expected %s, got %s", opts.ExpectedURI, eventURI)
98 }
99 }
100
101 // Validate 'method' tag
102 if opts.ExpectedMethod != "" {
103 methodTag := event.Tags.Find("method")
104 if methodTag == nil {
105 return fmt.Errorf("missing 'method' tag in auth event")
106 }
107
108 eventMethod := methodTag.Value()
109 if eventMethod != opts.ExpectedMethod {
110 return fmt.Errorf("method mismatch: expected %s, got %s", opts.ExpectedMethod, eventMethod)
111 }
112 }
113
114 // Validate payload hash if requested
115 if opts.ValidatePayload && opts.PayloadHash != "" {
116 payloadTag := event.Tags.Find("payload")
117 if payloadTag == nil {
118 return fmt.Errorf("missing 'payload' tag in auth event")
119 }
120
121 eventHash := payloadTag.Value()
122 if eventHash != opts.PayloadHash {
123 return fmt.Errorf("payload hash mismatch")
124 }
125 }
126
127 return nil
128}
129
130// ExtractPubkey returns the pubkey from a validated auth event.
131func ExtractPubkey(event *nostr.Event) string {
132 return event.PubKey
133}