Production-ready SvelteKit template for OAuth/OIDC authentication using the Backend-for-Frontend (BFF) pattern.
- π Secure by Design: Tokens never leave the server, only HTTP-only cookies in the browser
- β‘ PKCE Flow: Protection against authorization code interception attacks
- π¦ Flexible Session Storage:
- Memory Store (development)
- Redis Store (production, recommended)
- PostgreSQL Store (if you already use Postgres)
 
- π‘οΈ Rate Limiting: Built-in protection against brute-force attacks
- π― Type-Safe: Full TypeScript support with SvelteKit's generated types
- π Svelte 5: Modern reactive patterns with runes
- π Token Refresh: Automatic token renewal before expiration
- π§Ή Session Cleanup: Automatic cleanup of expired sessions
- Node.js >= 20.0.0
- pnpm >= 9.0.0 (or npm)
- OAuth/OIDC Provider (Keycloak, Auth0, Okta, etc.)
git clone https://github.com/FrankFMY/auth-bff-oidc-template.git
cd auth-bff-oidc-templatepnpm installCreate .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-keypnpm devOpen http://localhost:5173 in your browser.
No additional setup required. Memory store is used by default.
- Install Redis client:
pnpm add ioredis- Add Redis URL to .env:
REDIS_URL=redis://localhost:6379- Edit src/lib/server/auth/index.tsand 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,
);- Install PostgreSQL client:
pnpm add pg- Create sessions table:
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);- Add Database URL to .env:
DATABASE_URL=postgresql://user:password@localhost:5432/dbname- Edit src/lib/server/auth/index.tsand 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
    - No Token Exposure: Access/refresh tokens never reach the browser
- HTTP-Only Cookies: Session IDs are stored in secure, HTTP-only cookies
- PKCE: Protection against authorization code interception
- Rate Limiting: Configurable limits on authentication endpoints
- CSRF Protection: Built-in SvelteKit CSRF protection
- Token Refresh: Automatic token renewal 5 minutes before expiration
- Session Expiration: Automatic cleanup of expired sessions
// 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 previewEnsure these environment variables are set in production:
- OIDC_ISSUER
- OIDC_CLIENT_ID
- OIDC_CLIENT_SECRET
- OIDC_REDIRECT_URI
- REDIS_URLor- DATABASE_URL(depending on session store)
pnpm buildThe build output will be in the .svelte-kit directory. Configure your deployment platform to serve this directory.
- Vercel: Zero-config deployment
- Netlify: Works out of the box
- Cloudflare Pages: Supported with adapter-cloudflare
- Docker: Use Node.js adapter and create Dockerfile
Contributions are welcome! Please feel free to submit a Pull Request.
MIT Β© FrankFMY
- GitHub: @FrankFMY
- Email: Pryanishnikovartem@gmail.com