Skip to content
ecdsa.com
Node.js · WebCrypto

SimpleWebAuthn

Matthew Miller's SimpleWebAuthn, the most widely used passkey library for TypeScript: option generators and response verifiers that require user verification by default, accept only DER assertion signatures and refuse counter regressions.
License
MIT
Checked version
14.0.3 ·
Source
github.com/MasterKale/SimpleWebAuthn
Documentation
simplewebauthn.dev/docs/

Compatibility at a glance

WebAuthn

SimpleWebAuthn splits WebAuthn into two packages: @simplewebauthn/browser wraps navigator.credentials, and @simplewebauthn/server generates the options a relying party sends and verifies what comes back — attestation formats, COSE keys, counters and signatures. The check drove the server package with a software authenticator that produced standard registration and assertion JSON, the same bytes used for the Python, Go and Java pages.

The most common integration bug is a mismatch between what the options ask for and what verification demands. generateRegistrationOptions() defaults to userVerification: "preferred", but both verifiers default to requireUserVerification: true: an assertion from an authenticator that only proved user presence — a security key without a PIN — was refused with User verification required, but user could not be verified. Decide once and make both sides say the same thing, as the sample does.

Assertion signatures for ES256 must be ASN.1 DER, as the WebAuthn specification requires. A raw 64-byte r‖s signature is fed to the ASN.1 parser anyway and fails with a message that depends on the signature bytes — Too big integer, End of input reached before message was fully decoded or Data does not match to ECDSASigValue ASN1 schema. were the most frequent over 60 fresh credentials — none of which mentions the encoding. Authenticators and passkey providers produce DER, so raw signatures usually mean a hand-written client or a test harness that called WebCrypto's sign() directly; convert them with the DER ⇄ raw converter. A high-S DER signature verified.

A sign counter lower than the stored one is an error, not a warning: verification threw Response counter value 3 was lower than expected 5, and a counter of 0 after a stored 5 failed the same way. Only a credential that has always reported 0 — as synced passkeys do — passes with 0. Store the counter you received, and decide yourself whether a regression locks the credential or only alerts. Version 14 lists ML-DSA-44 (COSE -48) first in the default pubKeyCredParams, ahead of Ed25519 (-8), ES256 (-7) and RS256 (-257), and the Ed25519 credential verified end to end.

Decode what a browser actually sent with the WebAuthn inspector, try flows without a real authenticator in the passkey sandbox, and identify authenticator models by AAGUID with AAGUID lookup.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/simplewebauthn/run.sh ran SimpleWebAuthn 14.0.3 on Node.js 24.22.0 in the container image node:24.22.0-bookworm-slim on October 20, 2026; the page must match what it printed, and each fact lists those lines.

  • Default COSE algorithms

    -48-8-7-257

    generateRegistrationOptions() lists COSE algorithms -48 (ML-DSA-44), -8 (Ed25519), -7 (ES256) and -257 (RS256), in that order.

    Recorded by the check
    fact webauthn-default-algorithms = -48, -8, -7, -257
  • Default attestation

    none

    Attestation conveyance defaults to none.

    Recorded by the check
    fact webauthn-attestation = none
  • Default user verification

    preferred

    authenticatorSelection.userVerification defaults to preferred.

    Recorded by the check
    fact webauthn-user-verification = preferred
  • Default resident key

    preferred

    authenticatorSelection.residentKey defaults to preferred.

    Recorded by the check
    fact webauthn-resident-key = preferred
  • User verification enforced by default

    yes

    Both verifiers require the UV flag unless requireUserVerification: false is passed: a registration and an assertion with only UP set were refused with User verification was required, but user could not be verified and User verification required, but user could not be verified.

    Recorded by the check
    fact webauthn-uv-enforced-by-default = yes
    evidence webauthn-uv-enforced-by-default: registration with the UV flag clear: throws User verification was required, but user could not be verified
  • Assertion signature encoding

    der

    A correct ES256 assertion with an ASN.1 DER signature verified.

    Recorded by the check
    fact webauthn-assertion-encoding = der
  • Raw r‖s assertion signature

    rejected

    The same assertion with a raw 64-byte r‖s signature threw an ASN.1 parsing error whose wording depends on the signature bytes.

    Recorded by the check
    fact webauthn-raw-signature = rejected
  • High-S assertion signature

    accepted

    A DER signature with s replaced by n − s verified.

    Recorded by the check
    fact webauthn-high-s = accepted
  • Signature counter going back

    rejected

    A counter below the stored value threw Response counter value 3 was lower than expected 5; a counter of 0 after a stored 5 was refused too, while 0 after a stored 0 verified.

    Recorded by the check
    fact webauthn-counter-regression = rejected
    evidence webauthn-counter-regression: counter 0 after a stored 5: throws Response counter value 0 was lower than expected 5; counter 0 after a stored 0: verified
  • Ed25519 credentials

    supported

    An Ed25519 (COSE -8) credential registered and authenticated successfully.

    Recorded by the check
    fact webauthn-ed25519 = supported
    evidence webauthn-ed25519: verified

Usage

Generate registration and authentication options that ask for the same user-verification policy the verifiers enforce by default.
import { generateAuthenticationOptions, generateRegistrationOptions } from "@simplewebauthn/server";

const rpID = "example.com";

// Registration: the options ask for UV only as "preferred"...
const registration = await generateRegistrationOptions({
  rpName: "Example",
  rpID,
  userName: "user@example.com",
  authenticatorSelection: { residentKey: "required", userVerification: "required" },
});

// ...while verifyRegistrationResponse() and verifyAuthenticationResponse()
// require the UV flag by default. Ask for "required" up front (as here), or pass
// requireUserVerification: false when a security key without a PIN is acceptable.
const authentication = await generateAuthenticationOptions({ rpID, userVerification: "required" });

console.log("algorithms:", registration.pubKeyCredParams.map((param) => param.alg).join(", "));
console.log("uv:", registration.authenticatorSelection?.userVerification, authentication.userVerification);
console.log("challenge bytes:", Buffer.from(authentication.challenge, "base64url").length);

The snippet harness runs every sample verbatim in the container image node:24.22.0-bookworm-slim, and the run fails unless the output matches.

Setup
npm init -y >/dev/null && npm install --no-audit --no-fund @simplewebauthn/server@14.0.3
Command
node usage.mts
Output must match (regular expression)
algorithms: -48, -8, -7, -257\nuv: required required\nchallenge bytes: 32

Libraries