Sign in →
Embed Widgets1 min read

Embed Widgets: Authentication

Three auth modes — anonymous, bridge token, magic link — with full code samples for Node.js, Python, and Go. Choose the right mode for your portal.

Updated 2026-06-15Suggest edits
Docs Embed Widgets Authentication

Which mode should I use?#

The plugin tier supports three authentication modes. Pick the one that matches your portal's existing auth setup:

ModeWhen to useWhat widgets work
AnonymousMarketing pages. No customer identity yet.PricingCard only
Bridge tokenYour portal already has its own auth and an active customer session.All widgets
Magic linkYour portal has no session and you want Aforo to handle email-based auth.All customer-scoped widgets

Anonymous mode#

The simplest mode. Drop a PricingCard with just tenantSlug and embedKey. No backend changes needed.

pricing.html
<div
  data-aforo-widget="pricing-card"
  data-tenant-slug="acme"
  data-embed-key="embk_live_…"
></div>

Anonymous mode is enforced server-side: the headless config endpoint only returns offerings marked PUBLISHED, and the embed key's domain allowlist must include the requesting page's origin.

Bridge token mode#

For widgets that need to know which customer the page belongs to. Your backend mints a short-lived signed token; the widget exchanges it for an in-memory session JWT. Your signing key never reaches the browser.

Step 1 — Get your signing key

In Embed Studio, navigate to Auth Bridge, click Mint a new signing key, and choose HS256 or RS256. Store the key material in your secret manager (HSM, KMS, Vault). Never commit it to source.

WARNING
Treat the signing key like a database password. If it leaks, anyone can impersonate any of your customers to Aforo. Promote a NEXT key to ACTIVE before revoking the old one to avoid downtime.

Step 2 — Mint bridge tokens on your backend

Bridge tokens are short-lived (5 minutes recommended) JWTs signed with your key. The payload carries the customer identity Aforo should associate with this session.

mint-bridge-token.ts
import { SignJWT } from 'jose';

const SIGNING_KEY = new TextEncoder().encode(process.env.AFORO_BRIDGE_SIGNING_KEY!);
const KEY_ID = process.env.AFORO_BRIDGE_KEY_ID!;

export async function mintBridgeToken(customer: {
  externalId: string;
  email: string;
}) {
  return new SignJWT({
    external_customer_id: customer.externalId,
    email: customer.email,
  })
    .setProtectedHeader({ alg: 'HS256', kid: KEY_ID })
    .setIssuedAt()
    .setIssuer('https://shop.example.com')
    .setAudience('aforo-embed')
    .setExpirationTime('5m')
    .sign(SIGNING_KEY);
}

Step 3 — Pass the token to your widget

app/subscribe/page.tsx
import { AforoSubscribeButton } from '@aforoai/storefront-widgets';

export default async function SubscribePage() {
  const session = await getSession();
  const bridgeToken = await mintBridgeToken({
    externalId: session.customerId,
    email: session.email,
  });

  return (
    <AforoSubscribeButton
      tenantSlug="acme"
      embedKey="embk_live_…"
      bridgeToken={bridgeToken}
      offeringId="off_pro"
    />
  );
}

Step 4 — Aforo exchanges the bridge token for a session JWT

On mount, the widget calls AforoSession.getSessionJwt(bridgeToken) which POSTs to Aforo's bridge endpoint. Aforo verifies the signature against the registered public key (RS256) or shared secret (HS256), maps your external_customer_id to an internal Aforo customer (creating one on first sight), and returns a session JWT bound to that customer + tenant.

The session JWT lives in memory only — never in localStorage, never in cookies. It expires after 1 hour and is auto-refreshed 5 minutes before expiry. If your customer changes (e.g. logout/login), call AforoSession.destroy() and remount the widget to start a fresh exchange.

For portals that don't have their own session yet. The customer types their email, gets an Aforo-branded email, clicks the link, and lands back on your page with a session JWT. Aforo handles the entire flow — your backend doesn't need to mint anything.

app/account/invoices/page.tsx
import { AforoInvoiceList } from '@aforoai/storefront-widgets';

export default function InvoicesPage() {
  return (
    <AforoInvoiceList
      tenantSlug="acme"
      embedKey="embk_live_…"
      magicLinkMode={true}
    />
  );
}

On mount, the widget renders an email input. After the customer submits, they see a Check your inbox confirmation. The link in the email returns the customer to the same page with ?aforo_magic_token=… in the URL — the SDK detects it, exchanges it for a session JWT, and renders the actual invoice list. The URL parameter is stripped via history.replaceState so it doesn't end up in the page's analytics.

Anti-enumeration posture

The magic-link request endpoint always returns the same 202 Accepted envelope regardless of whether the email is registered. Attackers cannot use it as a user-registration probe.

Rate limits

  • Per IP — 10 magic-link requests per minute. Excess requests get a silent 202 (no error, but no email sent).
  • Per email — 5 magic-link requests per hour. Same silent 202 behavior past the cap.
  • Token validity — 15 minutes, one-time use, pessimistic-lock enforced server-side so the same link cannot be redeemed twice even if the customer clicks fast.

Security boundaries#

WARNING
Customer responsibilities (not Aforo's): securing your bridge-token signing key (HSM / KMS / secret manager), authenticating your own customers, choosing when to mint bridge tokens (only after your own auth succeeds), and validating Aforo webhooks before mutating your database.

See the for the full security model, CSP guidance, and how to report vulnerabilities.