← Back to blog

Add passkeys to an Own Auth application


Own Auth passkeys use WebAuthn to register a public key credential and authenticate by proving control of its private key. The server verifies the relying party, browser origin, challenge, signature, credential ownership, and user verification before it creates a session.

Configure the relying party

The relying party ID is the domain scope for the credential. It is not a full URL. List every exact browser origin that may run the WebAuthn ceremony. Production origins use HTTPS. Localhost can be used during local development under browser WebAuthn rules.

auth.ts
import { createOwnAuth } from "own-auth";

export const auth = createOwnAuth({
  tokenPepper: process.env.OWN_AUTH_TOKEN_PEPPER!,
  passkeys: {
    rpId: "example.com",
    rpName: "Example",
    origins: [
      "https://example.com",
      "https://app.example.com",
    ],
  },
});

Apply npx own-auth migrate before enabling the interface. The passkey migration creates credential and challenge storage. Challenges are hashed, expire, and can be consumed only once.

Register from an active session

Passkey registration changes how the account can sign in, so require a valid current session and user verification. The browser helper runs the complete options and verification ceremony through the Own Auth HTTP handler.

register-passkey.ts
import { registerPasskey } from "own-auth/passkeys";

import { authClient } from "./auth-client";

export async function addPasskey(name: string) {
  await registerPasskey(authClient, { name });
  return authClient.listPasskeys();
}

Give the credential a device label that the user can recognize later. A label is display metadata, not verified device identity. Keep the complete credential list on an account security screen so the user can identify and revoke an old authenticator.

Support usernameless sign in

Own Auth requests discoverable credentials by default. A compatible authenticator can therefore offer an account without asking for an email first. Older credentials that are not discoverable use a username first flow where the backend resolves the user and requests options for that user ID.

passkey-sign-in.ts
import { authenticateWithPasskey } from "own-auth/passkeys";

import { authClient } from "./auth-client";

export async function signInWithPasskey() {
  const session = await authenticateWithPasskey(authClient);
  return {
    user: {
      id: session.user.id,
      email: session.user.email,
      name: session.user.name,
      imageUrl: session.user.imageUrl,
    },
    expiresAt: session.session.expiresAt,
  };
}

The HTTP handler sets the HttpOnly session cookie after successful authentication. Browser JavaScript receives the public session result, not the raw session token. A passkey can also complete a pending MFA challenge through authenticateWithPasskey(authClient, { mfa: true }).

Manage credentials without lockout

manage-passkeys.ts
const { passkeys } = await authClient.listPasskeys();

await authClient.renamePasskey({
  passkeyId: passkeys[0].id,
  name: "Work laptop",
});

await authClient.revokePasskey({
  passkeyId: passkeys[0].id,
});

Before revocation, make sure another usable sign in or recovery method remains. A synced passkey can appear on several devices, while a device bound passkey may exist only on one authenticator. The account screen should not claim that one stored credential equals one physical device.

Verify the WebAuthn boundary

Test registration and authentication on every supported production origin. Reject an origin outside the allowlist, a relying party mismatch, an expired challenge, a replayed response, the wrong user credential, and a revoked passkey. Confirm that user verification is required and that the next protected request resolves the expected Own Auth user.

Design the unavailable authenticator path

A passkey may be synced by a platform, stored on a security key, or bound to one device. The sign in screen needs another supported route when the expected authenticator is unavailable. That route can be a password, magic link, another passkey, or a deliberate account recovery process. It should not weaken WebAuthn verification or bypass current account state.

Ask users to register more than one passkey when the product depends on passkeys as the primary sign in method. Make the credential list and recovery method visible before they remove their last broadly available authenticator.

Handle browser capability explicitly

Show the passkey action only in a secure browser context with WebAuthn support. A capability check controls the interface, not server policy. The server still validates every response. If the browser cancels the platform prompt, keep the user on the current screen and offer another method without recording the cancellation as invalid credentials.

Separate direct sign in from MFA

The same passkey credential can authenticate directly or complete a pending MFA challenge, but the server operation is different. Direct authentication begins without an MFA challenge. MFA completion must be bound to the pending challenge established by the first factor. Never let a client supplied flag convert an unrelated WebAuthn response into a second factor result.

After registration, rename, or revocation, reload the passkey list from the server. Do not update only an optimistic local list because a failed security change could leave the interface claiming a credential exists or was removed when server state says otherwise.

Record the browser and operating system combinations the product supports, then repeat the ceremony after major browser, WebView, or domain changes. A passkey created for one relying party ID cannot be repaired by client code when the production hostname later moves outside that domain scope.

Do not log WebAuthn responses, raw challenge values, session tokens, or credential identifiers in general analytics. Operational events can record the outcome, flow type, safe error code, and whether the ceremony was registration, direct authentication, or MFA completion.