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
- Completed the SvelteKit SPA with FastAPI tutorial or have a similar setup
- Basic understanding of SvelteKit and FastAPI
- Familiarity with Svelte 5 runes (
$state,$effect)
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:
- User enters their email and password in the login form
- Frontend sends credentials to the backend's
/auth/loginendpoint - Backend validates the credentials against the user database (in-memory for this tutorial)
- Backend creates a session token and sends it back as an HTTP-only cookie
- Browser automatically stores the cookie (JavaScript cannot access it due to
httponlyflag) - 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!
Top comments (0)