Skip to content

Production-ready SvelteKit template for OAuth/OIDC authentication using the Backend-for-Frontend (BFF) pattern.

License

Notifications You must be signed in to change notification settings

FrankFMY/auth-bff-oidc-template

Repository files navigation

πŸ” Auth BFF OIDC Template

Production-ready SvelteKit template for OAuth/OIDC authentication using the Backend-for-Frontend (BFF) pattern.

✨ Features

  • πŸ”’ 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

πŸ“‹ Prerequisites

  • Node.js >= 20.0.0
  • pnpm >= 9.0.0 (or npm)
  • OAuth/OIDC Provider (Keycloak, Auth0, Okta, etc.)

πŸš€ Quick Start

1. Clone the repository

git clone https://github.com/FrankFMY/auth-bff-oidc-template.git
cd auth-bff-oidc-template

2. Install dependencies

pnpm install

3. Configure environment variables

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

4. Run the development server

pnpm dev

Open http://localhost:5173 in your browser.

πŸ“¦ Session Store Configuration

Development: Memory Store (Default)

No additional setup required. Memory store is used by default.

⚠️ Warning: Memory store is NOT suitable for production. Sessions are lost on server restart.

Production: Redis Store (Recommended)

  1. Install Redis client:
pnpm add ioredis
  1. Add Redis URL to .env:
REDIS_URL=redis://localhost:6379
  1. Edit 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,
);

Production: PostgreSQL Store

  1. Install PostgreSQL client:
pnpm add pg
  1. 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);
  1. Add Database URL to .env:
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
  1. Edit src/lib/server/auth/index.ts and uncomment PostgreSQL configuration.

πŸ—οΈ Project Structure

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)

πŸ” Authentication Flow

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
Loading

πŸ›‘οΈ Security Features

  • 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

πŸ“ Usage Examples

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,
  };
};

Protected API Endpoint

// 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);
};

User Data in Components

<!-- 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}

βš™οΈ Configuration Options

Rate Limiting

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";
  },
});

Session TTL

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
});

πŸ§ͺ Development

# 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

πŸš€ Deployment

Environment Variables

Ensure these environment variables are set in production:

  • OIDC_ISSUER
  • OIDC_CLIENT_ID
  • OIDC_CLIENT_SECRET
  • OIDC_REDIRECT_URI
  • REDIS_URL or DATABASE_URL (depending on session store)

Build

pnpm build

The build output will be in the .svelte-kit directory. Configure your deployment platform to serve this directory.

Popular Platforms

  • Vercel: Zero-config deployment
  • Netlify: Works out of the box
  • Cloudflare Pages: Supported with adapter-cloudflare
  • Docker: Use Node.js adapter and create Dockerfile

πŸ“š Additional Resources

🀝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

πŸ“„ License

MIT Β© FrankFMY

πŸ“§ Contact

πŸ™ Acknowledgments

  • Inspired by the BFF (Backend-for-Frontend) security pattern
  • Built with SvelteKit and Svelte 5