VOOZH about

URL: https://dev.to/tortoise62/how-to-add-authentication-to-a-sveltekit-spa-fpi

⇱ How to Add Authentication to a SvelteKit SPA - DEV Community


Originally published at turtledev.io

Building on our previous tutorial where we created a SvelteKit SPA with a FastAPI backend, let's add authentication to our application.

Building a production app? Check out FastSvelte — a production-ready FastAPI + SvelteKit starter with authentication, payments, and more built-in.

This tutorial demonstrates a minimal authentication implementation for learning purposes, covering:

  • HTTP-only cookie-based sessions
  • Reactive auth state management with Svelte 5 runes
  • Protected routes with automatic redirects
  • Optimized auth checks with caching

Note: This is a tutorial project for learning concepts. For production applications, use solutions like FastSvelte, Auth.js, Lucia, or your backend framework's authentication library.

Prerequisites

Authentication Flow

Our authentication system uses HTTP-only cookies for secure session management. Here's how the complete flow works:

┌─────────────────────────────────────────────────────────────────────┐
│ LOGIN FLOW │
└─────────────────────────────────────────────────────────────────────┘

Browser SvelteKit Frontend FastAPI Backend
 │ │ │
 │ 1. Enter credentials │ │
 │ ──────────────────────────> │ │
 │ │ │
 │ │ 2. POST /auth/login │
 │ │ {email, password} │
 │ │ ──────────────────────────> │
 │ │ │
 │ │ │ 3. Validate
 │ │ │ credentials
 │ │ │
 │ │ 4. Set-Cookie: session=xxx │
 │ │ (HTTP-only, SameSite) │
 │ │ <────────────────────────── │
 │ │ │
 │ 5. Cookie stored │ │
 │ <────────────────────────── │ │
 │ (inaccessible to JS) │ │
 │ │ │
 │ 6. Redirect to /welcome │ │
 │ <────────────────────────── │ │
 │ │ │

Step-by-step breakdown:

  1. User enters their email and password in the login form
  2. Frontend sends credentials to the backend's /auth/login endpoint
  3. Backend validates the credentials against the user database (in-memory for this tutorial)
  4. Backend creates a session token and sends it back as an HTTP-only cookie
  5. Browser automatically stores the cookie (JavaScript cannot access it due to httponly flag)
  6. Frontend redirects the user to the dashboard/welcome page
┌─────────────────────────────────────────────────────────────────────┐
│ AUTHENTICATED REQUEST │
└─────────────────────────────────────────────────────────────────────┘

Browser SvelteKit Frontend FastAPI Backend
 │ │ │
 │ 1. Navigate to /todos │ │
 │ ──────────────────────────> │ │
 │ │ │
 │ │ 2. GET /users/me │
 │ │ Cookie: session=xxx │
 │ │ ──────────────────────────> │
 │ │ │
 │ │ │ 3. Validate
 │ │ │ session
 │ │ │
 │ │ 4. {id, email, ...} │
 │ │ <────────────────────────── │
 │ │ │
 │ 5. Update auth store │ │
 │ <────────────────────────── │ │
 │ │ │
 │ 6. GET /todos │ │
 │ Cookie: session=xxx │ │
 │ ─────────────────────────────────────────────────────────> │
 │ │ │
 │ │ │ 7. Validate
 │ │ │ session
 │ │ │
 │ 8. Todo list data │ │
 │ <───────────────────────────────────────────────────────── │
 │ │ │

Key Security Features

HTTP-only Cookies: Session tokens stored in HTTP-only cookies are completely inaccessible to JavaScript. First line of defense against XSS attacks.

SameSite Protection: SameSite=Lax during development. In production use SameSite=Strict for CSRF protection.

Credentials Configuration: Axios needs withCredentials: true to send cookies with cross-origin requests.

Session Validation on Every Request: Every protected endpoint validates the session cookie. Frontend auth state is only for UX — real security happens on the backend.

Backend Implementation

Quick note: This backend is intentionally minimal. We're using in-memory storage, plain-text passwords, and other shortcuts you'd never use in production. The focus is the frontend auth implementation.

Our backend does three key things:

1. Creates sessions when users log in

@app.post("/auth/login")
def login(request: LoginRequest, response: Response):
 user_data = MOCK_USERS.get(request.email)

 if not user_data or user_data["password"] != request.password:
 raise HTTPException(status_code=401, detail="Invalid credentials")

 token = create_session(user_data["id"])
 set_session_cookie(response, token)
 return LoginSuccess(user_id=user_data["id"], email=request.email)
def create_session(user_id: int) -> str:
 token = secrets.token_urlsafe(32)
 sessions[token] = user_id
 return token

secrets.token_urlsafe(32) generates a cryptographically secure token. Never use random or uuid for session tokens.

def set_session_cookie(response: Response, token: str):
 response.set_cookie(
 key="session",
 value=token,
 httponly=True,
 secure=False, # Set to True in production with HTTPS
 samesite="lax",
 max_age=3600,
 path="/"
 )

