Appearance
YubiKey Support
YubiKeys work with Web3Authn, but registration requires two passkey prompts (two Touch ID / security‑key taps) instead of one. This is expected and only applies to roaming authenticators (like YubiKeys); platform passkeys still use a single prompt.
Why Two Prompts Are Needed
The registration flow needs two things from the authenticator:
WebAuthn registration payload
- Comes from
navigator.credentials.create(). - Contains attestation data and the COSE public key (
attestationObject), which the contract stores and uses to verify future assertions.
- Comes from
WebAuthn PRF outputs
- Web3Authn derives deterministic VRF + NEAR keys from the WebAuthn PRF extension.
- Many browsers only expose PRF results during authentication (
navigator.credentials.get()), not during registration (navigator.credentials.create()), especially for roaming authenticators like YubiKeys.
To handle this, the SDK does:
Call
navigator.credentials.create()- Collects the registration credential (COSE public key + attestation).
- This is what gets sent to the Web3Authn contract for
webauthn_registration.
Immediately call
navigator.credentials.get()bound to the same credential- Uses
allowCredentialswith therawIdfrom the registration step. - Requests
prf.eval.first/secondusing salts derived from the NEAR account ID. - Extracts
getClientExtensionResults().prf.results.first/secondand uses those PRF outputs to derive and encrypt deterministic VRF + NEAR keypairs.
- Uses
That means YubiKey users will see:
- First prompt – creating the passkey (registration).
- Second prompt – authenticating once more to produce PRF outputs for key derivation.
On future logins and transactions, only the usual single navigator.credentials.get() prompt is used; the extra prompt happens only once at registration.
Browser Behavior With Roaming Authenticators
In practice, for many YubiKey + browser + OS combinations:
navigator.credentials.create()returns a registration credential where:getClientExtensionResults().prf.enabled === true- No
prf.results.first/secondare present.
navigator.credentials.get()withextensions.prf.eval:- Returns a 32‑byte PRF secret in:
getClientExtensionResults().prf.results.first(and optionallysecond).
- Returns a 32‑byte PRF secret in:
The SDK’s two‑prompt registration is exactly aligning with this behavior:
- create() → we get the permanent credential and COSE public key for the contract.
- get() → we obtain the PRF outputs that browsers currently only expose on authentication, and use them to derive deterministic wallet keys.
This preserves security (one credential controls both contract and key derivation) while working around PRF‑on‑create limitations for roaming authenticators.
YubiKey Backup (Single Device)
Use a YubiKey as a physical backup for an existing account without a second device or QR flow.
High-Level Flow
Register the YubiKey
- Call
navigator.credentials.create()with the wallet RP ID and user derived fromaccountId. - Immediately follow with
navigator.credentials.get()constrained to the new credential ID to obtain PRF outputs.
- Call
Derive deterministic keys
- Use PRF outputs to derive the deterministic VRF keypair and NEAR ed25519 keypair scoped to the same
accountId. - Encrypt and store these in IndexedDB (same as standard registration).
- Use PRF outputs to derive the deterministic VRF keypair and NEAR ed25519 keypair scoped to the same
Register the authenticator on-chain
- Call the contract’s link-device style method (e.g.,
link_device_register_user) with:vrf_datafrom the YubiKey’s deterministic VRF public key and a fresh VRF challenge.webauthn_registrationfrom the YubiKey registration credential.
- Call the contract’s link-device style method (e.g.,
Add the NEAR public key
- Use the logged-in platform passkey to send an
add_keytransaction for the YubiKey’s deterministic NEAR public key.
- Use the logged-in platform passkey to send an
Result
The same account now supports both:
- The original platform passkey, and
- The YubiKey (as an additional authenticator + NEAR key)