Demo · HMAC-bound browser token

Pre-auth anti-abuse gate

Many of the most valuable APIs on a public site can't sit behind a login — address lookups, product search, quote calculators, AI chat widgets. They're how the user gets to the login. But "no login required" isn't the same as "safe to leave wide open" — most of them cost real money on every call.

Why an open API is a security problem, not just an availability one

You can't put these endpoints behind a session — they precede the session. But you can put a gate between "any HTTP client on the planet" and "a browser that came from your own site and proved it with one round-trip." That gate is what this demo is.

How the gate works

This page calls a tiny Go backend that mints an HMAC-bound token and an HttpOnly cookie. The frontend then makes lookups carrying both. The cookie auto-attaches; the token must be explicitly sent as X-CSRF-Token. Every part has a deliberate rate limit.

Pattern adapted from OWASP's Signed Double-Submit Cookie CSRF defence (cookie nonce + signed header token, validated with HMAC). Repurposed here pre-authentication as a cost-protection gate for open APIs, addressing OWASP API4:2023 — Unrestricted Resource Consumption.

Backend config (returned by /api/v1/mint)

Token TTL
Max uses / token
Mint burst
Mint refill

Current token

(minting on load…)

Format: nonce | expiry_unix | hmac_sha256(server_secret, "nonce|expiry"). The nonce half is also sitting in your cookie jar as __sessionHttpOnly, so this JS can't read it; only the server can compare it with the header. (Named __session because Firebase Hosting strips every cookie except that one before reaching Cloud Run.)

Call the protected endpoint

(waiting for token…)

Try to bypass

Open a terminal and try these.

No cookie, no token — fails at the validate step:

curl -i http://localhost:5050/api/v1/lookup

Forged token, no cookie:

curl -i -H "X-CSRF-Token: abc|9999999999|fakehash" http://localhost:5050/api/v1/lookup

The legitimate two-step (works, but costs an entry-API round-trip per token-burst):

BASE=http://localhost:5050
TOKEN=$(curl -s -c /tmp/jar "$BASE/api/v1/mint" | sed -nE 's/.*"token":"([^"]+)".*/\1/p')
curl -i -b /tmp/jar -H "X-CSRF-Token: $TOKEN" "$BASE/api/v1/lookup"

Burst the mint endpoint — drains the token bucket and 429s:

for i in $(seq 1 25); do
  curl -s -o /dev/null -w "mint %{http_code}\n" http://localhost:5050/api/v1/mint
done
The gate raises cost, it does not eliminate scraping. A determined attacker can still drive the legitimate two-step (mint → lookup) in a loop — but they pay for an entry-API round-trip on every MAX_USES_PER_TOKEN requests, plus the per-IP token bucket forces them to slow down or rotate IPs (which itself costs money).

Demo vs production reality

The demo strips several production realities that meaningfully change the economics of scraping. In a real deployment the bot's job is much harder than what you see here:

Demo (localhost)Real deployment
Both requests round-trip in ~0.5ms Each request adds 50–300ms (TLS handshake, network). Two requests per token-burst means the bot's wall-clock cost scales with (N_requests / MAX_USES_PER_TOKEN) × latency.
Mint endpoint /api/v1/mint is unprotected (only the token bucket) Mint sits behind Cloudflare / Turnstile / Anubis / Akamai bot manager — the bot has to clear that challenge every time it needs a fresh token.
Tool's TLS fingerprint doesn't matter requests.Session() has a JA3 fingerprint that doesn't match any real browser; gets flagged immediately by JA4-aware WAFs.
Per-IP token bucket only — and the IP is the docker bridge, so all local requests share it Per-IP, per-ASN, per-subnet rate limits at every endpoint at the CDN and origin. Datacenter IP ranges (AWS, GCP, Hetzner) score worse than residential — and IP comes from X-Forwarded-For validated against the CDN's known range, not the inbound peer.
No SIEM / no anomaly detection Every 403 / 429 logged; a single IP doing 100 token-mints in a minute pages someone.
UX tradeoff: any per-IP limit hurts users behind shared IPs (corporate NAT, CGNAT mobile, public WiFi). The token bucket above lets honest users burst within capacity and only throttles sustained load. In production you go further: serve a Turnstile / Anubis challenge when the bucket empties, so real users solve a 1-second puzzle and continue. Don't punish humans for an emptied bucket; punish them for failing the challenge.
Net: the bot can still scrape, but each lookup carries a real cost — CAPTCHA bypass (~$0.001–0.003 per solve), headless-browser compute, residential-proxy fees, and operator attention from anomaly alerts. The honest gate is economic, not cryptographic — make scraping cost more than the data is worth to the attacker.

Need this built into your stack?

We help product and security teams design and ship pre-auth anti-abuse gates — HMAC-bound tokens, edge rate limits, Turnstile / Anubis bot scoring, and vendor-cost protection — across the public surfaces that can't sit behind a login.