# 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