Authentication
Passkeys
WebAuthn-based passwordless authentication using biometrics, security keys, or device credentials.
Passkeys provide passwordless authentication using the WebAuthn standard. Users authenticate with their device's biometric sensor (Face ID, Touch ID, Windows Hello), a hardware security key (YubiKey), or a device PIN. Passkeys are phishing-resistant, eliminate password reuse, and provide the strongest form of user authentication available today.
Banata Auth supports passkeys via the passkey plugin, built on the WebAuthn / FIDO2 specification.
How It Works
Passkeys involve two distinct flows: registration (creating a credential) and authentication (using a credential to sign in).
Registration Flow
1. User is signed in and navigates to security settings
2. User clicks "Add Passkey"
3. Client calls authClient.passkey.addPasskey()
4. Server generates a challenge and returns registration options
5. Browser triggers the WebAuthn API (navigator.credentials.create())
6. User completes the biometric/PIN/security key prompt
7. Browser returns the attestation response to the client
8. Client sends the attestation to the server for verification
9. Server validates the response and stores the credential public key
10. Passkey is now registered and ready for authenticationAuthentication Flow
1. User navigates to the sign-in page
2. User clicks "Sign in with Passkey"
3. Client calls authClient.signIn.passkey()
4. Server generates an authentication challenge
5. Browser triggers the WebAuthn API (navigator.credentials.get())
6. User completes the biometric/PIN/security key prompt
7. Browser returns the assertion response to the client
8. Client sends the assertion to the server for verification
9. Server validates the signature against the stored public key
10. Session is created and user is authenticatedConfiguration
Enable passkeys in your BanataAuthConfig:
// convex/banataAuth/auth.ts
function buildConfig(): BanataAuthConfig {
return {
appName: "My App",
siteUrl: process.env.SITE_URL!,
secret: process.env.BETTER_AUTH_SECRET!,
authMethods: {
passkey: true, // Enable passkey (WebAuthn) authentication
},
passkey: {
rpId: process.env.PASSKEY_RP_ID ?? "localhost",
rpName: "My App",
origin: process.env.PASSKEY_ORIGIN ?? "http://localhost:3000",
},
};
}Passkey Configuration Options
| Option | Type | Required | Description |
|---|---|---|---|
rpId | string | Yes | Relying Party ID. Must be the domain of your app (e.g., "myapp.com"). Use "localhost" in development. |
rpName | string | Yes | Human-readable name shown in the browser's passkey prompt. |
origin | string | Yes | The full origin of your app (e.g., "https://myapp.com"). Must match the page origin exactly. |
Important: The
rpIdmust be a valid domain that matches or is a registrable suffix of the page origin. For example, if your app is athttps://app.mycompany.com, therpIdcan be"app.mycompany.com"or"mycompany.com", but not"other.com".
Environment Variables
# .env
PASSKEY_RP_ID=myapp.com
PASSKEY_ORIGIN=https://myapp.comIn development:
# .env.local
PASSKEY_RP_ID=localhost
PASSKEY_ORIGIN=http://localhost:3000Browser and Platform Support
Passkeys are supported on all modern browsers and platforms:
| Platform | Support |
|---|---|
| Chrome (Desktop) | 67+ (Windows Hello, security keys) |
| Chrome (Android) | 67+ (fingerprint, screen lock) |
| Safari (macOS) | 13+ (Touch ID, security keys) |
| Safari (iOS) | 14+ (Face ID, Touch ID) |
| Firefox | 60+ (security keys); 122+ (platform authenticators) |
| Edge | 18+ (Windows Hello, security keys) |
Cross-Device Authentication
Modern passkey implementations support cross-device authentication, where a user can use their phone to authenticate on a desktop browser via Bluetooth proximity. This is handled natively by the browser and operating system -- no additional configuration is required.
Client-Side API
Register a Passkey
Users must be signed in to register a passkey. This is typically done in a security settings page:
import { authClient } from "@/lib/auth-client";
const { data, error } = await authClient.passkey.addPasskey();
if (error) {
// Handle errors:
// - "WebAuthn not supported" (browser doesn't support passkeys)
// - "Registration failed" (user cancelled or hardware error)
// - "Passkey already registered" (duplicate credential)
console.error(error.message);
} else {
console.log("Passkey registered successfully");
}Sign In with a Passkey
const { data, error } = await authClient.signIn.passkey();
if (error) {
// Handle errors:
// - "Authentication failed" (invalid credential or user cancelled)
// - "No passkeys found" (no credentials registered for this rpId)
console.error(error.message);
} else {
// data contains the user and session
console.log(data.user);
console.log(data.session);
window.location.href = "/dashboard";
}List Registered Passkeys
const { data, error } = await authClient.passkey.listPasskeys();
if (data) {
// data is an array of registered passkeys
data.forEach((passkey) => {
console.log(passkey.id); // Credential ID
console.log(passkey.createdAt); // When it was registered
console.log(passkey.deviceType); // "platform" or "cross-platform"
});
}Delete a Passkey
const { error } = await authClient.passkey.deletePasskey({
id: "credential_id_here",
});
if (!error) {
console.log("Passkey removed");
}Complete Registration Component
"use client";
import { authClient } from "@/lib/auth-client";
import { useState, useEffect } from "react";
export function PasskeySettings() {
const [passkeys, setPasskeys] = useState<any[]>([]);
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
useEffect(() => {
loadPasskeys();
}, []);
async function loadPasskeys() {
const { data } = await authClient.passkey.listPasskeys();
if (data) setPasskeys(data);
}
async function handleAdd() {
setError("");
setLoading(true);
const { error } = await authClient.passkey.addPasskey();
if (error) {
setError(error.message ?? "Failed to register passkey");
} else {
await loadPasskeys();
}
setLoading(false);
}
async function handleDelete(id: string) {
const { error } = await authClient.passkey.deletePasskey({ id });
if (!error) {
setPasskeys((prev) => prev.filter((p) => p.id !== id));
}
}
return (
<div>
<h2>Passkeys</h2>
<p>
Use your device's biometric sensor or security key to sign in
without a password.
</p>
{passkeys.length > 0 ? (
<ul>
{passkeys.map((passkey) => (
<li key={passkey.id}>
<span>
{passkey.deviceType === "platform"
? "This device"
: "Security key"}{" "}
-- Added {new Date(passkey.createdAt).toLocaleDateString()}
</span>
<button onClick={() => handleDelete(passkey.id)}>
Remove
</button>
</li>
))}
</ul>
) : (
<p>No passkeys registered yet.</p>
)}
{error && <p style={{ color: "red" }}>{error}</p>}
<button onClick={handleAdd} disabled={loading}>
{loading ? "Waiting for device..." : "Add Passkey"}
</button>
</div>
);
}Sign-In Page with Passkey Option
"use client";
import { authClient } from "@/lib/auth-client";
import { useState } from "react";
export function SignInPage() {
const [error, setError] = useState("");
async function handlePasskeySignIn() {
setError("");
const { data, error } = await authClient.signIn.passkey();
if (error) {
setError(error.message ?? "Passkey sign-in failed");
} else {
window.location.href = "/dashboard";
}
}
return (
<div>
<h1>Sign In</h1>
<button onClick={handlePasskeySignIn}>
Sign in with Passkey
</button>
{error && <p style={{ color: "red" }}>{error}</p>}
<hr />
<p>Or sign in with email and password:</p>
{/* Email/password form here */}
</div>
);
}Resident vs. Non-Resident Credentials
WebAuthn defines two types of credentials:
| Type | Also Called | Description |
|---|---|---|
| Resident (discoverable) | Passkey | Stored on the device. The user does not need to provide a username first -- the browser can enumerate available credentials for the site. |
| Non-resident (non-discoverable) | Server-side credential | The credential ID is stored on the server. The user must identify themselves first (e.g., enter their email), then the server provides the credential ID to the browser for the authentication challenge. |
Banata Auth defaults to resident credentials (true passkeys). This means:
- Users can sign in by clicking "Sign in with Passkey" without entering an email first
- The browser shows a credential picker if multiple passkeys are registered for the site
- Credentials sync across devices via iCloud Keychain (Apple), Google Password Manager (Android/Chrome), or Windows Hello
Combining with Other Methods
Passkeys work well alongside traditional authentication methods. Users can register a passkey after signing in with email/password, and then use the passkey for future sign-ins:
authMethods: {
emailPassword: true, // Traditional fallback
passkey: true, // Passwordless primary
magicLink: true, // Another passwordless option
},
passkey: {
rpId: process.env.PASSKEY_RP_ID ?? "localhost",
rpName: "My App",
origin: process.env.PASSKEY_ORIGIN ?? "http://localhost:3000",
},A recommended UI pattern is to show the passkey option prominently, with email/password as a secondary option:
+----------------------------------+
| [Sign in with Passkey] | <-- Primary, prominent
+----------------------------------+
| |
| -------- or continue with ----- |
| |
| Email: [____________] |
| Password: [____________] |
| [Sign In] |
+----------------------------------+Audit Events
| Event | When |
|---|---|
passkey.registered | New passkey credential registered |
passkey.deleted | Passkey credential removed |
passkey.authenticated | Successful authentication via passkey |
session.created | Session created after passkey authentication |
See the Audit Logs guide for more details.
Security Advantages Over Passwords
| Property | Passwords | Passkeys |
|---|---|---|
| Phishing resistance | Low -- users can enter passwords on fake sites | High -- credentials are bound to the origin |
| Credential reuse | Common -- users reuse passwords across sites | Impossible -- each credential is site-specific |
| Brute force | Possible if rate limiting is weak | Not applicable -- no shared secret to guess |
| Data breaches | Leaked password hashes can be cracked | Public keys only -- useless to attackers |
| User friction | High -- users must remember passwords | Low -- biometric or PIN, no memorization |
Passkeys are the most secure user authentication method available in web browsers today. The private key never leaves the user's device, and authentication requires physical presence (biometric or security key interaction), making remote attacks effectively impossible.
Security Considerations
- Always validate the origin -- The
originin your passkey config must exactly match the page origin. Mismatches will cause authentication failures. - Use HTTPS in production -- WebAuthn requires a secure context. It works on
localhostfor development but requires HTTPS in production. - Encourage multiple passkeys -- Users should register passkeys on multiple devices to avoid lockout if a single device is lost.
- Provide fallback methods -- Not all users have passkey-capable devices. Always offer an alternative sign-in method (email/password, magic link, etc.).
- Credential backup -- Platform passkeys (iCloud Keychain, Google Password Manager) are backed up and sync across devices. Hardware security key credentials are not -- losing the key means losing access.
Troubleshooting
"WebAuthn not supported"
The user's browser does not support the WebAuthn API. This is rare on modern browsers but can happen on older versions or certain embedded browser views. Check window.PublicKeyCredential before showing the passkey option.
"Registration cancelled"
The user dismissed the browser's passkey prompt. This is not an error -- simply allow the user to try again.
"Origin mismatch"
The origin in your passkey config does not match the actual page origin. Ensure PASSKEY_ORIGIN matches exactly (including protocol and port).
"Passkey not found"
No registered credentials match the rpId. This can happen if:
- The user hasn't registered a passkey for this site
- The
rpIdchanged between registration and authentication - The user's passkey was deleted from their device
What's Next
- Email OTP -- Passwordless authentication via email codes
- Multi-Factor Auth -- Add TOTP as a second factor
- Email & Password -- Traditional authentication
- Social OAuth -- Sign in with Google, GitHub, etc.