Skip to main content
Urbicon UI
Back to Recipes

Passkey Login

Passwordless and password sign-in in one form, backed by the WebAuthn passkey handlers.

Live Preview

Sign in

Forgot password? Don't have an account? Create account

Key Features

  • Email/password and passkey sign-in in one form (mode="both")
  • Discoverable (usernameless) passkey login — no email field needed
  • Server WebAuthn ceremony via createPasskeyAuthenticationOptions/Verify handlers
  • Per-ceremony challenge pinned to a single-use HttpOnly cookie
  • CSRF-protected requests through the bundled csrfFetch
  • PasskeyManager lets signed-in users add and remove credentials

Code

Passkey Login — full flow

// 1. src/lib/server/auth-setup.ts — auth deps + shared WebAuthn ceremony config
import { createAuthDeps, createPasskeyHandlers } from '@urbicon-ui/auth/server';
import type { WebAuthnConfig } from '@urbicon-ui/auth/server';
import { createPrismaRepos } from '@urbicon-ui/auth/server/adapters/prisma';
import { createLettermintTransport } from '@urbicon-ui/auth/server/email/lettermint';
import { env } from '$env/dynamic/private';
import { prisma } from './prisma';

export const authDeps = createAuthDeps({
  config: { jwt: { secret: env.JWT_SECRET }, appUrl: env.PUBLIC_APP_URL },
  repos: createPrismaRepos(prisma),
  email: createLettermintTransport({ token: env.LETTERMINT_TOKEN })
});

const webauthn: WebAuthnConfig = {
  rpId: 'example.com',        // your registrable domain (no scheme/port)
  rpName: 'My App',
  origin: env.PUBLIC_APP_URL  // e.g. https://app.example.com
  // challengeStore defaults to in-memory; pass a ChallengeStore for >1 instance
};

// One factory returns all six passkey handlers (both ceremonies + list/delete).
export const passkey = createPasskeyHandlers(authDeps, webauthn);

// 2. src/routes/api/auth/passkey/authentication-options/+server.ts
import { passkey } from '$lib/server/auth-setup';
export const POST = passkey.authenticationOptions.POST;

// 3. src/routes/api/auth/passkey/authentication-verify/+server.ts
import { passkey } from '$lib/server/auth-setup';
export const POST = passkey.authenticationVerify.POST;
// passkey.registrationOptions / passkey.registrationVerify (and passkey.list /
// passkey.item for the PasskeyManager) wire up identically on sibling routes.

// 4. src/routes/auth/login/+page.svelte
<script lang="ts">
  import { LoginPage } from '@urbicon-ui/auth';
  import { en } from '@urbicon-ui/auth/i18n/en';
  import { goto } from '$app/navigation';
</script>

<LoginPage
  t={en}
  mode="both"
  passkeyApiPath="/api/auth/passkey"
  onSuccess={() => goto('/')}
/>