Sveltekit Auth
Authentication patterns in SvelteKit using hooks, cookies, sessions, and OAuth flows
You are an expert in implementing authentication in SvelteKit applications, covering session management, hooks-based auth guards, OAuth integration, and secure patterns for protecting routes and API endpoints. ## Key Points 1. User submits credentials via a form action or OAuth redirect. 2. Server validates credentials and creates a session (database record or signed token). 3. Session identifier is stored in an HTTP-only cookie. 4. The `handle` hook reads the cookie on every request, validates the session, and populates `event.locals`. 5. Load functions and actions access `event.locals` to check authentication. - **Always use `httpOnly`, `secure`, and `sameSite` on session cookies.** This prevents XSS from stealing tokens and mitigates CSRF. - **Validate sessions on every request in hooks**, not just in individual load functions. Centralized validation prevents gaps. - **Hash passwords with bcrypt, scrypt, or argon2.** Never store plain-text passwords. - **Implement session expiry and rotation.** Refresh session expiry on activity and invalidate old sessions. - **Verify OAuth `state` parameter** to prevent CSRF attacks on the OAuth flow. - **Never expose sensitive user data to the client.** Return only the fields the UI needs from load functions (no password hashes, internal IDs if unnecessary). - **Use HTTPS in production.** The `secure` cookie flag requires it. ## Quick Example ```svelte <form method="POST" action="/logout"> <button>Log out</button> </form> ```
skilldb get svelte-skills/Sveltekit AuthFull skill: 379 linesAuthentication — SvelteKit
You are an expert in implementing authentication in SvelteKit applications, covering session management, hooks-based auth guards, OAuth integration, and secure patterns for protecting routes and API endpoints.
Core Philosophy
Authentication in SvelteKit is built on web platform fundamentals: HTTP-only cookies carry session identifiers, the handle hook validates sessions on every request, and event.locals passes authentication state to load functions and actions. There is no framework-specific auth module because authentication requirements vary wildly between applications. Instead, SvelteKit provides the primitives — cookies, hooks, locals — and you compose them into the auth flow your application needs.
The handle hook is the security perimeter. Every server request passes through it before reaching any load function, action, or API endpoint. This makes it the single place to validate sessions, refresh expiring tokens, and populate the locals object with user data. If authentication checking is scattered across individual load functions and actions, it is inevitable that some endpoints will be left unprotected. Centralizing session validation in hooks and then checking locals.user in individual handlers is the pattern that prevents gaps.
Cookie security is not optional. Every session cookie must be httpOnly (preventing XSS from reading it), secure (requiring HTTPS), and sameSite: 'lax' (mitigating CSRF). SvelteKit's built-in CSRF protection via Origin header checking covers form submissions, but the cookie flags are your responsibility. Skipping any of these flags creates a real security vulnerability, not a theoretical one.
Anti-Patterns
-
Checking Auth Only in Load Functions — protecting pages but not form actions or API endpoints, allowing attackers to call
POST /api/userdirectly. Every mutation endpoint must independently verify authentication. -
Storing Tokens in localStorage — using
localStoragefor session tokens instead of HTTP-only cookies. Any JavaScript on the page (including XSS payloads) can readlocalStorage, making token theft trivial. -
Forgetting
path: '/'on Session Cookies — setting a session cookie without specifyingpath: '/', which scopes it to the current URL path. The cookie then is not sent on requests to other routes, breaking authentication silently. -
Deleting Only the Cookie on Logout — removing the client-side cookie without invalidating the session record in the database. If the session ID is known, it can still be used until it expires naturally.
-
Redirecting with 302 After POST — using
redirect(302, ...)after a form submission instead ofredirect(303, ...). A 302 redirect preserves the HTTP method, which can cause the browser to re-POST to the redirect target.
Overview
SvelteKit provides a flexible foundation for authentication through its hooks system, cookies API, and locals object. There is no built-in auth module — instead, you compose these primitives (often with libraries like Lucia, Auth.js/NextAuth, or custom implementations) to build secure authentication flows tailored to your application.
Core Concepts
The Authentication Flow
- User submits credentials via a form action or OAuth redirect.
- Server validates credentials and creates a session (database record or signed token).
- Session identifier is stored in an HTTP-only cookie.
- The
handlehook reads the cookie on every request, validates the session, and populatesevent.locals. - Load functions and actions access
event.localsto check authentication.
Server Hooks (hooks.server.js)
The handle function runs on every server request. It is the central place for session validation.
// src/hooks.server.js
import { db } from '$lib/server/database';
export async function handle({ event, resolve }) {
const sessionId = event.cookies.get('session');
if (sessionId) {
const session = await db.getSession(sessionId);
if (session && session.expiresAt > new Date()) {
event.locals.user = session.user;
} else {
// Expired or invalid session — clear the cookie
event.cookies.delete('session', { path: '/' });
}
}
return resolve(event);
}
Cookie-Based Sessions
Set secure cookies when the user logs in:
// src/routes/login/+page.server.js
import { fail, redirect } from '@sveltejs/kit';
import { db } from '$lib/server/database';
import { verifyPassword } from '$lib/server/auth';
import crypto from 'crypto';
export const actions = {
default: async ({ request, cookies }) => {
const form = await request.formData();
const email = form.get('email');
const password = form.get('password');
const user = await db.getUserByEmail(email);
if (!user || !await verifyPassword(password, user.passwordHash)) {
return fail(401, { email, error: 'Invalid email or password' });
}
const sessionId = crypto.randomUUID();
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days
await db.createSession({ id: sessionId, userId: user.id, expiresAt });
cookies.set('session', sessionId, {
path: '/',
httpOnly: true,
sameSite: 'lax',
secure: true,
maxAge: 30 * 24 * 60 * 60 // 30 days in seconds
});
redirect(303, '/dashboard');
}
};
Accessing Auth State in Load Functions
// src/routes/dashboard/+layout.server.js
import { redirect } from '@sveltejs/kit';
export function load({ locals }) {
if (!locals.user) {
redirect(303, '/login');
}
return {
user: {
id: locals.user.id,
name: locals.user.name,
email: locals.user.email
}
};
}
Protecting API Endpoints
// src/routes/api/user/+server.js
import { json, error } from '@sveltejs/kit';
export function GET({ locals }) {
if (!locals.user) {
error(401, 'Unauthorized');
}
return json(locals.user);
}
Implementation Patterns
Auth Guard with Hook Sequence
Protect entire route groups in the hook:
// src/hooks.server.js
const protectedPaths = ['/dashboard', '/settings', '/api/user'];
export async function handle({ event, resolve }) {
// ... session validation (as above) ...
const isProtected = protectedPaths.some(p =>
event.url.pathname.startsWith(p)
);
if (isProtected && !event.locals.user) {
if (event.url.pathname.startsWith('/api/')) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
return new Response(null, {
status: 303,
headers: { Location: `/login?redirect=${event.url.pathname}` }
});
}
return resolve(event);
}
OAuth Flow (e.g., GitHub)
Step 1: Redirect to provider
// src/routes/login/github/+server.js
import { redirect } from '@sveltejs/kit';
import { GITHUB_CLIENT_ID } from '$env/static/private';
export function GET({ cookies }) {
const state = crypto.randomUUID();
cookies.set('oauth_state', state, {
path: '/',
httpOnly: true,
maxAge: 600
});
const params = new URLSearchParams({
client_id: GITHUB_CLIENT_ID,
redirect_uri: 'https://myapp.com/login/github/callback',
scope: 'read:user user:email',
state
});
redirect(302, `https://github.com/login/oauth/authorize?${params}`);
}
Step 2: Handle callback
// src/routes/login/github/callback/+server.js
import { redirect, error } from '@sveltejs/kit';
import { GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET } from '$env/static/private';
export async function GET({ url, cookies }) {
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
const storedState = cookies.get('oauth_state');
if (!code || state !== storedState) {
error(400, 'Invalid OAuth callback');
}
cookies.delete('oauth_state', { path: '/' });
// Exchange code for token
const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: GITHUB_CLIENT_ID,
client_secret: GITHUB_CLIENT_SECRET,
code
})
});
const { access_token } = await tokenRes.json();
// Fetch user profile
const userRes = await fetch('https://api.github.com/user', {
headers: { Authorization: `Bearer ${access_token}` }
});
const githubUser = await userRes.json();
// Create or update user in database
const user = await db.upsertUser({
githubId: githubUser.id,
name: githubUser.name,
email: githubUser.email,
avatar: githubUser.avatar_url
});
// Create session
const sessionId = crypto.randomUUID();
await db.createSession({ id: sessionId, userId: user.id });
cookies.set('session', sessionId, {
path: '/',
httpOnly: true,
sameSite: 'lax',
secure: true,
maxAge: 30 * 24 * 60 * 60
});
redirect(303, '/dashboard');
}
Logout
// src/routes/logout/+page.server.js
import { redirect } from '@sveltejs/kit';
export const actions = {
default: async ({ cookies, locals }) => {
const sessionId = cookies.get('session');
if (sessionId) {
await db.deleteSession(sessionId);
}
cookies.delete('session', { path: '/' });
redirect(303, '/');
}
};
<form method="POST" action="/logout">
<button>Log out</button>
</form>
Role-Based Access
// src/lib/server/auth.js
export function requireRole(locals, role) {
if (!locals.user) {
error(401, 'Not authenticated');
}
if (locals.user.role !== role) {
error(403, 'Forbidden');
}
}
// src/routes/admin/+page.server.js
import { requireRole } from '$lib/server/auth';
export function load({ locals }) {
requireRole(locals, 'admin');
return { users: db.getAllUsers() };
}
Using Lucia Auth Library
Lucia is a popular auth library designed for SvelteKit:
// src/lib/server/lucia.js
import { Lucia } from 'lucia';
import { DrizzleSQLiteAdapter } from '@lucia-auth/adapter-drizzle';
import { db } from './database';
import { sessions, users } from './schema';
const adapter = new DrizzleSQLiteAdapter(db, sessions, users);
export const lucia = new Lucia(adapter, {
sessionCookie: {
attributes: { secure: process.env.NODE_ENV === 'production' }
},
getUserAttributes: (attributes) => ({
email: attributes.email,
name: attributes.name
})
});
// src/hooks.server.js
import { lucia } from '$lib/server/lucia';
export async function handle({ event, resolve }) {
const sessionId = event.cookies.get(lucia.sessionCookieName);
if (sessionId) {
const { session, user } = await lucia.validateSession(sessionId);
if (session?.fresh) {
const cookie = lucia.createSessionCookie(session.id);
event.cookies.set(cookie.name, cookie.value, { path: '/', ...cookie.attributes });
}
if (!session) {
const cookie = lucia.createBlankSessionCookie();
event.cookies.set(cookie.name, cookie.value, { path: '/', ...cookie.attributes });
}
event.locals.user = user;
event.locals.session = session;
}
return resolve(event);
}
Best Practices
- Always use
httpOnly,secure, andsameSiteon session cookies. This prevents XSS from stealing tokens and mitigates CSRF. - Validate sessions on every request in hooks, not just in individual load functions. Centralized validation prevents gaps.
- Hash passwords with bcrypt, scrypt, or argon2. Never store plain-text passwords.
- Implement session expiry and rotation. Refresh session expiry on activity and invalidate old sessions.
- Verify OAuth
stateparameter to prevent CSRF attacks on the OAuth flow. - Never expose sensitive user data to the client. Return only the fields the UI needs from load functions (no password hashes, internal IDs if unnecessary).
- Use HTTPS in production. The
securecookie flag requires it.
Common Pitfalls
- Checking auth only in load functions. An attacker can call form actions or API endpoints directly. Protect actions and
+server.jshandlers too. - Storing JWTs in
localStorage. Use HTTP-only cookies instead.localStorageis accessible to any JavaScript on the page (XSS vulnerability). - Forgetting
path: '/'on cookies. Without it, the cookie is scoped to the current path and may not be sent on other routes. - Redirecting with 302 after POST. Use
redirect(303, ...)(See Other) after form submissions to ensure the browser makes a GET request to the redirect target. - Not invalidating server sessions on logout. Deleting only the cookie is insufficient if the session record remains valid in the database. Always delete both.
- Shared
localsacross requests.event.localsis per-request, which is correct. But module-level variables in hooks are shared — do not store per-request auth state in module scope.
Install this skill directly: skilldb add svelte-skills
Related Skills
Component Patterns
Svelte component composition patterns including props, snippets, context, and advanced reuse techniques
Form Actions
SvelteKit form actions for progressive enhancement with server-side form handling
Load Functions
SvelteKit server and universal load functions for fetching and passing data to pages and layouts
Reactivity
Svelte 5 runes system for fine-grained reactivity with $state, $derived, and $effect
Stores
Svelte stores and state management patterns including writable, readable, derived, and custom stores
Sveltekit Routing
SvelteKit file-based routing system including layouts, groups, and advanced route patterns