Configure Authentication
Passkeys
WebAuthn-based passwordless authentication using biometrics, security keys, or device credentials.
Passkeys provide passwordless authentication using the WebAuthn standard. Instead of typing a password, your 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 deliver the strongest form of user authentication available in web browsers today.
Banata Auth supports passkeys via the WebAuthn / FIDO2 specification. The private key never leaves the user's device, and authentication requires physical presence -- making remote attacks effectively impossible.
Enabling Passkeys
From the Dashboard
- Go to Authentication > Methods in your project.
- Toggle on Passkeys.
- Configure the Passkey Settings panel that appears (see below).
Because passkeys are bound to a specific domain and origin, you need to provide three values that cannot be auto-detected:
| Setting | Example (production) | Example (development) |
|---|---|---|
| RP ID | myapp.com | localhost |
| RP Name | My App | My App |
| Origin | https://myapp.com | http://localhost:3000 |
Fill these in and save.
From the SDK
If you prefer to configure passkeys programmatically, use saveDashboardConfig:
await banata.configuration.saveDashboardConfig({
authMethods: {
passkey: true,
},
passkey: {
rpId: "myapp.com",
rpName: "My App",
origin: "https://myapp.com",
},
});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 (e.g., "My App"). |
origin | string | Yes | The full origin of your app (e.g., "https://myapp.com"). Must match the page origin exactly, including protocol and port. |
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-Specific Values
In most setups, your RP ID and origin differ between development and production. A common pattern is to pass environment-specific values when calling saveDashboardConfig, or simply configure them separately in the dashboard for each environment.
Browser and Platform Support
Passkeys are supported on all modern browsers and platforms:
| Platform | Version | Authenticator Types |
|---|---|---|
| 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) | Security keys, platform authenticators |
| Edge | 18+ | Windows Hello, security keys |
Cross-Device Authentication
Modern passkey implementations support cross-device authentication -- 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 on your end.
Client-Side API
Banata Auth provides four passkey methods on the auth client.
Register a Passkey
Users must be signed in to register a passkey. This is typically called from a security settings page:
import { authClient } from "@/lib/auth-client";
const { data, error } = await authClient.passkey.addPasskey();
if (error) {
// Possible 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) {
// Possible 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 authenticated 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.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");
}Registration Component Example
Here is a complete React component that lets users manage their passkeys from a settings page:
"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 with Passkey Example
You can add a passkey sign-in button alongside your existing sign-in form:
"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>
{/* Your email/password form here */}
</div>
);
}A recommended UI pattern is to show the passkey option prominently as the primary action, with email/password as a secondary fallback.
Resident vs. Non-Resident Credentials
WebAuthn defines two types of credentials:
| Type | Also Called | How It Works |
|---|---|---|
| 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. |
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 other authentication methods. A common pattern is to let users sign up with email and password, then register a passkey from their account settings for faster future sign-ins.
You can enable multiple methods in the dashboard by toggling them on under Authentication > Methods, or programmatically:
await banata.configuration.saveDashboardConfig({
authMethods: {
emailPassword: true, // Traditional fallback
passkey: true, // Passwordless primary
magicLink: true, // Another passwordless option
},
passkey: {
rpId: "myapp.com",
rpName: "My App",
origin: "https://myapp.com",
},
});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 |
Troubleshooting
"WebAuthn not supported"
The user's browser does not support the WebAuthn API. This is rare on modern browsers but can occur in older versions or embedded browser views (e.g., in-app browsers). You can check for support before showing the passkey option:
if (window.PublicKeyCredential) {
// Safe to show passkey options
}"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 configuration does not match the actual page origin. Make sure it matches exactly, including protocol and port. For example, http://localhost:3000 is not the same as http://localhost:5173.
"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 deleted the passkey from their device or password manager.
Users locked out after losing a device
If a user's only passkey was on a lost device, they need a fallback sign-in method. Always enable at least one alternative method (email/password, magic link, or social OAuth) so users can recover access and register a new passkey.
Next Steps
- Email OTP -- Passwordless authentication via email codes
- Multi-Factor Auth -- Add TOTP as a second factor alongside passkeys
- Email & Password -- Traditional authentication as a fallback
- Social OAuth -- Sign in with Google, GitHub, and more