66 lines · 2.7 KB
import { fromBase64Url, toBase64Url } from "./base64url";

type Descriptor = { type: "public-key"; id: string };

type CreationOptions = {
  challenge: string;
  rp: { id: string; name: string };
  user: { id: string; name: string; displayName: string };
  pubKeyCredParams: { type: "public-key"; alg: number }[];
  excludeCredentials: Descriptor[];
  timeout: number;
};

type RequestOptions = { challenge: string; rpId: string; allowCredentials: Descriptor[]; timeout: number };

function descriptors(list: Descriptor[]): PublicKeyCredentialDescriptor[] {
  return list.map((item) => ({ type: item.type, id: fromBase64Url(item.id) }));
}

/** Runs the browser registration ceremony and returns base64url fields for the server to verify. */
export async function createPasskey(options: CreationOptions) {
  const credential = await navigator.credentials.create({
    publicKey: {
      challenge: fromBase64Url(options.challenge),
      rp: options.rp,
      user: { ...options.user, id: fromBase64Url(options.user.id) },
      pubKeyCredParams: options.pubKeyCredParams,
      excludeCredentials: descriptors(options.excludeCredentials),
      authenticatorSelection: { residentKey: "preferred", userVerification: "preferred" },
      attestation: "none",
      timeout: options.timeout,
    },
  });
  if (!(credential instanceof PublicKeyCredential)) throw new Error("Passkey creation was cancelled");
  const response = credential.response;
  if (!(response instanceof AuthenticatorAttestationResponse)) throw new Error("Unexpected passkey response");
  return {
    challenge: options.challenge,
    clientDataJSON: toBase64Url(new Uint8Array(response.clientDataJSON)),
    attestationObject: toBase64Url(new Uint8Array(response.attestationObject)),