// WebAuthn helpers for passkey registration/authentication against the // MyAccount Hamsoo API. Implements the real WebAuthn ceremony (the backend // performs cryptographic attestation/assertion verification). const BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api/v1"; function bufToBase64url(buf: ArrayBuffer): string { const bytes = new Uint8Array(buf); let str = ""; for (let i = 0; i < bytes.length; i++) str += String.fromCharCode(bytes[i]); return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } function base64urlToBuf(b64: string): ArrayBuffer { const pad = b64.length % 4 ? 4 - (b64.length % 4) : 0; const b = atob(b64.replace(/-/g, "+").replace(/_/g, "/") + "====".slice(0, pad)); const bytes = new Uint8Array(b.length); for (let i = 0; i < b.length; i++) bytes[i] = b.charCodeAt(i); return bytes.buffer; } function prepareCreationOptions(publicKey: any) { publicKey.challenge = base64urlToBuf(publicKey.challenge); if (publicKey.user) publicKey.user.id = base64urlToBuf(publicKey.user.id); if (Array.isArray(publicKey.excludeCredentials)) { publicKey.excludeCredentials = publicKey.excludeCredentials.map((c: any) => ({ ...c, id: base64urlToBuf(c.id), })); } return publicKey; } export async function registerPasskey(token: string, name: string) { const beginRes = await fetch(`${BASE}/auth/passkeys/register/begin/`, { method: "POST", headers: { Authorization: `Bearer ${token}` }, credentials: "include", }); if (!beginRes.ok) throw new Error("Failed to begin passkey registration"); const options = await beginRes.json(); const cred = (await navigator.credentials.create({ publicKey: prepareCreationOptions(options.publicKey), })) as any; const payload = { id: cred.id, rawId: bufToBase64url(cred.rawId), response: { clientDataJSON: bufToBase64url(cred.response.clientDataJSON), attestationObject: bufToBase64url(cred.response.attestationObject), }, type: cred.type, clientExtensionResults: cred.getClientExtensionResults ? cred.getClientExtensionResults() : {}, name, }; const finishRes = await fetch(`${BASE}/auth/passkeys/register/finish/`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, credentials: "include", body: JSON.stringify(payload), }); if (!finishRes.ok) { const err = await finishRes.json().catch(() => ({})); throw new Error(err.detail || "Passkey registration failed"); } return finishRes.json(); } export async function authenticatePasskey(token: string | null, email: string) { const headers: Record = {}; if (token) headers.Authorization = `Bearer ${token}`; const beginRes = await fetch(`${BASE}/auth/passkeys/authenticate/begin/`, { method: "POST", headers: { "Content-Type": "application/json", ...headers }, credentials: "include", body: JSON.stringify({ email }), }); if (!beginRes.ok) throw new Error("Failed to begin passkey authentication"); const options = await beginRes.json(); const requestOptions = options.publicKey; requestOptions.challenge = base64urlToBuf(requestOptions.challenge); if (Array.isArray(requestOptions.allowCredentials)) { requestOptions.allowCredentials = requestOptions.allowCredentials.map( (c: any) => ({ ...c, id: base64urlToBuf(c.id) }), ); } const assertion = (await navigator.credentials.get({ publicKey: requestOptions, })) as any; const payload = { id: assertion.id, rawId: bufToBase64url(assertion.rawId), response: { clientDataJSON: bufToBase64url(assertion.response.clientDataJSON), authenticatorData: bufToBase64url(assertion.response.authenticatorData), signature: bufToBase64url(assertion.response.signature), userHandle: assertion.response.userHandle ? bufToBase64url(assertion.response.userHandle) : undefined, }, type: assertion.type, clientExtensionResults: assertion.getClientExtensionResults ? assertion.getClientExtensionResults() : {}, }; const finishRes = await fetch(`${BASE}/auth/passkeys/authenticate/finish/`, { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify(payload), }); if (!finishRes.ok) { const err = await finishRes.json().catch(() => ({})); throw new Error(err.detail || "Passkey authentication failed"); } return finishRes.json(); }