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('/')}
/>