Production-ready SvelteKit template for OAuth/OIDC authentication using the Backend-for-Frontend (BFF) pattern.
git clone https://github.com/FrankFMY/auth-bff-oidc-template.git
cd auth-bff-oidc-template
pnpm install
Create .env file in the project root:
# OIDC Configuration
OIDC_ISSUER=https://your-oidc-provider.com
OIDC_CLIENT_ID=your-client-id
OIDC_CLIENT_SECRET=your-client-secret
OIDC_REDIRECT_URI=http://localhost:5173/auth/callback
# Session Configuration (optional)
# SESSION_SECRET=your-random-secret-key
pnpm dev
Open http://localhost:5173 in your browser.
No additional setup required. Memory store is used by default.
โ ๏ธ Warning: Memory store is NOT suitable for production. Sessions are lost on server restart.
pnpm add ioredis
.env:REDIS_URL=redis://localhost:6379
src/lib/server/auth/index.ts and uncomment Redis configuration:// Uncomment this section
import { Redis } from "ioredis";
import { RedisSessionStore } from "./stores/redis.js";
import { REDIS_URL } from "$env/static/private";
const redis = new Redis(REDIS_URL || "redis://localhost:6379");
const sessionStore = new RedisSessionStore(redis);
export const authService = new BFFAuthService(
{
issuer: OIDC_ISSUER,
clientId: OIDC_CLIENT_ID,
clientSecret: OIDC_CLIENT_SECRET,
redirectUri: OIDC_REDIRECT_URI,
scopes: ["openid", "profile", "email"],
},
sessionStore,
);
pnpm add pg
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
data JSONB NOT NULL,
expires_at BIGINT NOT NULL
);
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);
.env:DATABASE_URL=postgresql://user:password@localhost:5432/dbname
src/lib/server/auth/index.ts and uncomment PostgreSQL configuration.src/
โโโ lib/
โ โโโ server/
โ โโโ auth/
โ โโโ bff.ts # Core BFF Auth Service
โ โโโ index.ts # Auth configuration
โ โโโ middleware.ts # Authentication middleware
โ โโโ rate-limiter.ts # Rate limiting
โ โโโ session-store.ts # Session store interface
โ โโโ utils.ts # Utility functions
โ โโโ stores/
โ โโโ memory.ts # Memory session store
โ โโโ redis.ts # Redis session store
โ โโโ postgres.ts # PostgreSQL session store
โโโ routes/
โ โโโ +layout.server.ts # User data injection
โ โโโ +page.svelte # Home page
โ โโโ auth/
โ โ โโโ login/+server.ts # Login endpoint
โ โ โโโ callback/+server.ts # OAuth callback
โ โ โโโ logout/+server.ts # Logout endpoint
โ โโโ api/
โ โโโ user/
โ โโโ profile/+server.ts # Protected API example
โโโ hooks.server.ts # Global hooks (auth middleware)
sequenceDiagram
participant Browser
participant BFF (SvelteKit)
participant OIDC Provider
Browser->>BFF: GET /auth/login
BFF->>BFF: Generate PKCE challenge
BFF->>OIDC Provider: Redirect to authorization URL
OIDC Provider->>Browser: Login page
Browser->>OIDC Provider: Enter credentials
OIDC Provider->>BFF: Redirect to /auth/callback?code=...
BFF->>OIDC Provider: Exchange code for tokens (with PKCE)
OIDC Provider->>BFF: Return tokens
BFF->>BFF: Store tokens in session
BFF->>Browser: Set HTTP-only cookie, redirect to /
Browser->>BFF: GET / (with cookie)
BFF->>BFF: Validate session
BFF->>Browser: Return protected page
// src/routes/dashboard/+page.server.ts
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async ({ locals }) => {
if (!locals.user) {
redirect(303, "/auth/login");
}
return {
user: locals.user,
};
};
// src/routes/api/posts/+server.ts
import { json, error } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
export const GET: RequestHandler = async ({ locals }) => {
if (!locals.user) {
error(401, "Unauthorized");
}
const posts = await db.getPosts(locals.user.sub);
return json(posts);
};
<!-- src/routes/+page.svelte -->
<script lang="ts">
import type { PageProps } from "./$types";
let { data }: PageProps = $props();
</script>
{#if data.user}
<h1>Welcome, {data.user.name}!</h1>
<a href="/auth/logout">Logout</a>
{:else}
<a href="/auth/login">Login</a>
{/if}
Configure in src/lib/server/auth/rate-limiter.ts:
const limiter = new RateLimiter({
windowMs: 15 * 60 * 1000, // 15 minutes
maxRequests: 5, // 5 requests per window
keyGenerator: (request) => {
// Generate unique key per IP
return request.headers.get("x-forwarded-for") || "unknown";
},
});
Configure session expiration time:
// Redis
const sessionStore = new RedisSessionStore(redis, {
prefix: "session:",
defaultTTL: 86400, // 24 hours in seconds
});
// PostgreSQL
const sessionStore = new PostgresSessionStore(pool, {
tableName: "sessions",
cleanupIntervalMs: 3600000, // Cleanup every hour
});
# Run dev server
pnpm dev
# Type checking
pnpm check
# Linting
pnpm lint
# Format code
pnpm format
# Build for production
pnpm build
# Preview production build
pnpm preview
Ensure these environment variables are set in production:
OIDC_ISSUEROIDC_CLIENT_IDOIDC_CLIENT_SECRETOIDC_REDIRECT_URIREDIS_URL or DATABASE_URL (depending on session store)pnpm build
The build output will be in the .svelte-kit directory. Configure your deployment platform to serve this directory.
Contributions are welcome! Please feel free to submit a Pull Request.
MIT ยฉ FrankFMY