In August 2026 the entire auth surface went through three adversarial reviews, and this page is the honest version of what that means: the reviews were run by the people who built AuthPop — with AI assistance, in three independent passes — not by a hired audit firm. What we can offer instead of a firm's letterhead is the full findings list, including what is still open.
Three separate passes, each taking one adversarial lens across the whole codebase:
Every fix below was deployed and re-verified against production endpoints, not just in review.
The full internal write-up is longer; this is every finding, compressed to what it allowed and what closed it.
Audience binding was a no-op. Resource servers accepted any
aud starting with a client id, so a token minted for one client could be replayed
against another resource. Now: exact resource-URL match, enforced on both ends.
Unverified account squatting. Registering someone else's email created account state the real owner later inherited — with the squatter's passkey still attached. Now: verify-first registration; the inbox is proven before any account exists, and reclaiming a previously-unverified row deletes prior credentials and revokes sessions.
Existing accounts accepted new passkeys from just an email. Typing a victim's address bound the attacker's passkey to their account. Now: a 6-digit approval code gates every added credential.
OTP caps did not bind under concurrency (KV read-modify-write). Now: codes live in D1 and the attempt is counted by the same statement that enforces the ceiling.
The consent endpoint skipped the email-verified gate that the UI path enforced. Now: 403 on unverified, on every path.
Dev mode and test harnesses were live in production — a shared origin that dissolved per-subdomain passkey isolation, and endpoints that could mint sessions. Now: both off; harnesses 404 in production.
Refresh tokens had rotation but no reuse detection. Now: token families — replaying a rotated-out token revokes the whole chain; 30-day absolute lifetime; RFC 7009 revocation; "sign out everywhere" revokes families (a plain browser sign-out deliberately does not — it must not disconnect a user's MCP connectors). Users also see their connected apps on their sign-in server and can revoke any one on its own, and are emailed the first time a connection is renewed from a network it has not used before.
Account-existence oracle — registration answered differently for known and unknown emails. Now: identical responses either way.
Tenant signing keys sat in plaintext rows. Now: AES-GCM wrapped under a separate key-wrap secret; a database read alone can no longer forge tokens.
resource and scope were attacker-controlled
strings baked into signed tokens. Now: validated URL shape, owner allowlists, scope
intersection.
The consent screen showed only the client's self-chosen name — ideal phishing. Now: it names the redirect host, the resource, the scopes, and flags brand-new clients.
Open client registration was invisible to app owners. Now: a per-app registration policy (open / allowlist / off), a client list with provenance, and revocation that actually kills the client's outstanding tokens.
Customer-controlled names flowed unescaped into emails (phishing on our sending reputation, header injection). Now: escaped, stripped, validated at creation.
Racy KV rate counters. Now: atomic D1 counters for every security-relevant cap, keyed per tenant and IP.
CSRF rested on SameSite alone, which does not separate sibling subdomains. Now: Origin validated on state-changing POSTs.
Missing OAuth hardening — no RFC 9207 iss, loose
response_type/PKCE-length/token-endpoint validation. Now: all enforced.
Reserved-slug gaps including demo and security-sounding
names usable for phishing. Now: list extended.
Unanchored session-cookie regex — a fixation primitive. Now: anchored.
Re-enrolling a device 500'd (missing excludeCredentials);
UV policy hardcoded; error responses leaked internals; admin secret compared non-constant-time.
Now: all fixed; user-verification is a per-app setting.
Schema drift — a rebuild from schema.sql would have
dropped the verification column. Now: realigned, migrations recorded.
{app}.authpop.dev confirms an app exists and
shows its display name. We judged the friendly landing page worth more than hiding it; tell us if
your threat model disagrees.Two items originally tracked here have since shipped. Signing-key rotation: rotate from the dashboard (or the API); the new key signs immediately and the old key keeps verifying already-issued tokens for 24 hours before it drops out of the JWKS, so nothing breaks mid-rotation. Admin audit log: every privileged change to an app — clients added or revoked, registration-policy changes, key rotations, whether made in the dashboard or through an AI assistant — is now recorded internally and kept for 90 days, which closes the finding. There is no customer-facing view of that record yet; an owner-facing audit view is planned as part of a future paid tier.
A known, accepted tradeoff rather than a finding: the refresh-token family id travels inside the token, so anyone who sees a truncated token in a log can force one family's revocation — a re-authentication, never an access. That is the price of reuse detection, and we chose it.
email_verified.Email security@authpop.com — machine-readable version at /.well-known/security.txt. AuthPop is operated by one person: reports are read by the person who wrote the code, and a first reply should take a couple of days. Please include steps to reproduce. There is no bug bounty yet; credit is gladly given.