โ† Back to DevBytes

Svelte Authentication: JWT, Sessions, and OAuth Integration

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:

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 cookie


The jose library handles JWT signing and verification, cookie simplifies cookie parsing, and @auth/core provides 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-secret


JWT-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 in src/lib/server/jwt.ts so 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 bcrypt and 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.ts to verify the JWT and attach the user to locals.

// 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 httpOnly cookies rather than localStorage to 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, and secure flags, 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.ts to read the session cookie and populate locals.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.ts to 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 state parameter 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=lax on 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.

๐Ÿ›  Tools from DevBytes

Inventory Tracker Pro โ€” Excel inventory system, low-stock alerts ยท $19
AI Dev Kit for Mac โ€” local AI dev environment templates ยท $9.99
KeyMapper for Mac โ€” custom keyboard shortcut toolkit ยท $7.99

โ† Back to all articles