2. Validates sessions on protected endpoints

@app.get("/todos")
def list_todos(user: User = Depends(get_current_user)):
 return list(todos.values())
def get_current_user(request: Request) -> User:
 token = request.cookies.get("session")

 if not token or token not in sessions:
 raise HTTPException(status_code=401, detail="Not authenticated")

 user_id = sessions[token]
 # Look up user from database and return User object
 # ...

3. Clears sessions on logout

@app.post("/auth/logout", status_code=204)
def logout(request: Request, response: Response, user: User = Depends(get_current_user)):
 token = get_session_token(request)
 if token:
 invalidate_session(token)
 clear_session_cookie(response)

CORS configuration:

app.add_middleware(
 CORSMiddleware,
 allow_origins=["http://localhost:5173"],
 allow_credentials=True, # Critical: allows cookies
 allow_methods=["*"],
 allow_headers=["*"],
)

allow_credentials=True is essential. Without it, the browser won't send or receive cookies in cross-origin requests.

Frontend Implementation

Step 1: Configure Axios to Send Cookies

// lib/api/axios-config.ts
import axios from 'axios';

axios.defaults.withCredentials = true;

Import this in +layout.ts so it runs before anything else:

// routes/+layout.ts
import '$lib/api/axios-config';

export const csr = true;
export const ssr = false;
export const prerender = false;

Step 2: Build a Reactive Auth Store

// lib/auth/auth.svelte.ts
import type { User } from '$lib/api/gen/model';

class AuthStore {
 user = $state<User | null>(null);
 isLoading = $state(true);

 get isAuthenticated(): boolean {
 return this.user !== null;
 }

 setUser(user: User | null) {
 this.user = user;
 this.isLoading = false;
 }

 clear() {
 this.user = null;
 this.isLoading = false;
 }
}

export const authStore = new AuthStore();

The $state rune makes user and isLoading reactive. Any component that reads them automatically updates when they change. No subscriptions, no boilerplate.

Step 3: Session Validation with Smart Caching

// lib/auth/session.ts
const api = getFastAPI();

let lastSuccessfulCheck = 0;
const AUTH_CHECK_EXPIRES_MS = 20000; // 20 seconds

export async function ensureAuthenticated(): Promise<boolean> {
 const now = Date.now();

 if (authStore.isAuthenticated && now - lastSuccessfulCheck < AUTH_CHECK_EXPIRES_MS) {
 return true;
 }

 if (!authStore.isAuthenticated) {
 authStore.setLoading(true);
 }

 try {
 const response = await api.getCurrentUser();
 authStore.setUser(response.data);
 lastSuccessfulCheck = now;
 return true;
 } catch (error) {
 authStore.clear();
 window.location.href = '/login';
 return false;
 }
}

About the 20-second cache: This is a performance optimization to avoid hammering /users/me, not your session expiry. Your actual session might last 30-60 minutes on the backend. The backend still validates the session on every API call.

Step 4: Protect Routes with a Layout

<!-- routes/(protected)/+layout.svelte -->
<script lang="ts">
 import { onMount } from 'svelte';
 import { ensureAuthenticated } from '$lib/auth/session';
 import { authStore } from '$lib/auth/auth.svelte';

 let { children } = $props();

 onMount(async () => {
 await ensureAuthenticated();
 });
</script>

{#if authStore.isLoading}
 <div class="loading">Loading...</div>
{:else if authStore.isAuthenticated}
 {@render children()}
{:else}
 <div class="loading">Redirecting to login...</div>
{/if}

Any route inside the (protected) folder automatically requires authentication:

routes/
 (protected)/
 +layout.svelte ← Auth check happens here
 todos/
 +page.svelte ← Automatically protected
 profile/
 +page.svelte ← Automatically protected
 login/
 +page.svelte ← Public route

Step 5: Logout

export async function logout(): Promise<void> {
 try {
 await api.logout();
 } catch (error) {
 console.error('Logout failed:', error);
 } finally {
 authStore.clear();
 lastSuccessfulCheck = 0;
 goto('/login');
 }
}

Even if the API call fails, local state clears and the user gets redirected. They can't stay on a protected page without re-authenticating.

Wrapping Up

You now have a working authentication system for your SvelteKit SPA:

  • HTTP-only cookie-based sessions
  • Reactive auth store with Svelte 5 runes
  • Smart caching to reduce backend calls
  • Protected routes via layouts
  • Clean login and logout flows

Source code: GitHub

See also: Full-stack FastAPI Tutorial 1: Project Setup & Tooling

This covers the fundamentals. Production apps need password reset, email verification, OAuth, and RBAC. If you want all of that without building it from scratch, check out FastSvelte — a SvelteKit + FastAPI starter kit with auth, Stripe billing, multi-tenancy, and more already wired up.

Smooth coding!