You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
191 lines
4.6 KiB
191 lines
4.6 KiB
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 |
|
}
|
|
|