Passkeys (WebAuthn)
Phishing-resistant authentication with passkeys in Wocha SDKs and Kratos
Passkeys are a modern, phishing-resistant authentication method built on the Web Authentication (WebAuthn) standard. Wocha uses Ory Kratos as the identity backend, which supports passkey registration and passwordless login natively. The Wocha SDKs expose this capability through React hooks, Next.js route handlers, and UI components.
What are passkeys?
A passkey is a cryptographic credential stored on a user's device (or synced via iCloud Keychain, Google Password Manager, etc.). Instead of typing a password, the user verifies their identity with a biometric or device PIN.
Benefits:
- Phishing-resistant — credentials are bound to your site's domain (relying party ID)
- No shared secrets — the private key never leaves the device
- Better UX — one tap or biometric instead of remembering passwords
- Sync across devices — platform providers sync passkeys where enabled
Enabling passkeys in Wocha
Passkeys are configured in Kratos. Ensure WebAuthn is enabled in your Kratos configuration:
Important settings:
| Setting | Description |
|---|---|
rp.id | Must match the domain users authenticate against (e.g. example.com) |
rp.origins | Must include the exact URL (scheme + host + port) of pages that trigger WebAuthn |
passwordless | When true, users can sign in with a passkey alone (no password) |
For Wocha Cloud tenants, WebAuthn is enabled by default on the hosted auth UI. For self-hosted deployments, update your Kratos config and redeploy.
Passkeys count as multi-factor authentication
A passkey whose authenticator verifies the user — Touch ID, Face ID, Windows Hello, or a device PIN — is a multi-factor cryptographic authenticator (NIST SP 800-63B): possession of the key plus something the user is or knows. Wocha treats such a sign-in as urn:wocha:aal2, so:
- A passkey user is not asked for a TOTP or SMS code after signing in, even in an organisation that requires MFA.
- ID and access tokens carry
acr: urn:wocha:aal2andamrincludingswk(orhwk) andmfa. urn:wocha:aal3is reported only for a device-bound key, such as a hardware security key. Synced passkeys — including a phone passkey used through the QR code flow — are AAL2 at most, as NIST SP 800-63B-4 requires.- Registering a passkey completes required MFA setup.
- A step-up request (
acr_values=urn:wocha:aal2) is completed with the passkey.
Wocha verifies the passkey itself: the ceremony runs on Wocha's hosted sign-in, which requires user verification and refuses any assertion it cannot prove was user-verified. An authenticator that cannot verify the user (a PIN-less security key) can still be enrolled as a second factor, but cannot sign in passwordless.
Sign in with passkey
Passkeys are bound to one domain. Wocha runs every passkey sign-in on its own hosted page, so one passkey works in every Wocha app, and your app never handles WebAuthn. Your "Sign in with passkey" button sends the user to the hosted sign-in, which opens straight at the passkey step: the user taps once, confirms with Face ID / Touch ID / Windows Hello, and returns to your app signed in. An account without a passkey sees its usual sign-in methods.
If you already know who is signing in, pass their email as loginHint and the identifier step is skipped as well.
Next.js
On the server, passkeySignInUrl(returnTo, basePath, loginHint) returns the same link.
React (browser apps)
UI components
Other frameworks
| SDK | Passkey sign-in |
|---|---|
@wocha/remix | useSignIn()("/dashboard", { method: "passkey" }) or passkeySignInUrl() |
@wocha/sveltekit | signIn("/dashboard", "/auth", { method: "passkey" }) or passkeySignInUrl() |
@wocha/nuxt | useWochaAuth().signIn("/dashboard", { method: "passkey" }) |
@wocha/vue | signIn({ method: "passkey" }) |
@wocha/angular | authService.signIn({ method: "passkey" }) |
@wocha/react-native | signIn({ method: "passkey" }) |
Each also accepts loginHint.
Any OAuth client
Add two parameters to the authorization request:
| Parameter | Meaning |
|---|---|
login_method=passkey | Open the hosted sign-in at the passkey step |
login_hint=<email> | The identifier you already know (OIDC Core), optional |
Adding passkeys to an account
Users add and remove passkeys in their Wocha account settings (the Passkeys section of the hosted settings page). Link to it from your app, or use the enrolment helpers below.
PasskeyButton in register mode uses usePasskey().register() from @wocha/react, which calls your Next.js passkey routes (/api/auth/passkeys by default):
| Route | Handler export |
|---|---|
POST /api/auth/passkeys/register/options | registerOptions |
POST /api/auth/passkeys/register/verify | registerVerify |
Deprecated: embedded passkey sign-in
Earlier SDK versions ran the passkey sign-in inside your app. That only created a Wocha session in your server; Wocha could not verify the assertion, and the bridge that turned it into an app sign-in (POST {authBasePath}/passkey/continue) now answers 403 passkey_bridge_unavailable. These APIs are deprecated and will be removed in the next major version:
| Deprecated | Use instead |
|---|---|
createPasskeyHandlers().authenticateOptions / authenticateVerify | signIn(returnTo, { method: "passkey" }) / <PasskeySignInButton /> |
usePasskey().authenticate() | useWochaAuth().login({ method: "passkey" }) |
useConditionalUI() | login({ method: "passkey" }); passkey autofill on the hosted page is planned |
passkeyContinueUrl() | passkeySignInUrl() |
PasskeyButton mode="authenticate" and <SignIn passkeys /> already use the hosted sign-in.
Browser support matrix
| Browser | Passkeys | Conditional UI | Notes |
|---|---|---|---|
| Chrome 108+ | ✅ | ✅ (109+) | Sync via Google Password Manager |
| Safari 16+ | ✅ | ✅ (16+) | Sync via iCloud Keychain |
| Firefox 119+ | ✅ | ❌ | Passkeys work; autofill not yet supported |
| Edge 108+ | ✅ | ✅ | Same engine as Chrome |
| Safari iOS 16+ | ✅ | ✅ | Face ID / Touch ID |
| Chrome Android 108+ | ✅ | Partial | Platform authenticator required |
Use isWebAuthnSupported() and isConditionalMediationAvailable() to feature-detect at runtime.
Security considerations
- Passkey routes must be served from the same origin as your app so Kratos CSRF cookies are forwarded correctly.
- Set
rp.idandrp.originsin Kratos to match your production domain — mismatches cause WebAuthn failures. - Registration typically requires an existing session (settings flow) or can be part of the signup flow depending on your Kratos configuration.
- For OAuth-integrated apps, passkey login establishes a Kratos session; you may still need to complete the Hydra OAuth flow for API tokens depending on your architecture.
- Do not lower
userVerificationwhen building custom UI on the low-level utilities: the BFF handler rejects assertions without user verification, and the sign-in would not count as MFA.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| "WebAuthn is not supported" | Non-HTTPS origin, or browser too old |
| "WebAuthn is not enabled" in API response | Kratos webauthn.enabled: false or flow missing webauthn nodes |
SecurityError in browser | rp.id / origins mismatch with current URL |
| CSRF errors from Kratos | Cookies not forwarded — ensure credentials: "include" on fetch |
| Passkey prompt never appears | User has no registered passkeys for this RP ID |
See also: Getting started