usernameless-webauthn-classes
v1.0.1
Published
Helpers for usernameless WebAuthn flows with PRF-based credential decryption and server verification utilities.
Maintainers
Readme
usernameless-webauthn-classes
Helpers for a niche WebAuthn flow where credentials are usernameless, verifiers are not pre-bound to accounts, and the PRF extension output is reused to decrypt an encrypted credential blob that holds the account pointer and keys.
What this library assumes
- Anyone can submit a
PublicKeyCredentialJSONverifier to your DB. You later look it up bycredentialIdat assertion time. - The credential you return to the client should be encrypted and hold: a signing-capable private key, a symmetric key for account data, and the account id/url. Decrypt it with the PRF extension result from the assertion.
- Assertion flow: fetch verifier by claimed credential id -> check challenge + signature -> fetch encrypted credential by credential id -> decrypt with PRF result -> sign account id/url with the private key so the server can verify ownership (401 otherwise). Repeat the pattern for other resources, layering ACLs as needed to allow revocation even when someone still holds a resource key.
- Challenge generation is yours to own; use
generateNoncefrombytecodecto produce a 256-bit random Base64URL string.
Requirements
- Browser/WebAuthn with the PRF extension available.
- Server runtime with WebCrypto (
Node.js >= 18works; if needed setglobalThis.crypto = require("node:crypto").webcryptobefore calling the server helpers). - You handle transport, persistence, ACLs, and actual credential encryption/decryption.
API
Browser: WebAuthnBrowserAgent
generateVerifierOptions(displayName, authenticatorAttachment)->PublicKeyCredentialCreationOptionswith resident key required, PRFfirstinput set to"credential-encryption-key", UV required, and a 60s timeout.generateVerifier(publicKey, signal?)-> awaitsnavigator.credentials.createand returnsPublicKeyCredentialJSON.generateAssertionPublicKeyParam(challengeBase64)->PublicKeyCredentialRequestOptionsfor usernameless flows (emptyallowCredentials, PRF extension, UV required, 60s timeout) using the server-issued challenge.getAssertion(publicKey, mediation, signal?)-> awaitsnavigator.credentials.getand returnsPublicKeyCredentialJSONplus the base64urlchallengeyou passed in.
Server: WebAuthnServerAgent
getVerifierReadyForStorage(verifierJson)-> extractscredentialId,rpId,publicKeyJwk, andcreatedAt(shape:StoredVerifier).resolveCredentialOwnershipAssertion(assertionJsonWithChallenge, storedVerifier, challengeStoredByServer)-> checks the challenge, parses client/authenticator data, enforces UV, validates rpId hash, and verifies the ES256 signature against the stored JWK.
StoredVerifier is small and storage-friendly:
type StoredVerifier = {
credentialId: Base64URLString;
rpId: string;
publicKeyJwk: {
kty: "EC";
crv: "P-256";
x: Base64URLString;
y: Base64URLString;
ext: true;
};
createdAt: number;
};Quick usage
// Registration (browser)
import { WebAuthnBrowserAgent } from "usernameless-webauthn-classes";
const publicKey = WebAuthnBrowserAgent.generateVerifierOptions(
"Alice",
"platform"
);
const verifierJson = await WebAuthnBrowserAgent.generateVerifier(publicKey);
// send verifierJson to the server for storage (anyone can post one)
// Registration (server)
import { WebAuthnServerAgent } from "usernameless-webauthn-classes";
const storedVerifier =
WebAuthnServerAgent.getVerifierReadyForStorage(verifierJson);
// persist storedVerifier so assertions can be matched by credentialId
// Assertion (server)
import { generateNonce } from "bytecodec";
const challenge = generateNonce(); // 256-bit Base64URL string from your server
// Assertion (browser)
const assertionOptions =
WebAuthnBrowserAgent.generateAssertionPublicKeyParam(challenge);
const assertionJson = await WebAuthnBrowserAgent.getAssertion(
assertionOptions,
"required"
);
// Assertion (server)
const ok = await WebAuthnServerAgent.resolveCredentialOwnershipAssertion(
assertionJson,
storedVerifier,
challenge
);
if (!ok) return res.status(401).end();
// derive PRF output from the assertion to decrypt the credential payload, then continue with your ACL logicDevelopment
- Tests:
npm test(node test runner; browser APIs are stubbed/mocked). - Benchmarks:
npm run bench(quick sanity timings for option generation and assertion verification).
Everything is ESM, tree-shakeable, and ships with TypeScript definitions for IntelliSense. Minimal changes were made to stay focused on the core usernameless WebAuthn flow described above.
