Svelte Authentication: JWT, Sessions, and OAuth Integration
Authentication is one of the most critical features in any modern web application. In Svelte and SvelteKit, you have multiple strategies available depending on your security requirements, architecture, and user experience goals. This tutorial walks through the three most common approaches: JSON Web Tokens (JWT), server-side sessions, and OAuth integration with third-party providers. By the end, you will understand when to use each approach and how to implement them safely in a SvelteKit application.
Why Authentication Matters in Svelte
SvelteKit is a full-stack framework that runs code both on the client and the server. This dual execution environment means authentication must be handled carefully โ sensitive credentials, tokens, and validation logic should never leak to the browser bundle. A well-designed authentication system protects user data, prevents session hijacking, and provides a smooth sign-in experience across page reloads and navigation.
The three approaches covered here solve different problems:
- JWT โ Stateless tokens ideal for APIs and microservices.
- Sessions โ Server-side storage with cookies, best for traditional web apps.
- OAuth โ Delegated authentication through providers like Google, GitHub, or Auth0.
Setting Up the Project
Before diving into each strategy, create a new SvelteKit project and install the dependencies you will need throughout this tutorial.
npm create svelte@latest svelte-auth-app cd svelte-auth-app npm install npm install @auth/core jose cookieThe
joselibrary handles JWT signing and verification,cookiesimplifies cookie parsing, and@auth/coreprovides OAuth primitives. You will also need environment variables for secrets and OAuth client credentials.# .env AUTH_SECRET=your-super-secret-key-at-least-32-chars GOOGLE_CLIENT_ID=your-google-client-id GOOGLE_CLIENT_SECRET=your-google-client-secretJWT-Based Authentication
What Is JWT?
JSON Web Tokens are compact, URL-safe strings that encode claims about a user. They are signed by the server using a secret key, which means the server can verify their integrity without storing them. This makes JWT stateless and scalable, but it also introduces challenges around token revocation and expiration.
Creating a JWT Utility
Start by building a small utility module that signs and verifies tokens using
jose. Place this file insrc/lib/server/jwt.tsso it never ships to the client.// src/lib/server/jwt.ts import { SignJWT, jwtVerify } from 'jose'; const secret = new TextEncoder().encode(process.env.AUTH_SECRET); export async function signJwt(payload: Record<string, unknown>): Promise<string> { return new SignJWT(payload) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('1h') .sign(secret); } export async function verifyJwt(token: string): Promise<Record<string, unknown> | null> { try { const { payload } = await jwtVerify(token, secret); return payload; } catch { return null; } }Issuing Tokens on Login
Create a login endpoint that validates credentials and returns a JWT. In a real application, you would hash passwords with
bcryptand look up users in a database.// src/routes/api/login/+server.ts import { json, error } from '@sveltejs/kit'; import { signJwt } from '$lib/server/jwt'; const USERS = [ { id: 1, email: 'demo@example.com', password: 'password123' } ]; export async function POST({ request }) { const { email, password } = await request.json(); const user = USERS.find(u => u.email === email && u.password === password); if (!user) { throw error(401, 'Invalid credentials'); } const token = await signJwt({ sub: String(user.id), email: user.email }); return json({ token }); }Protecting Routes with Hooks
SvelteKit hooks run on every request. Use
hooks.server.tsto verify the JWT and attach the user tolocals.// src/hooks.server.ts import type { Handle } from '@sveltejs/kit'; import { verifyJwt } from '$lib/server/jwt'; export const handle: Handle = async ({ event, resolve }) => { const authHeader = event.request.headers.get('authorization'); const token = authHeader?.replace('Bearer ', ''); if (token) { const payload = await verifyJwt(token); if (payload) { event.locals.user = { id: payload.sub as string, email: payload.email as string }; } } return resolve(event); };Best Practices for JWT
- Keep token lifetimes short (15โ60 minutes) and use refresh tokens for longer sessions.
- Never store sensitive data in the JWT payload โ it is encoded, not encrypted.
- Store tokens in
httpOnlycookies rather thanlocalStorageto reduce XSS exposure. - Implement a revocation list or use short-lived tokens with rotating refresh tokens.
Session-Based Authentication
What Are Sessions?
Sessions store user state on the server, identified by a random session ID sent to the client in a cookie. Because the server maintains the session record, you can invalidate sessions instantly, track active devices, and avoid exposing user data in the token itself. Sessions are the recommended default for traditional server-rendered SvelteKit applications.
Creating a Session Store
For this example, use an in-memory Map. In production, replace this with Redis, a database table, or another persistent store.
// src/lib/server/sessionStore.ts import { randomUUID } from 'crypto'; const sessions = new Map<string, { userId: string; email: string; createdAt: number }>(); export function createSession(user: { id: string; email: string }): string { const sessionId = randomUUID(); sessions.set(sessionId, { userId: user.id, email: user.email, createdAt: Date.now() }); return sessionId; } export function getSession(sessionId: string) { return sessions.get(sessionId) ?? null; } export function deleteSession(sessionId: string): void { sessions.delete(sessionId); }Login and Logout Endpoints
These endpoints set and clear the session cookie. Note the
httpOnly,sameSite, andsecureflags, which protect against cross-site scripting and cross-site request forgery.// src/routes/api/session/+server.ts import { json, error } from '@sveltejs/kit'; import { createSession, deleteSession } from '$lib/server/sessionStore'; export async function POST({ request, cookies }) { const { email, password } = await request.json(); // Replace with real user lookup if (email !== 'demo@example.com' || password !== 'password123') { throw error(401, 'Invalid credentials'); } const sessionId = createSession({ id: '1', email }); cookies.set('session', sessionId, { path: '/', httpOnly: true, sameSite: 'strict', secure: process.env.NODE_ENV === 'production', maxAge: 60 * 60 * 24 * 7 }); return json({ success: true }); } export async function DELETE({ cookies }) { const sessionId = cookies.get('session'); if (sessionId) { deleteSession(sessionId); cookies.delete('session', { path: '/' }); } return json({ success: true }); }Loading the Session in Hooks
Update
hooks.server.tsto read the session cookie and populatelocals.user.// src/hooks.server.ts import type { Handle } from '@sveltejs/kit'; import { getSession } from '$lib/server/sessionStore'; export const handle: Handle = async ({ event, resolve }) => { const sessionId = event.cookies.get('session'); if (sessionId) { const session = getSession(sessionId); if (session) { event.locals.user = { id: session.userId, email: session.email }; } } return resolve(event); };Guarding Pages with Load Functions
Use
+layout.server.tsto expose the user to the client and redirect unauthenticated visitors.// src/routes/+layout.server.ts import { redirect } from '@sveltejs/kit'; import type { LayoutServerLoad } from './$types'; export const load: LayoutServerLoad = async ({ locals, url }) => { if (!locals.user && url.pathname.startsWith('/dashboard')) { throw redirect(303, '/login'); } return { user: locals.user }; };Best Practices for Sessions
- Use cryptographically random session IDs with at least 128 bits of entropy.
- Rotate session IDs after privilege changes, such as login or password reset.
- Set an absolute session timeout in addition to an idle timeout.
- Store sessions in a fast, shared data store like Redis when running multiple instances.
OAuth Integration
What Is OAuth?
OAuth allows users to authenticate using a third-party provider instead of creating a new account on your site. This improves conversion rates, reduces password management burden, and leverages the security infrastructure of major providers. The OAuth 2.0 authorization code flow is the most secure and widely used pattern for server-side applications.
Configuring a Google OAuth Provider
Register your application in the Google Cloud Console to obtain a client ID and secret. Set the authorized redirect URI to http://localhost:5173/auth/callback/google for local development.
Building the OAuth Flow Manually
You can implement the authorization code flow without any library. The first step is to redirect the user to Google's consent screen.
// src/routes/auth/login/google/+server.ts import { redirect } from '@sveltejs/kit'; const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'; export function GET({ url }) { const params = new URLSearchParams({ client_id: process.env.GOOGLE_CLIENT_ID!, redirect_uri: `${url.origin}/auth/callback/google`, response_type: 'code', scope: 'openid email profile', state: crypto.randomUUID() }); throw redirect(302, `${GOOGLE_AUTH_URL}?${params.toString()}`); }After the user consents, Google redirects back to your callback URL with an authorization code. Exchange that code for an access token and user profile.
// src/routes/auth/callback/google/+server.ts import { json, error, redirect } from '@sveltejs/kit'; import { createSession } from '$lib/server/sessionStore'; export async function GET({ url, cookies }) { const code = url.searchParams.get('code'); if (!code) throw error(400, 'Missing code'); const tokenResponse = await fetch('https://oauth2.googleapis.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ code, client_id: process.env.GOOGLE_CLIENT_ID!, client_secret: process.env.GOOGLE_CLIENT_SECRET!, redirect_uri: `${url.origin}/auth/callback/google`, grant_type: 'authorization_code' }) }); if (!tokenResponse.ok) throw error(400, 'Token exchange failed'); const tokens = await tokenResponse.json(); const profileResponse = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', { headers: { Authorization: `Bearer ${tokens.access_token}` } }); const profile = await profileResponse.json(); const sessionId = createSession({ id: profile.sub, email: profile.email }); cookies.set('session', sessionId, { path: '/', httpOnly: true, sameSite: 'lax', secure: process.env.NODE_ENV === 'production', maxAge: 60 * 60 * 24 * 7 }); throw redirect(303, '/dashboard'); }Using Auth.js for a Cleaner Approach
For applications with multiple providers,
@auth/sveltekit(the SvelteKit adapter for Auth.js) reduces boilerplate significantly. Install it and configure providers in a single file.npm install @auth/sveltekit// src/hooks.server.ts import { SvelteKitAuth } from '@auth/sveltekit'; import Google from '@auth/core/providers/google'; import GitHub from '@auth/core/providers/github'; import type { Handle } from '@sveltejs/kit'; export const handle: Handle = SvelteKitAuth({ providers: [ Google({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET! }), GitHub({ clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET! }) ], secret: process.env.AUTH_SECRET, callbacks: { async session({ session, token }) { if (token) { session.user.id = token.sub as string; } return session; } } });With Auth.js configured, you can access the session in any load function or page component.
// src/routes/+layout.server.ts export const load = async (event) => { const session = await event.locals.getSession(); return { session }; };<!-- src/routes/+layout.svelte --> <script> let { data, children } = $props(); </script> {#if data.session} <p>Signed in as {data.session.user.email}</p> <a href="/auth/signout">Sign out</a> {:else} <a href="/auth/signin/google">Sign in with Google</a> {/if} {@render children()}Best Practices for OAuth
- Always validate the
stateparameter to prevent CSRF attacks during the callback. - Store access tokens server-side only; never expose them to the browser.
- Link OAuth accounts to local user records so users can sign in with multiple methods.
- Handle token refresh gracefully for providers that issue expiring access tokens.
- Use
sameSite=laxon session cookies so OAuth redirects work correctly.
Choosing the Right Strategy
Each authentication method has trade-offs. JWT works well for stateless APIs and mobile backends, but revocation is harder. Sessions are simpler to invalidate and ideal for server-rendered apps, but require server-side storage. OAuth offloads credential management to trusted providers and improves user experience, but adds external dependencies and redirect complexity. Many production applications combine these approaches โ for example, using OAuth for initial login, then issuing a session cookie for subsequent requests.
Conclusion
Authentication in SvelteKit is flexible enough to support any of these strategies, and the framework's server hooks make it straightforward to enforce auth consistently across your application. Start with session-based authentication for server-rendered apps, reach for JWT when you need stateless API tokens, and add OAuth when you want to reduce friction for your users. Whichever approach you choose, always follow security best practices: use httpOnly cookies, validate inputs, keep secrets on the server, and test your flows against common attacks like CSRF and session fixation. With the patterns shown in this tutorial, you have a solid foundation for building secure, production-ready authentication in Svelte.