Skip to main content

Security & Limitations

Production checklist

  • Origin filtering (host side). Drop every MessageEvent whose event.origin !== PRIVATEKIT_ORIGIN. The iframe currently sends with targetOrigin: '*'; the host is the only side that can enforce origin safety today.
  • Origin filtering (host → iframe). When posting to the iframe via iframeRef.current.contentWindow.postMessage(...), pass PRIVATEKIT_ORIGIN as the second argument in production builds. '*' is acceptable only for local development.
  • connectionId discipline. Always echo the latest connectionId from PRIVATE_KIT_INIT. Never reuse a stored connectionId after the iframe has been unmounted — a new mount generates a new one.
  • authToken exposure. The token is sent inside a postMessage envelope addressed to the iframe. With targetOrigin: '*' any other window listening on the parent's message channel could in theory observe it; this is the main reason to lock down targetOrigin to PRIVATEKIT_ORIGIN in production. The iframe itself never forwards the parent-supplied token back to the parent.
  • Tokens in EMAIL_CONFIRMED / PHONE_CONFIRMED. Both email and phone confirmation rotate the user's session JWTs server-side, and the iframe forwards the new token / refreshToken to the parent so the host can persist them. The same targetOrigin discipline applies in the other direction: the iframe posts with targetOrigin: '*' today, so until that is tightened the host must filter inbound messages strictly by event.origin === PRIVATEKIT_ORIGIN before reading those token fields.
  • Token freshness. Treat PRIVATE_KIT_AUTH_TOKEN_401 as the canonical "refresh now" signal. Do not pre-validate the token on the host — let the iframe do the round-trip; otherwise you risk the local clock and the backend disagreeing about expiry.
  • Multiple iframes. Each mounted iframe has its own connectionId. If you embed several PrivateKit instances simultaneously (e.g. one for username, one for a future action), route inbound messages by connectionId to the right host-side handler.
  • Iframe sandbox attribute. If you add sandbox, include at least allow-scripts allow-same-origin — otherwise the iframe cannot call the auth API.
  • Replay. The contract has no per-message nonce. A malicious host that captures a valid UPDATE_USERNAME envelope can resend it — but the backend itself is the source of truth: a replayed envelope simply produces the same outcome (idempotent rename, "same username", or 401). Do not treat the contract as a defence against a compromised host page.

Limitations

  • Supported actions. Today PrivateKit supports username changes, the email-change + verification flow, the phone-change + verification flow (each with code resend), and single-step password change. The message shape is extensible — additional *_UPDATE_* / *_UPDATED / *_VALIDATION_ERROR triples can be added without renaming the existing ones.
  • *_VALIDATION_ERROR.reason granularity. Reasons are coarse: required, invalid, exist, unknown for USERNAME_VALIDATION_ERROR; plus limitReached for EMAIL_VALIDATION_ERROR and PHONE_VALIDATION_ERROR; required, max, invalid, invalidCode, unknown for the *_CONFIRMATION_ERROR events; requiredCurrent, requiredNew, min, uppercase, special, number, invalidCurrent, unknown for PASSWORD_VALIDATION_ERROR. Other backend 400 conditions are collapsed into unknown. If new specific error codes appear, expose them as new reasons rather than overloading unknown.
  • Password confirmation is host-side. The iframe contract for password change carries only currentPassword and newPassword. The confirmation field that the standalone ChangePasswordForm shows is a UX-only check; the host must enforce confirmation === newPassword before posting. Don't send the confirmation across postMessage — it doubles the exposure of the plaintext password without any backend benefit.
  • targetOrigin: '*' on iframe side. Temporary, pending the agreement on an allowlist of accepted host origins.
  • No refresh inside the iframe. PrivateKit deliberately does not refresh the token itself, because it does not have access to the host's refresh-token cookie. Refreshing is always the host's job, signalled by AUTH_TOKEN_401.
  • Logging. Both sides log only when the auth-central app is built with appConfig.demo.available = true. In production builds the loggers are no-ops; rely on browser DevTools network/console for diagnostics or stand up a dedicated demo build.
  • Single contract source. The enums are exported from the iframe page's source file. A consumer in another repo must keep its own mirror in sync. A future refactor will extract the contract into a publishable package shared with Web3Kit.