192 lines
4.6 KiB
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
|
|
}
|