Wocha Docs

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:

selfservice:
  methods:
    webauthn:
      enabled: true
      config:
        passwordless: true   # allow passkey-only login
        rp:
          id: auth.example.com          # top-level domain (no scheme/port)
          display_name: My Application
          origins:
            - https://auth.example.com  # exact origin of the login page

Important settings:

SettingDescription
rp.idMust match the domain users authenticate against (e.g. example.com)
rp.originsMust include the exact URL (scheme + host + port) of pages that trigger WebAuthn
passwordlessWhen 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.

Set WOCHA_KRATOS_URL in your Next.js app if Kratos is not at the same origin as WOCHA_ISSUER:

WOCHA_KRATOS_URL=https://auth.example.com

Next.js route handlers

Mount the passkey API routes in your App Router project. These handlers proxy to Kratos WebAuthn endpoints and manage CSRF cookies.

// app/api/auth/passkeys/register/options/route.ts
import { createPasskeyHandlers } from "@wocha/nextjs";
 
const { registerOptions } = createPasskeyHandlers({
  clientId: process.env.WOCHA_CLIENT_ID!,
  clientSecret: process.env.WOCHA_CLIENT_SECRET!,
  issuer: process.env.WOCHA_ISSUER!,
  kratosUrl: process.env.WOCHA_KRATOS_URL, // optional
});
 
export const POST = registerOptions;

Create matching routes for all four endpoints:

RouteHandler export
POST /api/auth/passkeys/register/optionsregisterOptions
POST /api/auth/passkeys/register/verifyregisterVerify
POST /api/auth/passkeys/authenticate/optionsauthenticateOptions
POST /api/auth/passkeys/authenticate/verifyauthenticateVerify

Each handler proxies to the corresponding Kratos self-service endpoints:

  • GET /self-service/login/flows?id={flowId} — includes WebAuthn challenge options
  • POST /self-service/login — submit webauthn_login credential
  • GET /self-service/registration/flows?id={flowId} — registration challenge
  • POST /self-service/registration — submit webauthn_register credential

React hooks

usePasskey

import { usePasskey } from "@wocha/react";
 
function PasskeySignIn() {
  const {
    isSupported,
    register,
    authenticate,
    isRegistering,
    isAuthenticating,
    error,
  } = usePasskey();
 
  if (!isSupported) {
    return <p>Passkeys are not supported in this browser.</p>;
  }
 
  return (
    <div>
      <button onClick={() => void authenticate()} disabled={isAuthenticating}>
        Sign in with passkey
      </button>
      <button
        onClick={() => void register({ displayName: "My MacBook" })}
        disabled={isRegistering}
      >
        Add passkey
      </button>
      {error && <p role="alert">{error.message}</p>}
    </div>
  );
}

The hook calls your Next.js passkey routes at /api/auth/passkeys by default. Override with:

usePasskey({ apiBase: "/custom/api/passkeys" });

Low-level utilities

For custom UI, use the utility functions directly:

import {
  isWebAuthnSupported,
  createPasskeyCredential,
  getPasskeyCredential,
  encodeCredentialForServer,
} from "@wocha/react";
 
// Fetch options from your BFF, then:
const credential = await createPasskeyCredential(optionsFromServer);
const payload = encodeCredentialForServer(credential);
// POST payload to /register/verify or /authenticate/verify

UI components

PasskeyButton

import { PasskeyButton } from "@wocha/ui";
 
<PasskeyButton
  mode="authenticate"
  onSuccess={() => console.log("Signed in")}
  onError={(err) => console.error(err)}
/>

For registration (e.g. in account settings):

<PasskeyButton mode="register" displayName="Work laptop">
  Add a passkey
</PasskeyButton>

SignIn with passkeys

Enable the passkey button on the pre-built SignIn component:

import { SignIn } from "@wocha/ui";
 
<SignIn passkeys providers={["google", "github"]} />

When passkeys={true}, a "Sign in with passkey" button appears between the primary sign-in action and social providers.


Conditional UI (passkey autofill)

Browsers that support Conditional UI can show passkeys in the username field autofill menu. Use useConditionalUI to enable this:

import { useConditionalUI } from "@wocha/react";
 
function LoginForm() {
  const { isAvailable, startConditionalUI, abort } = useConditionalUI();
 
  return (
    <>
      <input
        type="email"
        name="username"
        autoComplete="username webauthn"
        placeholder="Email"
      />
      {!isAvailable && (
        <button type="button" onClick={() => void startConditionalUI()}>
          Use passkey
        </button>
      )}
    </>
  );
}

Requirements for conditional UI:

  1. Input must have autoComplete="username webauthn" (or webauthn alone)
  2. Page must be served over HTTPS (or localhost)
  3. Browser must support PublicKeyCredential.isConditionalMediationAvailable()

Call abort() when navigating away to cancel any pending WebAuthn prompt.


Browser support matrix

BrowserPasskeysConditional UINotes
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+PartialPlatform 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.id and rp.origins in 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.

Troubleshooting

SymptomLikely cause
"WebAuthn is not supported"Non-HTTPS origin, or browser too old
"WebAuthn is not enabled" in API responseKratos webauthn.enabled: false or flow missing webauthn nodes
SecurityError in browserrp.id / origins mismatch with current URL
CSRF errors from KratosCookies not forwarded — ensure credentials: "include" on fetch
Passkey prompt never appearsUser has no registered passkeys for this RP ID

See also: Getting started

On this page