Files
next.orly.dev/pkg/protocol/httpauth/validate.go

192 lines
4.6 KiB
Go

package httpauth
import (
"encoding/base64"
"fmt"
"net/http"
"strings"
"time"
"lol.mleku.dev/chk"
"lol.mleku.dev/errorf"
"lol.mleku.dev/log"
"next.orly.dev/pkg/encoders/event"
"next.orly.dev/pkg/encoders/ints"
"next.orly.dev/pkg/encoders/kind"
)
var ErrMissingKey = fmt.Errorf(
"'%s' key missing from request header", HeaderKey,
)
// CheckAuth verifies a received http.Request has got a valid authentication
// event in it, with an optional specification for tolerance of before and
// after, and provides the public key that should be verified to be authorized
// to access the resource associated with the request.
func CheckAuth(r *http.Request, tolerance ...time.Duration) (
valid bool,
pubkey []byte, err error,
) {
val := r.Header.Get(HeaderKey)
if val == "" {
err = ErrMissingKey
valid = true
return
}
if len(tolerance) == 0 {
tolerance = append(tolerance, time.Minute)
}
// log.I.S(tolerance)
if tolerance[0] == 0 {
tolerance[0] = time.Minute
}
tolerate := int64(tolerance[0] / time.Second)
log.T.C(func() string { return fmt.Sprintf("validating auth '%s'", val) })
switch {
case strings.HasPrefix(val, NIP98Prefix):
split := strings.Split(val, " ")
if len(split) == 1 {
err = errorf.E(
"missing nip-98 auth event from '%s' http header key: '%s'",
HeaderKey, val,
)
}
if len(split) > 2 {
err = errorf.E(
"extraneous content after second field space separated: %s",
val,
)
return
}
var evb []byte
if evb, err = base64.URLEncoding.DecodeString(split[1]); chk.E(err) {
return
}
ev := event.New()
var rem []byte
if rem, err = ev.Unmarshal(evb); chk.E(err) {
return
}
if len(rem) > 0 {
err = errorf.E("rem", rem)
return
}
// log.T.F("received http auth event:\n%s\n", ev.SerializeIndented())
// The kind MUST be 27235.
if ev.Kind != kind.HTTPAuth.K {
err = errorf.E(
"invalid kind %d %s in nip-98 http auth event, require %d %s",
ev.Kind, kind.GetString(ev.Kind), kind.HTTPAuth.K,
kind.HTTPAuth.Name(),
)
return
}
// if there is an expiration timestamp, check it supersedes the
// created_at for validity.
exp := ev.Tags.GetAll([]byte("expiration"))
if len(exp) > 1 {
err = errorf.E(
"more than one \"expiration\" tag found",
)
return
}
var expiring bool
if len(exp) == 1 {
ex := ints.New(0)
exp1 := exp[0]
if rem, err = ex.Unmarshal(exp1.Value()); chk.E(err) {
return
}
tn := time.Now().Unix()
if tn > ex.Int64()+tolerate {
err = errorf.E(
"HTTP auth event is expired %d time now %d",
tn, ex.Int64()+tolerate,
)
return
}
expiring = true
} else {
// The created_at timestamp MUST be within a reasonable time window
// (suggestion 60 seconds)
ts := ev.CreatedAt
tn := time.Now().Unix()
if ts < tn-tolerate || ts > tn+tolerate {
err = errorf.E(
"timestamp %d is more than %d seconds divergent from now %d",
ts, tolerate, tn,
)
return
}
}
ut := ev.Tags.GetAll([]byte("u"))
if len(ut) > 1 {
err = errorf.E(
"more than one \"u\" tag found",
)
return
}
// The u tag MUST be exactly the same as the absolute request URL
// (including query parameters).
proto := r.URL.Scheme
// if this came through a proxy, we need to get the protocol to match
// the event
if p := r.Header.Get("X-Forwarded-Proto"); p != "" {
proto = p
}
if proto == "" {
proto = "http"
}
fullUrl := proto + "://" + r.Host + r.URL.RequestURI()
evUrl := string(ut[0].Value())
log.T.F("full URL: %s event u tag value: %s", fullUrl, evUrl)
if expiring {
// if it is expiring, the URL only needs to be the same prefix to
// allow its use with multiple endpoints.
if !strings.HasPrefix(fullUrl, evUrl) {
err = errorf.E(
"request URL %s is not prefixed with the u tag URL %s",
fullUrl, evUrl,
)
return
}
} else if fullUrl != evUrl {
err = errorf.E(
"request has URL %s but signed nip-98 event has url %s",
fullUrl, string(ut[0].Value()),
)
return
}
if !expiring {
// The method tag MUST be the same HTTP method used for the
// requested resource.
mt := ev.Tags.GetAll([]byte("method"))
if len(mt) != 1 {
err = errorf.E(
"more than one \"method\" tag found",
)
return
}
if !strings.EqualFold(string(mt[0].Value()), r.Method) {
err = errorf.E(
"request has method %s but event has method %s",
string(mt[0].Value()), r.Method,
)
return
}
}
if valid, err = ev.Verify(); chk.E(err) {
return
}
if !valid {
return
}
pubkey = ev.Pubkey
default:
err = errorf.E("invalid '%s' value: '%s'", HeaderKey, val)
return
}
return
}