Secure quickstart
This path uses the published server verifier with the hosted browser module. It never trusts browser-decoded claims and never uses the Wamen token as the application session.
Before starting
- Use HTTPS in production. HTTP callbacks are accepted only for localhost and loopback development.
- Choose the exact application origin. The expected audience is
origin:<scheme>://<host>[:port]. - Install
@wamenjs/serveron the backend. Use@wamenjs/authin bundled browser apps or the hosted module for server-rendered and no-build apps.
1. Verify Wamen identity tokens on the backend
Install @wamenjs/server, then create one verifier for the Wamen issuer and your exact application origin.
npm install @wamenjs/server
import { createWamenVerifier } from "@wamenjs/server";
const APP_ORIGIN = "https://app.example";
const verifier = createWamenVerifier({
issuer: "https://wamen.link",
audience: "origin:" + new URL(APP_ORIGIN).origin,
});
export async function verifyWamenIdentity(idToken: string) {
if (typeof idToken !== "string" || idToken.length === 0 || idToken.length > 16_384) {
throw new Error("Invalid Wamen identity token");
}
return verifier.verify(idToken);
}
2. Exchange the verified identity for your app session
This working Hono example uses an in-memory session map for a local quickstart. Before production, use your application's durable session store without changing the Wamen verification boundary.
npm install hono
import { randomUUID } from "node:crypto";
import { Hono } from "hono";
import { setCookie } from "hono/cookie";
import { verifyWamenIdentity } from "./verify-wamen.js";
const app = new Hono();
const sessions = new Map<string, { subject: string; expiresAt: number }>();
app.post("/api/wamen/session", async (c) => {
if (!c.req.header("content-type")?.startsWith("application/json")) {
return c.json({ error: "expected_json" }, 415);
}
const body = await c.req.json<unknown>().catch(() => null);
if (
typeof body !== "object" ||
body === null ||
!("idToken" in body) ||
typeof body.idToken !== "string"
) {
return c.json({ error: "invalid_identity_token" }, 400);
}
try {
const identity = await verifyWamenIdentity(body.idToken);
const sessionId = randomUUID();
sessions.set(sessionId, {
subject: identity.subject,
expiresAt: Date.now() + 8 * 60 * 60 * 1000,
});
setCookie(c, "__Host-app_session", sessionId, {
httpOnly: true,
secure: true,
sameSite: "Lax",
path: "/",
maxAge: 8 * 60 * 60,
});
return c.body(null, 204);
} catch {
return c.json({ error: "identity_verification_failed" }, 401);
}
});
3. Add the hosted browser flow
Register the completion listener before loading the module. Call preventDefault() synchronously so Wamen does not navigate before your backend session request finishes.
<script>
async function finishWamenSignIn(event) {
event.preventDefault();
const { identity, returnTo } = event.detail;
try {
const response = await fetch("/api/wamen/session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ idToken: identity.idToken }),
});
if (!response.ok) throw new Error("The app backend rejected the identity");
location.replace(returnTo);
} catch (error) {
console.error("Wamen sign-in failed", error);
} finally {
window.Wamen?.clearIdentity();
}
}
window.addEventListener(
"wamen:authentication-complete",
(event) => void finishWamenSignIn(event),
);
</script>
<script
type="module"
src="https://wamen.link/wamen.js"
data-wamen-redirect-uri="https://app.example/callback"
></script>
<a
data-wamen-authorize
data-wamen-return-to="/account"
href="https://wamen.link/authorize"
>
Continue with WhatsApp
</a>
Bundled browser applications can install @wamenjs/auth instead. It exposes the same sign-in, callback, identity, and cleanup operations as the hosted module.
npm install @wamenjs/auth
4. Test the complete boundary
- Start from a clean browser tab and select Continue with WhatsApp.
- Confirm the authorization URL contains a random state, an S256 code challenge, and the exact callback URI.
- Send the unedited WhatsApp message and return to the same browser tab.
- Confirm the backend returns 204, the Wamen browser identity is cleared, and the app session cookie is Secure, HttpOnly, SameSite=Lax, and scoped to
/. - Confirm an expired token, wrong audience, wrong issuer, and backend rejection do not create a session.
Prompt for an AI coding agent
Paste this into the agent working in your application repository. It directs the agent to the machine-readable Wamen context and prevents the common insecure shortcuts.
Integrate Wamen passwordless sign-in into this application.
Authoritative context:
- Read https://wamen.link/llms.txt first.
- Read https://wamen.link/llms-full.txt for the complete current contract.
- Use issuer https://wamen.link exactly.
- Do not invent Wamen APIs, parameters, scopes, claims, packages, or session endpoints.
Current release constraints:
- @wamenjs/auth@0.1.0 and @wamenjs/server@0.1.0 are published on npm.
- Use @wamenjs/auth for bundled browser applications or the hosted module at https://wamen.link/wamen.js for server-rendered and no-build applications.
- Use @wamenjs/server for backend identity verification. The direct jose verifier at https://wamen.link/docs/server-verification remains the framework-independent reference.
- Wamen is authorization-code-and-PKCE-shaped, but it does not claim full OAuth or OpenID Connect compliance.
- Supported scopes are openid and optional phone. Phone requires fresh consent for every login.
Required implementation:
1. Inspect the existing framework, routing, session, cookie, and test conventions before editing.
2. Add a same-origin callback URL. Public callbacks must use HTTPS; HTTP is allowed only for localhost or loopback.
3. Reuse the application's package and framework conventions. For a bundled browser app, install @wamenjs/auth. For a server-rendered or no-build app, register the wamen:authentication-complete listener before loading wamen.js.
4. Stop automatic callback navigation while the app session is created. With @wamenjs/auth, call handleCallback({ navigate: false }); with the hosted module, call event.preventDefault() synchronously. Send only identity.idToken to the app backend, await the response, clear the browser identity in a finally block, then navigate to the validated returnTo only after the backend session succeeds.
5. On the backend, install @wamenjs/server and verify the token with createWamenVerifier({ issuer, audience }). Treat ES256, kid, issuer, exact audience, subject format, iat, exp, the exact 600-second lifetime, jti, amr=["whatsapp"], and the optional phone pair as enforced package invariants.
6. Derive the audience as origin: plus the normalized application origin, including a non-default port when present.
7. Find or create the app user by the verified pairwise subject. Never use a browser-decoded claim or phone number as the stable identity key.
8. Create the application's own opaque session and set a Secure, HttpOnly, SameSite=Lax cookie. Do not use the Wamen identity token as a durable app session.
9. Keep returnTo same-origin. Do not add open redirects.
10. Add behavior tests for successful sign-in, wrong state, backend rejection, wrong issuer or audience, expired token, optional phone, logout, and cookie attributes.
Do not:
- trust JWT claims in browser code;
- log tokens, authorization codes, phone numbers, message bodies, or raw callback URLs;
- request phone unless the product needs it and explains why;
- add access tokens, refresh tokens, remembered Wamen sessions, email, name, or profile claims;
- claim official Meta or WhatsApp affiliation.
Finish by running the project's formatter, type checker, targeted tests, and a browser smoke test of the complete callback-to-session path. Report exact files changed and verification evidence.