DEV Community

Tortoise
Tortoise

Posted on

How to Add Authentication to a SvelteKit SPA

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     │                              │
   │  <────────────────────────── │                              │
   │                              │                              │
Enter fullscreen mode Exit fullscreen mode

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           │                              │
   │  <───────────────────────────────────────────────────────── │
   │                              │                              │
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode
def create_session(user_id: int) -> str:
    token = secrets.token_urlsafe(32)
    sessions[token] = user_id
    return token
Enter fullscreen mode Exit fullscreen mode

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="/"
    )
Enter fullscreen mode Exit fullscreen mode

2. Validates sessions on protected endpoints

@app.get("/todos")
def list_todos(user: User = Depends(get_current_user)):
    return list(todos.values())
Enter fullscreen mode Exit fullscreen mode
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
    # ...
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

CORS configuration:

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

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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');
    }
}
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)