feat(auth): X-Bn-Verified-Captcha trusted header + CaptchaVerified helper
The host verifies (and consumes) a request's cap-token against its stateful captcha server before wasm dispatch and stamps this header; guests enforce captcha by checking CaptchaVerified, fail closed. Included in AllTrustedHeaders so the host's strip-then-stamp loop makes it unforgeable, like the identity headers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
54716d705f
commit
9a4394a07c
@ -36,6 +36,13 @@ const (
|
|||||||
HeaderVerifiedPublicUserID = "X-Bn-Verified-Public-User-Id"
|
HeaderVerifiedPublicUserID = "X-Bn-Verified-Public-User-Id"
|
||||||
HeaderVerifiedPublicUsername = "X-Bn-Verified-Public-Username"
|
HeaderVerifiedPublicUsername = "X-Bn-Verified-Public-Username"
|
||||||
HeaderVerifiedPublicEmail = "X-Bn-Verified-Public-Email"
|
HeaderVerifiedPublicEmail = "X-Bn-Verified-Public-Email"
|
||||||
|
|
||||||
|
// Captcha proof. The host verifies (and consumes — replay-protected) the
|
||||||
|
// request's cap-token against the instance captcha server before dispatch
|
||||||
|
// and stamps "1" on success. A guest cannot hold the host's stateful
|
||||||
|
// verifier, so this header is the ONLY way to enforce captcha on a plugin
|
||||||
|
// HTTP endpoint; render the widget with the ninjatpl captcha tag.
|
||||||
|
HeaderVerifiedCaptcha = "X-Bn-Verified-Captcha"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AllTrustedHeaders lists every trusted identity header, canonical form. The
|
// AllTrustedHeaders lists every trusted identity header, canonical form. The
|
||||||
@ -49,9 +56,16 @@ func AllTrustedHeaders() []string {
|
|||||||
HeaderVerifiedPublicUserID,
|
HeaderVerifiedPublicUserID,
|
||||||
HeaderVerifiedPublicUsername,
|
HeaderVerifiedPublicUsername,
|
||||||
HeaderVerifiedPublicEmail,
|
HeaderVerifiedPublicEmail,
|
||||||
|
HeaderVerifiedCaptcha,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CaptchaVerified reports whether the host verified a captcha token for this
|
||||||
|
// request. Fail closed: enforcing handlers must reject when this is false.
|
||||||
|
func CaptchaVerified(h http.Header) bool {
|
||||||
|
return h.Get(HeaderVerifiedCaptcha) == "1"
|
||||||
|
}
|
||||||
|
|
||||||
// ContextFromTrustedHeaders rebuilds the request's auth context from the host's
|
// ContextFromTrustedHeaders rebuilds the request's auth context from the host's
|
||||||
// verified identity headers. Trust model: see the const block above — these
|
// verified identity headers. Trust model: see the const block above — these
|
||||||
// headers are host-controlled, so no token parsing or signature check happens
|
// headers are host-controlled, so no token parsing or signature check happens
|
||||||
|
|||||||
@ -80,3 +80,27 @@ func TestTrustedHeaderMiddleware(t *testing.T) {
|
|||||||
t.Fatalf("middleware did not install principal: %+v", got)
|
t.Fatalf("middleware did not install principal: %+v", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCaptchaVerifiedReadsTrustedHeader(t *testing.T) {
|
||||||
|
h := http.Header{}
|
||||||
|
if CaptchaVerified(h) {
|
||||||
|
t.Fatal("CaptchaVerified() = true with no header, want false")
|
||||||
|
}
|
||||||
|
h.Set(HeaderVerifiedCaptcha, "1")
|
||||||
|
if !CaptchaVerified(h) {
|
||||||
|
t.Fatal("CaptchaVerified() = false with host-stamped header, want true")
|
||||||
|
}
|
||||||
|
h.Set(HeaderVerifiedCaptcha, "true")
|
||||||
|
if CaptchaVerified(h) {
|
||||||
|
t.Fatal(`CaptchaVerified() = true for non-"1" value, want false`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAllTrustedHeadersIncludesCaptcha(t *testing.T) {
|
||||||
|
for _, h := range AllTrustedHeaders() {
|
||||||
|
if h == HeaderVerifiedCaptcha {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatalf("AllTrustedHeaders() missing %s — hosts strip-then-stamp from this list, so omitting it makes the header client-forgeable", HeaderVerifiedCaptcha)
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user