@simplewebauthn or webauthn-rs.
Supported algorithms
The verify path supports the two algorithms every shipping authenticator implements:- ES256 (
alg=-7) — ECDSA P-256, the default for Apple / iCloud Keychain, 1Password, most YubiKeys. - Ed25519 (
alg=-8) — Edwards-curve, used by newer Linux + Android authenticators.
Endpoints
All under/api/auth/:
Registering a passkey
The user must already be signed in (this is a credential add for an existing account).challenge is single-use. verify_registration consumes the matching record from the PasskeyStore on every call (success or failure), so a replay attempt gets 401 PASSKEY_REGISTER_FAILED. Pylon accepts only fmt = "none" attestation (the passkey default that Touch ID, Face ID, Windows Hello, and 1Password use). It rejects packed, tpm, and android-key because verifying them needs a FIDO MDS trust store that Pylon does not ship.
Signing in with a passkey
- Decode
clientDataJSONand confirmorigin == PYLON_WEBAUTHN_ORIGINandtype == "webauthn.get". - Confirm the
rpIdHashinauthenticatorDatamatches SHA-256(PYLON_WEBAUTHN_RP_ID). - Recompute the signed payload (
authenticatorData || SHA-256(clientDataJSON)). - ECDSA-P256 or Ed25519 verify against the stored public key.
- Counter-regression check: the new sign count must be strictly greater than the stored value (with 0 → 0 as the only allowed equality, for authenticators that don’t increment).
- Update
sign_count+last_used_at; mint a session.
Counter regression
WebAuthn authenticators increment a 32-bit counter on every signature. A clone of an authenticator (or a leaked private key) replays an old counter value. Pylon’s verify path rejects any assertion whosesign_count is less than or equal to the stored value. One exception: authenticators that do not implement counters keep emitting 0, which is permitted as long as the stored value is also 0.
When a regression fires, the assertion is rejected with PASSKEY_VERIFY_FAILED. The right operational response is to revoke that credential and have the user re-register.
Listing + revoking keys
user_id must match the stored passkey’s user_id).
Configuration
localhost / https://localhost for dev. Production must set both. A mismatch between rpIdHash in the assertion and the configured RP_ID rejects every login with PASSKEY_VERIFY_FAILED.
For subdomain apps, set RP_ID to the parent. Set example.com so credentials work across app.example.com and admin.example.com. Browsers enforce the same-registrable-domain rule client-side.
Composing the verify path
If you need a custom passkey flow (e.g., step-up auth for a specific high-value action), import the building block directly:verify_assertion does the full ES256 / Ed25519 verify, counter check, and metadata update. The pylon-auth crate is Send + Sync, so you can call it from any handler.
Where to go next
- TOTP / 2FA — software-token 2FA for accounts without a passkey
- Trusted devices — remember-this-browser cookie that skips the second factor on re-login
- Sessions — what
/login/finishmints