Phase 2: /api/amber/whoami token introspection for amber-api
amber-api must know whether an inbound request carries a valid Amber user
token before it spends an ffprobe on a caller-supplied URL. PocketBase
auth tokens are stateless JWTs signed with a per-user secret only
PocketBase holds, so offline verification is impossible by design — the
only correct check is to present the token and see if PocketBase accepts
it. $apis.requireAuth("users") does that before the handler runs, so
reaching the body IS the answer and the body is deliberately trivial.
Returns exactly {id, nsfwEnabled} and nothing else. A token-introspection
endpoint is a tempting place to leak account data (email, username,
profiles) and amber-api has no business knowing any of it — asserted.
Verified against a real PocketBase 0.39.6 running the repo's actual hooks
and migrations (the binary, no Docker needed): full suite green including
8 new assertions — anonymous 401, bogus token 401, valid 200, own id
returned, nsfwEnabled present, no extra keys, per-caller distinctness.
Also verified the integration this exists for: a genuine app token from
PocketBase authenticates a probe enqueue on amber-api, an invalid one
still 401s, and amber-api's 10-minute positive cache means a second
request costs no PocketBase round-trip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a98f52e3bc
commit
1d50e5a680
2 changed files with 62 additions and 0 deletions
36
pb_hooks/whoami.pb.js
Normal file
36
pb_hooks/whoami.pb.js
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
|
||||||
|
// Token introspection for amber-api (the facts service).
|
||||||
|
//
|
||||||
|
// GET /api/amber/whoami (auth: users) → { id, nsfwEnabled }
|
||||||
|
//
|
||||||
|
// WHY THIS EXISTS: amber-api has to know whether an inbound request carries a
|
||||||
|
// valid Amber user token before it will spend an ffprobe on a caller-supplied
|
||||||
|
// URL. PocketBase auth tokens are stateless JWTs signed with a per-user secret
|
||||||
|
// that only PocketBase holds, so **offline verification is impossible by
|
||||||
|
// design** — a third-party service cannot check one itself. The only correct
|
||||||
|
// check is to present the token here and see whether PocketBase accepts it,
|
||||||
|
// which `$apis.requireAuth("users")` does before this handler ever runs.
|
||||||
|
//
|
||||||
|
// So the handler body is deliberately trivial: reaching it *is* the answer. It
|
||||||
|
// returns the two facts amber-api needs and nothing else — no email, no
|
||||||
|
// username, no profile list. A token-introspection endpoint is a tempting place
|
||||||
|
// to leak account data, and amber-api has no business knowing any of it.
|
||||||
|
//
|
||||||
|
// `nsfwEnabled` is included because it's the same admin-only flag that gates
|
||||||
|
// adult release channels, and a future amber-api surface may need to scope
|
||||||
|
// results by it. It is read-only here.
|
||||||
|
//
|
||||||
|
// amber-api caches positives for ~10 minutes and negatives for ~30 seconds, so
|
||||||
|
// this route sees roughly one request per device per 10 minutes — cheap enough
|
||||||
|
// to sit in front of every probe lookup.
|
||||||
|
//
|
||||||
|
// NOTE: PocketBase runs each routerAdd handler in its own isolated JSVM, so
|
||||||
|
// anything a handler needs must be declared INSIDE it — file-scope helpers throw
|
||||||
|
// ReferenceError at request time. Nothing is hoisted here; keep it that way.
|
||||||
|
routerAdd("GET", "/api/amber/whoami", (e) => {
|
||||||
|
return e.json(200, {
|
||||||
|
id: e.auth.id,
|
||||||
|
nsfwEnabled: e.auth.getBool("nsfwEnabled"),
|
||||||
|
})
|
||||||
|
}, $apis.requireAuth("users"))
|
||||||
|
|
@ -244,5 +244,31 @@ st, r = req("POST", "/api/collections/watch_state/records", alice,
|
||||||
"updatedAt": CLIENT_TS})
|
"updatedAt": CLIENT_TS})
|
||||||
check("alice CANNOT attach a row to bob's profile", st, st in (400, 403, 404))
|
check("alice CANNOT attach a row to bob's profile", st, st in (400, 403, 404))
|
||||||
|
|
||||||
|
# --- /api/amber/whoami: token introspection for amber-api ---
|
||||||
|
# amber-api cannot verify a PocketBase JWT itself (stateless, signed with a
|
||||||
|
# per-user secret PocketBase alone holds), so it asks this route. These checks
|
||||||
|
# guard two things: that it fails closed for anyone unauthenticated, and that it
|
||||||
|
# leaks nothing beyond the two fields amber-api needs.
|
||||||
|
st, r = req("GET", "/api/amber/whoami")
|
||||||
|
check("whoami: anonymous → 401 (fails closed)", st, st == 401)
|
||||||
|
|
||||||
|
st, r = req("GET", "/api/amber/whoami", "Bearer totally-not-a-token")
|
||||||
|
check("whoami: bogus token → 401", st, st == 401)
|
||||||
|
|
||||||
|
st, r = req("GET", "/api/amber/whoami", alice)
|
||||||
|
check("whoami: valid token → 200", st, st == 200)
|
||||||
|
check("whoami returns the caller's own id", st, r.get("id") == ua["id"])
|
||||||
|
check("whoami returns nsfwEnabled", st, r.get("nsfwEnabled") is True)
|
||||||
|
# A token-introspection endpoint is a tempting place to leak account data;
|
||||||
|
# amber-api has no business knowing the email, username or profile list.
|
||||||
|
check("whoami leaks nothing else", st,
|
||||||
|
set(r.keys()) == {"id", "nsfwEnabled"})
|
||||||
|
|
||||||
|
# Per-caller, not "some authenticated user": a shared/cached answer here would
|
||||||
|
# let amber-api attribute one device's requests to another account.
|
||||||
|
st, r = req("GET", "/api/amber/whoami", bob)
|
||||||
|
check("whoami is per-caller (bob's token returns bob's id)", st, r.get("id") == ub["id"])
|
||||||
|
check("whoami ids are distinct per caller", st, ua["id"] != ub["id"])
|
||||||
|
|
||||||
print("\n" + ("ALL CHECKS PASSED" if allpass else "SOME CHECKS FAILED"))
|
print("\n" + ("ALL CHECKS PASSED" if allpass else "SOME CHECKS FAILED"))
|
||||||
sys.exit(0 if allpass else 1)
|
sys.exit(0 if allpass else 1)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue