Passkeys (FIDO2 / WebAuthn) are phishing-resistant credentials backed by the user’s device’s secure enclave or hardware key. Pylon ships a complete WebAuthn server implementation in the binary — challenge mint, signature verification (ES256 + Ed25519), counter-regression detection, key management endpoints. NoDocumentation Index
Fetch the complete documentation index at: https://docs.pylonsync.com/llms.txt
Use this file to discover all available pages before exploring further.
@simplewebauthn or webauthn-rs to bolt on.
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/:
| Endpoint | Method | Auth | Purpose |
|---|---|---|---|
/passkey/register/begin | POST | session | Issue a registration challenge |
/passkey/register/finish | POST | session | Persist a new credential after the authenticator signs |
/passkey/login/begin | POST | none | Issue an assertion challenge |
/passkey/login/finish | POST | none | Verify the assertion → mint session |
/passkey/keys | GET | session | List the caller’s passkeys |
/passkey/keys/:id | DELETE | session | Revoke a passkey by id |
Registering a passkey
The user must already be signed in (this is a credential add for an existing account).challenge is single-use — it’s consumed inside /finish regardless of success or failure, so a replay attempt gets 401 BAD_CHALLENGE. Without the right challenge cookie, the call to /finish fails fast.
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.
| Status | Code | Reason |
|---|---|---|
| 401 | PASSKEY_VERIFY_FAILED | bad challenge, bad signature, wrong origin/rpId, counter regression, unknown credential |
Counter regression
WebAuthn authenticators increment a 32-bit counter on every signature. A clone of an authenticator (or a leaked private key) would replay an old counter value — Pylon’s verify path rejects any assertion whosesign_count is less than or equal to the stored value (with one exception: authenticators that legitimately don’t 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 will reject every login with PASSKEY_VERIFY_FAILED.
For subdomain apps, RP_ID should be the parent — set to 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 — drop it into 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