# Wamen complete integration context > Wamen provides passwordless identity proof through one WhatsApp message. It returns a ten-minute ES256 identity token. The integrating application verifies the token, maps the pairwise subject to its own user, and creates its own durable session. ## Current release - Issuer: https://wamen.link - Availability: experimental, free, one unofficial WhatsApp connection, no SLA - Protocol: authorization-code-and-PKCE-shaped, not full OAuth or OpenID Connect - PKCE: S256 required - Transaction lifetime: 3 minutes - Identity token lifetime: exactly 600 seconds - Scopes: openid and optional phone - Browser storage: short identity in localStorage; clear it after backend exchange - Stable app identity: verified pairwise subject, not phone - Packages: @wamenjs/auth@0.1.0 and @wamenjs/server@0.1.0 are published on npm ## Required application flow 1. Use @wamenjs/auth in a bundled browser application or https://wamen.link/wamen.js in a server-rendered or no-build application. 2. Start sign-in. Wamen creates state and PKCE, runs WhatsApp proof, returns a one-time code, validates state, and exchanges the code. 3. Stop automatic navigation while the application creates its session. Call handleCallback({ navigate: false }) with @wamenjs/auth, or call event.preventDefault() synchronously in the hosted completion listener. 4. Send only the returned identity.idToken to the app backend. 5. Verify it with @wamenjs/server using the exact issuer and audience for the application origin. 6. Find or create the app user by the verified pairwise subject. 7. Create an opaque app session with a Secure, HttpOnly, SameSite=Lax cookie. 8. Clear the Wamen browser identity in a finally block. 9. Navigate to the validated returnTo only after the backend session succeeds. ## Install packages ```sh npm install @wamenjs/auth @wamenjs/server ``` Use @wamenjs/auth for bundled browser applications. The hosted module remains supported for server-rendered and no-build applications. ## Hosted browser example ```html Continue with WhatsApp ``` ## Recommended server verifier ```ts 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); } ``` ## Direct jose verifier reference Use this implementation as an auditable description of the package invariants or as a fallback when @wamenjs/server cannot be installed. ```sh npm install jose ``` ```ts import { createRemoteJWKSet, jwtVerify } from "jose"; const ISSUER = "https://wamen.link"; const APP_ORIGIN = "https://app.example"; const AUDIENCE = "origin:" + new URL(APP_ORIGIN).origin; const JWKS = createRemoteJWKSet( new URL(ISSUER + "/.well-known/jwks.json"), ); const SUBJECT = /^wam_[A-Za-z0-9_-]{43}$/; const REQUIRED = ["iss", "aud", "sub", "iat", "exp", "jti", "amr"]; export async function verifyWamenIdentity(idToken: string) { if (typeof idToken !== "string" || idToken.length === 0 || idToken.length > 16_384) { throw new Error("Invalid Wamen identity token"); } const now = new Date(); const { payload, protectedHeader } = await jwtVerify(idToken, JWKS, { algorithms: ["ES256"], issuer: ISSUER, audience: AUDIENCE, requiredClaims: REQUIRED, currentDate: now, clockTolerance: 0, }); if ( protectedHeader.alg !== "ES256" || typeof protectedHeader.kid !== "string" || protectedHeader.kid.length === 0 || payload.iss !== ISSUER || payload.aud !== AUDIENCE || typeof payload.sub !== "string" || !SUBJECT.test(payload.sub) || !Number.isSafeInteger(payload.iat) || !Number.isSafeInteger(payload.exp) || Number(payload.exp) - Number(payload.iat) !== 600 || Number(payload.iat) > Math.floor(now.getTime() / 1000) + 5 || typeof payload.jti !== "string" || payload.jti.length === 0 || !Array.isArray(payload.amr) || payload.amr.length !== 1 || payload.amr[0] !== "whatsapp" ) { throw new Error("Invalid Wamen identity claims"); } const hasPhone = Object.hasOwn(payload, "phone_number"); const hasPhoneVerified = Object.hasOwn(payload, "phone_number_verified"); if (hasPhone !== hasPhoneVerified) { throw new Error("Invalid Wamen phone claims"); } const identity = { issuer: ISSUER, audience: AUDIENCE, subject: payload.sub, issuedAt: Number(payload.iat), expiresAt: Number(payload.exp), jwtId: payload.jti, authenticationMethods: ["whatsapp"] as const, }; if (!hasPhone) return identity; if ( typeof payload.phone_number !== "string" || !/^\+[1-9]\d{1,14}$/.test(payload.phone_number) || payload.phone_number_verified !== true ) { throw new Error("Invalid Wamen phone claims"); } return { ...identity, phoneNumber: payload.phone_number, phoneNumberVerified: true as const, }; } ``` ## App session endpoint example The following Hono example is functional for local development. Replace the in-memory Map with the application's durable session store before production. ```ts 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(); 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().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); } }); ``` ## Trusted identity A verified identity contains: - issuer: https://wamen.link - audience: origin: plus the normalized app origin - subject: wam_ plus 43 base64url characters - issuedAt and expiresAt numeric dates - jwtId: non-empty token identifier - authenticationMethods: exactly ["whatsapp"] - optional phoneNumber with phoneNumberVerified=true when fresh phone consent was granted Never use phone as the stable identity key. Never trust claims merely decoded in browser JavaScript. ## Security requirements - Public callback URLs use HTTPS. HTTP is allowed only for localhost or loopback. - Keep returnTo same-origin and reject open redirects. - Never log tokens, authorization codes, raw callback URLs, phone numbers, raw LIDs, or WhatsApp message bodies. - Do not persist the Wamen identity token as the durable application session. - Do not add access tokens, refresh tokens, remembered Wamen sessions, email, name, picture, or authorization claims. - Request phone only when the application needs it and explains why. - Describe Wamen accurately as experimental and unofficial; do not claim Meta or WhatsApp affiliation. ## Browser errors The browser client uses stable error codes including browser_unavailable, crypto_unavailable, fetch_unavailable, storage_unavailable, invalid_configuration, invalid_scope, unsafe_return_to, no_pending_flow, pending_flow_expired, invalid_callback, state_mismatch, authorization_error, token_exchange_failed, and invalid_token_response. ## Verification failures Treat malformed, wrongly signed, expired, wrong-issuer, wrong-audience, wrong-lifetime, malformed-subject, malformed-amr, and inconsistent-phone tokens as authentication failures. Return a generic 401 to the browser and retain only sanitized server diagnostics. ## AI coding-agent prompt ```text 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. ``` ## Primary documentation - https://wamen.link/docs/quickstart - https://wamen.link/docs/hosted-script - https://wamen.link/docs/server-verification - https://wamen.link/docs/security - https://wamen.link/.well-known/wamen-configuration - https://wamen.link/.well-known/jwks.json