Once a user has enrolled a WebAuthn credential (see the enrolment topic), authentication uses it. Two shapes of usage:
- Primary authentication — the user signs in with a passkey instead of a password. No password is required.
- Second-factor authentication — the user signs in with a password (or another primary factor), then proves possession of the WebAuthn credential as a second step.
Both use the same browser API; the difference is what the platform decides about the resulting session.
Primary authentication (passwordless)
Section titled “Primary authentication (passwordless)”const { loginWithPasskey } = useIntelliAuth()
async function signIn() { const result = await loginWithPasskey() if (result.kind === 'success') { // The user is signed in. Tokens are in the SDK's session. navigate('/dashboard') } else if (result.kind === 'cancelled') { // User dismissed the prompt. } else { // Inspect result.error. }}The SDK:
- Asks the platform to begin a WebAuthn authentication. The platform returns a challenge and (for resident-key flows) no allowList — the browser will let the user pick from credentials available on the device.
- Calls
navigator.credentials.get()with the challenge. - Ships the resulting assertion back to the platform to finish.
- Receives the session and tokens.
The user sees the browser's native sheet listing their passkeys. They pick one, confirm with Touch ID / Face ID / Windows Hello / PIN, and they're signed in.
Second-factor flow
Section titled “Second-factor flow”After a password sign-in that requires MFA, the platform returns a "MFA required" state with a flow_id. The SDK calls the WebAuthn step:
const { completeMfaStep } = useIntelliAuth()
async function completeMfa(flowId: string) { const result = await completeMfaStep(flowId, 'webauthn') // same shape as loginWithPasskey}The shape is the same WebAuthn ceremony; the difference is the flow already has a partial session bound to it, so the assertion only needs to prove "yes, this user holds the credential."
What the platform does on the assertion
Section titled “What the platform does on the assertion”For every WebAuthn assertion:
- Verifies the signature against the stored public key.
- Checks the
signCountis greater than the previous value (defence against cloned authenticators). - Validates the
originmatches the relying-party id. - Validates the
userHandlematches a known user.
A mismatch on any of these fails the assertion with a specific error code. The audit log records the failure including the WebAuthn-specific reason.
Sign-count anomalies
Section titled “Sign-count anomalies”Hardware keys (YubiKey, etc.) increment a signCount on every use. The platform stores the highest seen value and rejects an assertion with a count lower than or equal to that. This catches a specific attack: an attacker who cloned the credential to a second device cannot replay; the first time the clone signs, the platform sees a counter regression and refuses.
Some platform authenticators (notably iCloud Keychain passkeys) deliberately keep the counter at 0 — passkeys sync across devices, so a strictly increasing counter would break sync. For these credentials, IntelliAuth disables the counter check at enrolment time. You do not need to do anything; the platform decides based on the authenticator's attestation.
Allow-list mode (for non-resident credentials)
Section titled “Allow-list mode (for non-resident credentials)”For older non-resident WebAuthn credentials, the platform must tell the browser which credential ids are acceptable. The SDK handles this by:
- The user types their username at sign-in.
- The SDK begins a WebAuthn authentication with the platform, passing the user identifier.
- The platform returns the user's credential ids in the
allowCredentialsfield. - The browser asks the connected authenticator about those specific ids.
If you build new flows, prefer resident keys (passwordless) — the user does not need to remember a username, and the UX is shorter.
When the user has multiple credentials
Section titled “When the user has multiple credentials”A user can enrol several credentials: a phone passkey, a laptop passkey, a hardware key. At sign-in, the browser presents the credentials currently available on the device they're on. If they're at their work laptop, they see the laptop passkey first; their phone passkey shows in the "Use a different device" sub-menu.
This is browser behaviour — you don't influence it from your code. What you do influence: at enrolment time, name the credentials clearly so the user recognises them in management screens later.
Falling back when WebAuthn fails
Section titled “Falling back when WebAuthn fails”If loginWithPasskey() returns error, do not loop the user back to the same prompt. Offer:
- "Try again" (in case it was a tap miss).
- "Use a password instead" (fall back to a different primary).
- "Use a backup code" (if they enrolled recovery codes).
Designing fallbacks well is more important than the WebAuthn happy path. The happy path is short and obvious; the fallback is where users get stuck.