SvelteKit
This guide walks through setting up authentication in a SvelteKit application using ideal-auth. By the end, you will have working login, registration, logout, route protection via hooks, and access to the current user in pages and layouts.
Installation
Section titled “Installation”-
Install ideal-auth
Terminal window bun add ideal-auth -
Set the session secret
Add a secret to your
.envfile. It must be at least 32 characters..env IDEAL_AUTH_SECRET="at-least-32-characters-long-secret-here"Generate a strong secret:
Terminal window bunx ideal-auth secret
Cookie bridge
Section titled “Cookie bridge”SvelteKit provides a cookies object on every RequestEvent. The bridge maps its API to the three functions ideal-auth expects.
import type { Cookies } from '@sveltejs/kit';import type { CookieBridge } from 'ideal-auth';
export function createCookieBridge(cookies: Cookies): CookieBridge { return { get(name: string) { return cookies.get(name); }, set(name, value, options) { cookies.set(name, value, { ...options, // SvelteKit requires an explicit path path: options.path ?? '/', }); }, delete(name) { cookies.delete(name, { path: '/' }); }, };}Auth setup
Section titled “Auth setup”Create an auth() factory that accepts a SvelteKit Cookies object and returns an AuthInstance.
import { createAuth, createHash } from 'ideal-auth';import { createCookieBridge } from './cookies';import { db } from '$lib/server/db';import { IDEAL_AUTH_SECRET } from '$env/static/private';
type User = { id: string; email: string; name: string; password: string;};
export const hash = createHash({ rounds: 12 });
export function auth(cookies: import('@sveltejs/kit').Cookies) { const authFactory = createAuth<User>({ secret: IDEAL_AUTH_SECRET, cookie: createCookieBridge(cookies), hash,
async resolveUser(id) { return db.user.findUnique({ where: { id } }); },
async resolveUserByCredentials(credentials) { return db.user.findUnique({ where: { email: credentials.email }, }); }, });
return authFactory();}Login (form action)
Section titled “Login (form action)”import { fail, redirect } from '@sveltejs/kit';import { auth } from '$lib/server/auth';import type { Actions, PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ cookies }) => { const session = auth(cookies);
if (await session.check()) { redirect(303, '/dashboard'); }};
export const actions: Actions = { default: async ({ request, cookies }) => { const data = await request.formData(); const email = data.get('email') as string; const password = data.get('password') as string; const remember = data.get('remember') === 'on';
if (!email || !password) { return fail(400, { error: 'Email and password are required.', email }); }
const session = auth(cookies); const success = await session.attempt( { email, password }, { remember }, );
if (!success) { return fail(400, { error: 'Invalid email or password.', email }); }
redirect(303, '/dashboard'); },};Login form
Section titled “Login form”<script lang="ts"> import { enhance } from '$app/forms'; import type { ActionData } from './$types';
let { form }: { form: ActionData } = $props();</script>
<h1>Sign in</h1>
{#if form?.error} <p class="error">{form.error}</p>{/if}
<form method="POST" use:enhance> <label for="email">Email</label> <input id="email" name="email" type="email" value={form?.email ?? ''} required />
<label for="password">Password</label> <input id="password" name="password" type="password" required />
<label> <input name="remember" type="checkbox" /> Remember me </label>
<button type="submit">Sign in</button></form>Registration (form action)
Section titled “Registration (form action)”import { fail, redirect } from '@sveltejs/kit';import { auth, hash } from '$lib/server/auth';import { db } from '$lib/server/db';import type { Actions } from './$types';
export const actions: Actions = { default: async ({ request, cookies }) => { const data = await request.formData(); const email = data.get('email') as string; const name = data.get('name') as string; const password = data.get('password') as string; const passwordConfirmation = data.get('password_confirmation') as string;
if (!email || !name || !password) { return fail(400, { error: 'All fields are required.', email, name }); }
if (password.length < 8) { return fail(400, { error: 'Password must be at least 8 characters.', email, name }); }
if (password !== passwordConfirmation) { return fail(400, { error: 'Passwords do not match.', email, name }); }
const existing = await db.user.findUnique({ where: { email } }); if (existing) { return fail(400, { error: 'An account with this email already exists.', email, name }); }
const user = await db.user.create({ data: { email, name, password: await hash.make(password), }, });
// Log the user in immediately after registration const session = auth(cookies); await session.login(user);
redirect(303, '/dashboard'); },};Logout (form action)
Section titled “Logout (form action)”import { redirect } from '@sveltejs/kit';import { auth } from '$lib/server/auth';import type { Actions } from './$types';
export const actions: Actions = { default: async ({ cookies }) => { const session = auth(cookies); await session.logout(); redirect(303, '/login'); },};Logout button
Section titled “Logout button”<script lang="ts"> import { enhance } from '$app/forms';</script>
<form method="POST" action="/logout" use:enhance> <button type="submit">Sign out</button></form>Auth guard (handle hook)
Section titled “Auth guard (handle hook)”Use a handle hook to protect routes and make the current user available throughout the application via event.locals.
First, declare the types:
declare global { namespace App { interface Locals { user: { id: string; email: string; name: string; } | null; } }}
export {};Then implement the hook:
import { redirect, type Handle } from '@sveltejs/kit';import { auth } from '$lib/server/auth';
const protectedRoutes = ['/dashboard', '/settings', '/profile'];const authRoutes = ['/login', '/register'];
export const handle: Handle = async ({ event, resolve }) => { const session = auth(event.cookies); const user = await session.user();
// Attach user to locals for access in load functions and actions event.locals.user = user ? { id: user.id, email: user.email, name: user.name } : null;
const { pathname } = event.url;
// Redirect unauthenticated users away from protected routes if (protectedRoutes.some((route) => pathname.startsWith(route))) { if (!user) { redirect(303, `/login?callbackUrl=${encodeURIComponent(pathname)}`); } }
// Redirect authenticated users away from auth routes if (authRoutes.some((route) => pathname.startsWith(route))) { if (user) { redirect(303, '/dashboard'); } }
return resolve(event);};Getting the current user
Section titled “Getting the current user”In a load function
Section titled “In a load function”import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals }) => { // user is already available from the handle hook return { user: locals.user, };};In a page component
Section titled “In a page component”<script lang="ts"> import LogoutButton from '$lib/components/LogoutButton.svelte'; import type { PageData } from './$types';
let { data }: { data: PageData } = $props();</script>
<h1>Welcome, {data.user?.name}</h1><p>Email: {data.user?.email}</p>
<LogoutButton />In a layout
Section titled “In a layout”import { redirect } from '@sveltejs/kit';import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals }) => { if (!locals.user) { redirect(303, '/login'); }
return { user: locals.user, };};CSRF protection
Section titled “CSRF protection”SvelteKit has built-in CSRF protection. It automatically checks the Origin header on all form submissions and rejects cross-origin requests with a 403 response. This is enabled by default and requires no additional configuration.
If you need to customize this behavior (for example, to allow specific origins), you can configure it in svelte.config.js:
const config = { kit: { csrf: { checkOrigin: true, // this is the default }, },};
export default config;Security notes
Section titled “Security notes”- Session secret: Use
$env/static/privateor$env/dynamic/privateto access the secret. Never import secrets into client-side code. - HTTPS: In production, set
NODE_ENV=productionso session cookies are automatically markedSecure. - Cookie scope: The default
SameSite=LaxandHttpOnly=truesettings protect against CSRF and XSS cookie theft. - Locals safety: Only attach non-sensitive, serializable data to
event.locals. Data in locals is not automatically exposed to the client, but load functions that return it will serialize it. - Password hashing:
createHashuses bcrypt with SHA-256 pre-hashing for passwords over 72 bytes. The default 12 rounds is suitable for production. - Form actions vs API routes: Prefer form actions for auth operations. They benefit from SvelteKit’s built-in CSRF protection and progressive enhancement via
use:enhance.