// @pdsjs/core/passkey - The passkey ceremony: a challenge issued under a // short-lived cookie, and the cookie read back to prove a response answers // it. Shared by the account passkey pages, the account sign-in, and the OAuth // consent screen's passkey approval. The WebAuthn verification math is in // webauthn.js. import { createChallengeJwt, verifyChallengeJwt } from './auth.js'; import { readCookie } from './http.js'; import { generateChallenge } from './webauthn.js'; // Cookie holding the challenge for an in-flight passkey ceremony export const PASSKEY_COOKIE = 'pdsjs_webauthn'; /** * @param {import('./ports.js').SharedStoragePort} sharedStorage * @param {string} jwtSecret */ export function createPasskeyCeremony(sharedStorage, jwtSecret) { /** * Whether this deployment's storage can hold and look up a passkey. * @returns {boolean} */ function passkeysAvailable() { return Boolean(sharedStorage.getPasskey); } /** * The relying party a passkey is bound to: this server's hostname, without * a port. A credential registered here works only here. * @param {URL} url * @returns {string} */ function passkeyRpId(url) { return url.hostname; } /** * Start a ceremony: a fresh challenge, plus the cookie that will prove the * response answers it. * @param {string} purpose - 'register' or 'sign-in' * @param {URL} url * @param {string} [cookiePath] * @returns {Promise<{challenge: string, cookie: string}>} */ async function startPasskeyCeremony(purpose, url, cookiePath = '/account') { const challenge = generateChallenge(); const token = await createChallengeJwt(challenge, purpose, jwtSecret); return { challenge, cookie: [ `${PASSKEY_COOKIE}=${encodeURIComponent(token)}`, `Path=${cookiePath}`, 'HttpOnly', 'SameSite=Lax', 'Max-Age=300', url.protocol === 'https:' ? 'Secure' : null, ] .filter(Boolean) .join('; '), }; } /** * The challenge this request's cookie was issued for. * @param {Request} request * @param {string} purpose * @returns {Promise} */ async function readPasskeyChallenge(request, purpose) { const token = readCookie(request, PASSKEY_COOKIE); if (!token) return null; try { return await verifyChallengeJwt(token, purpose, jwtSecret); } catch { return null; } } return { passkeysAvailable, passkeyRpId, startPasskeyCeremony, readPasskeyChallenge, }; }