Security

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.

How it was reviewed

Three separate passes, each taking one adversarial lens across the whole codebase:

  • OAuth & tokens — authorization code flow, PKCE, audience binding, refresh rotation, token endpoint validation.
  • WebAuthn, sessions & account lifecycle — registration, recovery, email verification, enumeration, session revocation. Two of the worst findings here were caught first in live testing by re-running flows as an existing user, then the review generalized them.
  • Multi-tenancy, control plane & MCP — tenant isolation, reserved slugs, dev-mode surfaces, rate limiting, client registration policy.

Every fix below was deployed and re-verified against production endpoints, not just in review.

What was found and fixed — 20 findings

The full internal write-up is longer; this is every finding, compressed to what it allowed and what closed it.

Critical

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.

Critical

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.

High

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.

High

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.

High

The consent endpoint skipped the email-verified gate that the UI path enforced. Now: 403 on unverified, on every path.

High

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.

High

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.

High

Account-existence oracle — registration answered differently for known and unknown emails. Now: identical responses either way.

High

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.

Medium

resource and scope were attacker-controlled strings baked into signed tokens. Now: validated URL shape, owner allowlists, scope intersection.

Medium

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.

Medium

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.

Medium

Customer-controlled names flowed unescaped into emails (phishing on our sending reputation, header injection). Now: escaped, stripped, validated at creation.

Medium

Racy KV rate counters. Now: atomic D1 counters for every security-relevant cap, keyed per tenant and IP.

Medium

CSRF rested on SameSite alone, which does not separate sibling subdomains. Now: Origin validated on state-changing POSTs.

Medium

Missing OAuth hardening — no RFC 9207 iss, loose response_type/PKCE-length/token-endpoint validation. Now: all enforced.

Low

Reserved-slug gaps including demo and security-sounding names usable for phishing. Now: list extended.

Low

Unanchored session-cookie regex — a fixation primitive. Now: anchored.

Low

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.

Low

Schema drift — a rebuild from schema.sql would have dropped the verification column. Now: realigned, migrations recorded.

Still open, on purpose

  • App existence is discoverable. {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.

Standing design choices

  • No passwords exist anywhere in the system — nothing to phish, stuff, or leak.
  • Tokens verify against a public JWKS on your servers; an AuthPop outage does not sit in your request path for already-issued tokens.
  • Email ownership is proven before an account exists, and tokens carry email_verified.
  • Passkeys bind to the app's own subdomain, so one app's credential is useless against another's.
  • Security caps are counted atomically in the database, not in eventually-consistent storage.

Report a vulnerability

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.