Passkey Authentication

Passkey Authentication

Nuxt Starter Kit includes built-in support for passkey authentication, providing a modern, secure, and user-friendly way to authenticate users without passwords.

What are Passkeys?

Passkeys are a modern authentication standard based on public-key cryptography. Instead of remembering and typing passwords, users can authenticate using their device's biometric sensors (Face ID, Touch ID, Windows Hello) or screen lock.

Benefits

More Secure

Resistant to phishing, credential stuffing, and password-related attacks. Private keys never leave the user's device.

Faster & Simpler

No passwords to remember or type. Authentication happens in seconds with a simple biometric scan or device unlock.

Cross-Device Support

Passkeys can be synced securely across devices through platform mechanisms like iCloud Keychain or Google Password Manager.

How It Works

Passkeys use WebAuthn (Web Authentication API), a W3C standard:

  1. Registration: When creating a passkey, a public-private key pair is generated on the user's device
  2. Storage: The private key stays securely on the device; the public key is stored on the server
  3. Authentication: When signing in, the server sends a challenge that only the private key can sign
  4. Verification: The server verifies the signed challenge using the stored public key

Using Passkeys

For Users

Registration

  1. Navigate to the Register or Login page
  2. Click the Passkey provider button (fingerprint icon)
  3. Enter your email address when prompted
  4. Follow your device's prompts (Face ID, Touch ID, Windows Hello, etc.)
  5. Your passkey is created and you're logged in!

Sign In

  1. Navigate to the Login page
  2. Click Sign In with Passkey
  3. If you have multiple passkeys, select the one you want to use
  4. Authenticate with your device's biometric or screen lock
  5. You're signed in!

Browser Support

Passkeys are supported in all modern browsers:

  • Chrome/Edge: Version 67+
  • Safari: Version 13+
  • Firefox: Version 60+
  • All major mobile browsers (iOS Safari, Chrome Mobile, Samsung Internet)

For Developers

Architecture

The passkey implementation uses Better Auth with the @better-auth/passkey plugin and consists of:

  1. Server Configuration (packages/layer-auth/server/utils/auth.ts)
    • Better Auth instance with passkey() plugin enabled
    • Automatic WebAuthn ceremony handling
    • Database integration via Drizzle adapter
  2. Client Integration (packages/layer-auth/app/composables/useAuth.ts)
    • Better Auth client with passkeyClient() plugin
    • Auto-imported composable providing passkey methods
  3. UI Components
    • Login/Signup pages with passkey provider buttons
    • PasskeyRegisterModal.vue - Email collection for registration
    • SettingsPasskeys.vue - Passkey management in dashboard
  4. Database Schema (packages/layer-auth/server/db/schema/auth.ts)
    • passkey table managed by Better Auth

Database Schema

Passkeys are stored in the passkey table:

{
  id: string // Primary key
  userId: string // Foreign key to user table
  name: string | null // Optional passkey name (e.g., "My iPhone")
  publicKey: string // Public key for verification
  counter: number // Signature counter (replay protection)
  backedUp: boolean // Whether credential is backed up
  deviceType: string // Device type (platform/cross-platform)
  transports: string // Supported transports (USB, NFC, BLE, etc.)
  webauthnUserID: string // WebAuthn user ID
  createdAt: Date // Creation timestamp
}

Client API

The useAuth() composable exposes the Better Auth client with passkey methods:

Sign In with Passkey

<script setup lang="ts">
const { client } = useAuth()
const { showErrorToast, showSuccessToast } = useAppToast()

async function signInWithPasskey () {
  const { error } = await client.passkey.signIn()

  if (error) {
    showErrorToast('Sign in failed', error)
  }
  else {
    showSuccessToast({ title: 'Signed in successfully!' })
    await navigateTo('/dashboard')
  }
}
</script>

<template>
  <UButton
    icon="i-lucide-fingerprint"
    @click="signInWithPasskey"
  >
    Sign in with Passkey
  </UButton>
</template>

Register a Passkey

<script setup lang="ts">
const { client } = useAuth()

