chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// @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<string|null>}
*/
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,
};
}