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
|
package auth
import (
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"
"code.northwest.io/nostr"
)
type ValidationOptions struct {
TimestampWindow int64
ValidatePayload bool
ExpectedURI string
ExpectedMethod string
PayloadHash string
}
// ParseAuthHeader parses "Nostr <base64-encoded-event-json>" format.
func ParseAuthHeader(header string) (*nostr.Event, error) {
if header == "" {
return nil, fmt.Errorf("empty authorization header")
}
if !strings.HasPrefix(header, "Nostr ") {
return nil, fmt.Errorf("invalid authorization header: must start with 'Nostr '")
}
encoded := strings.TrimPrefix(header, "Nostr ")
if encoded == "" {
return nil, fmt.Errorf("empty authorization token")
}
decoded, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return nil, fmt.Errorf("invalid base64 encoding: %w", err)
}
var event nostr.Event
if err := json.Unmarshal(decoded, &event); err != nil {
return nil, fmt.Errorf("invalid event JSON: %w", err)
}
return &event, nil
}
func ValidateAuthEvent(event *nostr.Event, opts ValidationOptions) error {
if event.Kind != 27235 {
return fmt.Errorf("invalid event kind: expected 27235, got %d", event.Kind)
}
if !event.Verify() {
return fmt.Errorf("invalid event signature")
}
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)
}
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)
}
}
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)
}
}
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
}
func ExtractPubkey(event *nostr.Event) string {
return event.PubKey
}
|