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
|
package auth
import (
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"
"northwest.io/muxstr/internal/nostr"
)
// ValidationOptions configures how NIP-98 events are validated.
type ValidationOptions struct {
// TimestampWindow is the maximum age of events in seconds
TimestampWindow int64
// ValidatePayload checks the payload hash if present
ValidatePayload bool
// ExpectedURI is the URI that should match the 'u' tag
ExpectedURI string
// ExpectedMethod is the method that should match the 'method' tag
ExpectedMethod string
// PayloadHash is the expected payload hash (if ValidatePayload is true)
PayloadHash string
}
// ParseAuthHeader extracts and decodes a NIP-98 event from an Authorization header.
// Expected format: "Nostr <base64-encoded-event-json>"
func ParseAuthHeader(header string) (*nostr.Event, error) {
if header == "" {
return nil, fmt.Errorf("empty authorization header")
}
// Check for "Nostr " prefix
if !strings.HasPrefix(header, "Nostr ") {
return nil, fmt.Errorf("invalid authorization header: must start with 'Nostr '")
}
// Extract base64 part
encoded := strings.TrimPrefix(header, "Nostr ")
if encoded == "" {
return nil, fmt.Errorf("empty authorization token")
}
// Decode base64
decoded, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return nil, fmt.Errorf("invalid base64 encoding: %w", err)
}
// Unmarshal event
var event nostr.Event
if err := json.Unmarshal(decoded, &event); err != nil {
return nil, fmt.Errorf("invalid event JSON: %w", err)
}
return &event, nil
}
// ValidateAuthEvent validates a NIP-98 auth event according to the spec.
func ValidateAuthEvent(event *nostr.Event, opts ValidationOptions) error {
// Check event kind
if event.Kind != 27235 {
return fmt.Errorf("invalid event kind: expected 27235, got %d", event.Kind)
}
// Verify signature
if !event.Verify() {
return fmt.Errorf("invalid event signature")
}
// Check timestamp (prevent replay attacks)
now := time.Now().Unix()
age := now - event.CreatedAt
if age < 0 {
return fmt.Errorf("event timestamp is in the future")
}
if opts.TimestampWindow > 0 && age > opts.TimestampWindow {
return fmt.Errorf("event too old: %d seconds (max %d)", age, opts.TimestampWindow)
}
// Validate 'u' tag (URL)
if opts.ExpectedURI != "" {
uTag := event.Tags.Find("u")
if uTag == nil {
return fmt.Errorf("missing 'u' tag in auth event")
}
eventURI := uTag.Value()
if eventURI != opts.ExpectedURI {
return fmt.Errorf("URI mismatch: expected %s, got %s", opts.ExpectedURI, eventURI)
}
}
// Validate 'method' tag
if opts.ExpectedMethod != "" {
methodTag := event.Tags.Find("method")
if methodTag == nil {
return fmt.Errorf("missing 'method' tag in auth event")
}
eventMethod := methodTag.Value()
if eventMethod != opts.ExpectedMethod {
return fmt.Errorf("method mismatch: expected %s, got %s", opts.ExpectedMethod, eventMethod)
}
}
// Validate payload hash if requested
if opts.ValidatePayload && opts.PayloadHash != "" {
payloadTag := event.Tags.Find("payload")
if payloadTag == nil {
return fmt.Errorf("missing 'payload' tag in auth event")
}
eventHash := payloadTag.Value()
if eventHash != opts.PayloadHash {
return fmt.Errorf("payload hash mismatch")
}
}
return nil
}
// ExtractPubkey returns the pubkey from a validated auth event.
func ExtractPubkey(event *nostr.Event) string {
return event.PubKey
}
|