async function registerPasskey () {
  const { error } = await client.passkey.signUp({
    email: '[email protected]',
    name: 'My iPhone', // Optional passkey name
  })

  if (!error) {
    // User is now registered and signed in
    await navigateTo('/dashboard')
  }
}
</script>

List User's Passkeys

<script setup lang="ts">
const { client } = useAuth()
const passkeys = ref([])

async function loadPasskeys () {
  const { data, error } = await client.passkey.listPasskeys()
  if (!error) {
    passkeys.value = data
  }
}

onMounted(loadPasskeys)
</script>

Add Additional Passkey (for authenticated users)

<script setup lang="ts">
const { client } = useAuth()

async function addPasskey () {
  const { error } = await client.passkey.addPasskey({
    name: 'Work Laptop', // Optional name
  })

  if (!error) {
    // Passkey added successfully
  }
}
</script>

Delete a Passkey

<script setup lang="ts">
const { client } = useAuth()

async function deletePasskey (passkeyId: string) {
  const { error } = await client.passkey.deletePasskey({
    id: passkeyId,
  })

  if (!error) {
    // Passkey deleted successfully
  }
}
</script>

Configuration

Passkey support is configured in packages/layer-auth/server/utils/auth.ts:

import { passkey } from '@better-auth/passkey'
import { betterAuth } from 'better-auth'

export function createBetterAuth () {
  return betterAuth({
    // ...other config
    plugins: [
      passkey(),
      // ...other plugins
    ],
  })
}

And in the client (packages/layer-auth/app/composables/useAuth.ts):

import { passkeyClient } from '@better-auth/passkey/client'
import { createAuthClient } from 'better-auth/vue'

const client = createAuthClient({
  plugins: [
    passkeyClient(),
    // ...other plugins
  ],
})

Security Features

  1. Replay Attack Prevention: Signature counter verified and updated on each authentication
  2. Challenge-Response: Single-use cryptographic challenges
  3. Email Verification: Users created via passkey are automatically verified
  4. Secure Session Management: Built-in session handling via Better Auth
  5. Device-Bound Credentials: Private keys never leave the user's device

Managing Passkeys in Dashboard

Users can manage their passkeys in Dashboard → Settings → Security:

  • View all registered passkeys with names and creation dates
  • Add new passkeys with custom names (e.g., "iPhone 15", "Work Laptop")
  • Delete passkeys they no longer use
  • See device type and backup status for each passkey

The SettingsPasskeys component provides this functionality and can be found at packages/layer-dashboard/app/components/settings/Passkeys.vue.

Technical Details

Dependencies

  • better-auth - Modern authentication library for TypeScript
  • @better-auth/passkey - Passkey plugin for Better Auth
  • Uses standard WebAuthn API under the hood

Database Migrations

The passkey table is managed by Better Auth and defined in the auth schema:

# Generate schema (already done)
pnpm db:generate

# Apply migrations
pnpm db:migrate

# View database
pnpm db:studio

Auto-Imports

The useAuth() composable is auto-imported in all components and pages. No explicit imports needed:

<script setup lang="ts">
// useAuth() is automatically available
const { client, user } = useAuth()
</script>

Best Practices

  1. Offer Multiple Auth Methods: Keep password and OAuth options alongside passkeys
  2. Clear Communication: Explain what passkeys are to users who may be unfamiliar
  3. Graceful Degradation: Handle cases where passkeys aren't supported
  4. Multiple Passkeys: Allow users to register multiple passkeys per account (supported out-of-box)
  5. Recovery Options: Provide alternative authentication methods for account recovery
  6. Named Passkeys: Encourage users to name their passkeys for easier management

Troubleshooting

"Passkey not found" Error

  • Ensure the user has registered a passkey for this site
  • Check browser compatibility
  • Verify the domain matches the registration domain

Registration Fails

  • Confirm Better Auth passkey plugin is enabled
  • Check that the database migrations have been applied (pnpm db:migrate)
  • Verify the user's device supports WebAuthn
  • Check browser console for WebAuthn errors

Cross-Device Issues

  • Not all platforms support cross-device passkey sync yet
  • Users may need to register separate passkeys on each device
  • Named passkeys help users identify which device each passkey belongs to

Learn More