validate.go raw
1 package httpauth
2
3 import (
4 "encoding/base64"
5 "fmt"
6 "net/http"
7 "strings"
8 "time"
9
10 "next.orly.dev/pkg/nostr/encoders/event"
11 "next.orly.dev/pkg/nostr/encoders/ints"
12 "next.orly.dev/pkg/nostr/encoders/kind"
13 "next.orly.dev/pkg/lol/chk"
14 "next.orly.dev/pkg/lol/errorf"
15 "next.orly.dev/pkg/lol/log"
16 )
17
18 var ErrMissingKey = fmt.Errorf(
19 "'%s' key missing from request header", HeaderKey,
20 )
21
22 // CheckAuth verifies a received http.Request has got a valid authentication
23 // event in it, with an optional specification for tolerance of before and
24 // after, and provides the public key that should be verified to be authorized
25 // to access the resource associated with the request.
26 func CheckAuth(r *http.Request, tolerance ...time.Duration) (
27 valid bool,
28 pubkey []byte, err error,
29 ) {
30 val := r.Header.Get(HeaderKey)
31 if val == "" {
32 err = ErrMissingKey
33 valid = true
34 return
35 }
36 if len(tolerance) == 0 {
37 tolerance = append(tolerance, time.Minute)
38 }
39 // log.I.S(tolerance)
40 if tolerance[0] == 0 {
41 tolerance[0] = time.Minute
42 }
43 tolerate := int64(tolerance[0] / time.Second)
44 log.T.C(func() string { return fmt.Sprintf("validating auth '%s'", val) })
45 switch {
46 case strings.HasPrefix(val, NIP98Prefix):
47 split := strings.Split(val, " ")
48 if len(split) == 1 {
49 err = errorf.E(
50 "missing nip-98 auth event from '%s' http header key: '%s'",
51 HeaderKey, val,
52 )
53 }
54 if len(split) > 2 {
55 err = errorf.E(
56 "extraneous content after second field space separated: %s",
57 val,
58 )
59 return
60 }
61 var evb []byte
62 if evb, err = base64.URLEncoding.DecodeString(split[1]); chk.E(err) {
63 return
64 }
65 ev := event.New()
66 var rem []byte
67 if rem, err = ev.Unmarshal(evb); chk.E(err) {
68 return
69 }
70 if len(rem) > 0 {
71 err = errorf.E("rem", rem)
72 return
73 }
74 // log.T.F("received http auth event:\n%s\n", ev.SerializeIndented())
75 // The kind MUST be 27235.
76 if ev.Kind != kind.HTTPAuth.K {
77 err = errorf.E(
78 "invalid kind %d %s in nip-98 http auth event, require %d %s",
79 ev.Kind, kind.GetString(ev.Kind), kind.HTTPAuth.K,
80 kind.HTTPAuth.Name(),
81 )
82 return
83 }
84 // if there is an expiration timestamp, check it supersedes the
85 // created_at for validity.
86 exp := ev.Tags.GetAll([]byte("expiration"))
87 if len(exp) > 1 {
88 err = errorf.E(
89 "more than one \"expiration\" tag found",
90 )
91 return
92 }
93 var expiring bool
94 if len(exp) == 1 {
95 ex := ints.New(0)
96 exp1 := exp[0]
97 if rem, err = ex.Unmarshal(exp1.Value()); chk.E(err) {
98 return
99 }
100 tn := time.Now().Unix()
101 if tn > ex.Int64()+tolerate {
102 err = errorf.E(
103 "HTTP auth event is expired %d time now %d",
104 tn, ex.Int64()+tolerate,
105 )
106 return
107 }
108 expiring = true
109 } else {
110 // The created_at timestamp MUST be within a reasonable time window
111 // (suggestion 60 seconds)
112 ts := ev.CreatedAt
113 tn := time.Now().Unix()
114 if ts < tn-tolerate || ts > tn+tolerate {
115 err = errorf.E(
116 "timestamp %d is more than %d seconds divergent from now %d",
117 ts, tolerate, tn,
118 )
119 return
120 }
121 }
122 ut := ev.Tags.GetAll([]byte("u"))
123 if len(ut) > 1 {
124 err = errorf.E(
125 "more than one \"u\" tag found",
126 )
127 return
128 }
129 // The u tag MUST be exactly the same as the absolute request URL
130 // (including query parameters).
131 proto := r.URL.Scheme
132 // if this came through a proxy, we need to get the protocol to match
133 // the event
134 if p := r.Header.Get("X-Forwarded-Proto"); p != "" {
135 proto = p
136 }
137 if proto == "" {
138 // Check if this is a direct TLS connection
139 if r.TLS != nil {
140 proto = "https"
141 } else {
142 proto = "http"
143 }
144 }
145 fullUrl := proto + "://" + r.Host + r.URL.RequestURI()
146 evUrl := string(ut[0].Value())
147 log.T.F("full URL: %s event u tag value: %s", fullUrl, evUrl)
148 if expiring {
149 // if it is expiring, the URL only needs to be the same prefix to
150 // allow its use with multiple endpoints.
151 if !strings.HasPrefix(fullUrl, evUrl) {
152 err = errorf.E(
153 "request URL %s is not prefixed with the u tag URL %s",
154 fullUrl, evUrl,
155 )
156 return
157 }
158 } else if fullUrl != evUrl {
159 err = errorf.E(
160 "request has URL %s but signed nip-98 event has url %s",
161 fullUrl, string(ut[0].Value()),
162 )
163 return
164 }
165 if !expiring {
166 // The method tag MUST be the same HTTP method used for the
167 // requested resource.
168 mt := ev.Tags.GetAll([]byte("method"))
169 if len(mt) != 1 {
170 err = errorf.E(
171 "more than one \"method\" tag found",
172 )
173 return
174 }
175 if !strings.EqualFold(string(mt[0].Value()), r.Method) {
176 err = errorf.E(
177 "request has method %s but event has method %s",
178 string(mt[0].Value()), r.Method,
179 )
180 return
181 }
182 }
183 if valid, err = ev.Verify(); chk.E(err) {
184 return
185 }
186 if !valid {
187 return
188 }
189 pubkey = ev.Pubkey
190 default:
191 err = errorf.E("invalid '%s' value: '%s'", HeaderKey, val)
192 return
193 }
194
195 return
196 }
197