This episode covers authentication and authorization in SvelteKit: common auth patterns, session handling with cookies and server-side auth, protected routes with authorization guards, plus integrating external auth providers like OAuth and OIDC.

Almost every modern web application needs to distinguish who is accessing it: admin pages, personal profiles, down to user data. Episode 12 covers authentication and authorization in SvelteKit — from common auth patterns, session handling with cookies, protected routes, to integrating external providers like OAuth and OIDC.
Authentication answers the question "who are you", while authorization answers "what are you allowed to do". The two must be clearly separated in the application's architecture. In SvelteKit, both layers live on the server: server actions, hooks, and load functions.
After this episode, you can build secure login flows, protect routes by role, and connect accounts from third-party providers without compromising security.
The most common pattern in SvelteKit is the session cookie: the server creates a token, stores it in an HTTP-only cookie, then reads that token on every request through hooks. The token is never handed to JavaScript in the browser, so injected scripts cannot read it.
The alternative is a bearer token like JWT sent via the Authorization header. This pattern suits APIs consumed by other applications, but it requires you to manage token validity and expiry yourself, including more involved revocation.
import { fail } from "@sveltejs/kit";
import { buatSession } from "$lib/server/session";
export const actions = {
login: async ({ request, cookies }) => {
const form = await request.formData();
const email = String(form.get("email") ?? "");
const password = String(form.get("password") ?? "");
const user = await verifikasiKredensial(email, password);
if (!user) {
return fail(401, { error: "Email atau password salah" });
}
cookies.set("session", buatSession(user.id), {
httpOnly: true,
sameSite: "lax",
secure: true,
path: "/",
maxAge: 60 * 60 * 24 * 7
});
return { sukses: true };
}
};The cookie is set with httpOnly: true so JavaScript cannot access it, sameSite: "lax" to reduce CSRF risk, and secure: true when running over HTTPS. The default expiry is kept short; add a refresh mechanism if sessions need to last longer.
For browser-facing web apps, the session cookie is the first choice. Use tokens only for specific needs, such as a public API or service-to-service authentication. Make sure this decision is written down in the documentation so the whole team stays consistent.
All verification is centralized in one place: the handle hook in src/hooks.server.js. The hook runs for every request before the route is processed, so verification results can be stored on event.locals and used anywhere.
export const handle = async ({ event, resolve }) => {
const token = event.cookies.get("session");
if (token) {
const user = await verifikasiSession(token);
if (user) {
event.locals.user = user;
}
}
return await resolve(event);
};Sessions can be stored in the database (persistent and easy to revoke) or in a signed token. For small apps, a signed token is enough; for larger apps, store sessions in the database and use random IDs so logout and account blocking take effect instantly.
Declare the shape of locals in src/app.d.ts so TypeScript knows which fields are available. Without this declaration, accessing event.locals.user gets no autocomplete and can trigger confusing type errors.
import type { User } from "$lib/types";
declare global {
namespace App {
interface Locals {
user?: User;
}
}
}
export {};Routes that only authenticated users may access check locals in the load function and throw a redirect when the check fails. By putting the guard in +layout.server.js, every route below it is protected automatically.
import { redirect } from "@sveltejs/kit";
export const load = async ({ locals }) => {
if (!locals.user) {
throw redirect(303, "/login");
}
return { user: locals.user };
};Once a user is identified, role-based access control ensures admin pages cannot be opened by just anyone. Compare the user's role against the list of allowed roles, and centralize this policy in one helper so it is easy to change.
import { redirect } from "@sveltejs/kit";
import { peranDiizinkan } from "$lib/server/otorisasi";
export const load = async ({ locals }) => {
const user = locals.user;
if (!user) {
throw redirect(303, "/login");
}
if (!peranDiizinkan(user.peran, ["admin", "editor"])) {
throw redirect(303, "/");
}
return { user };
};Never rely on the UI alone for authorization: a hidden button is no substitute for a server-side guard. Every load function and server action that handles sensitive data must re-check access rights.
Instead of managing passwords themselves, many applications use external providers via OAuth or OIDC: Google, GitHub, or enterprise providers. The flow is standard: the app redirects the user to the provider, the provider returns a code, and the app exchanges that code for a token.
import { redirect } from "@sveltejs/kit";
export const actions = {
loginProvider: async ({ cookies }) => {
const state = crypto.randomUUID();
cookies.set("oauth_state", state, { httpOnly: true, path: "/" });
const params = new URLSearchParams({
client_id: process.env.PROVIDER_CLIENT_ID,
redirect_uri: process.env.PROVIDER_REDIRECT_URI,
response_type: "code",
state,
scope: "openid profile email"
});
throw redirect(303, `https://provider.example/authorize?${params}`);
}
};The state parameter prevents CSRF attacks on the callback; it must be verified before the code is exchanged. Libraries like Auth.js manage this flow along with PKCE and refresh tokens, so you do not have to rewrite error-prone logic. Install it with npm install @auth/sveltekit and follow the adapter docs to configure the provider.
Access and refresh tokens are stored on the server, not in a client cookie. Never expose tokens to JavaScript in the browser. Use secret management through server-side environment variables, rotate tokens regularly, and audit the list of connected apps so no access is left forgotten.
Key takeaways:
handle hook and stored in event.locals.state parameter and PKCE.In the next episode we get into secure data fetching: securing API requests with auth headers, handling secrets and server-only config, CSRF and XSS protection, plus secure deployment configuration.