SvelteKit + better-auth + Keycloak (OIDC) Example
This repo demonstrates how to integrate Keycloak (as an OpenID Connect provider) with SvelteKit using better-auth. It shows a minimal setup for server-side session handling and client-side session usage, with a ready-to-use local Keycloak configuration for manual testing.
What you get in this example
genericOAuth against KeycloakPrerequisites
Install & run the SvelteKit app
npm install
npm run dev
Start a local Keycloak with Docker
Run Keycloak in dev mode on port 8080:
docker run --name keycloak -p 8080:8080 \
-e KEYCLOAK_ADMIN=admin \
-e KEYCLOAK_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:26.0 \
start-dev
Once it’s up, open http://localhost:8080 and log in with the admin credentials you set above.
Configure Keycloak for this example
This project expects a realm named customers and a public client named svelte-app with PKCE enabled. The corresponding better-auth configuration points to the OIDC discovery endpoint at:
http://localhost:8080/realms/customers/.well-known/openid-configurationSteps in the Keycloak Admin Console:
Create a realm:
customersCreate a client:
svelte-appConfigure client URLs and PKCE:
**Create a test user:
With this in place, the app will redirect unauthenticated users to Keycloak and handle the callback.
How it works in this repo
Server-side better-auth configuration: src/lib/auth.ts
export const auth = betterAuth({
advanced: {
cookies: {
state: { attributes: { sameSite: "lax", secure: false } }
}
},
plugins: [
genericOAuth({
config: [{
providerId: "keycloak",
clientId: "svelte-app",
discoveryUrl: "http://localhost:8080/realms/customers/.well-known/openid-configuration",
scopes: ["openid", "profile", "email"],
pkce: true,
redirectURI: "http://localhost:5173/api/auth/callback/keycloak"
}]
}),
sveltekitCookies(getRequestEvent)
]
})
Notes:
secure: false and sameSite: "lax" are suitable for local HTTP dev only; for production behind HTTPS set secure: true and consider sameSite as needed.better-auth SvelteKit route handler: src/routes/api/auth/[...all]/+server.ts
const handler = toSvelteKitHandler(auth)
export { handler as GET, handler as POST }
Global hooks: src/hooks.server.ts
setSessionHook wires better-auth per-request.checkAuthHook redirects to Keycloak if there’s no session yet:const res = await auth.api.signInSocial({
body: { provider: 'keycloak', callbackURL: event.url.href }
})
redirect(302, res.url!)
event.locals.user.Client-side usage: src/lib/auth-client.ts
export const authClient = createAuthClient({
baseURL: "http://localhost:5173",
plugins: [genericOAuthClient()]
})
In src/routes/+page.svelte the session is read and displayed.
Running the full flow locally
npm run dev -- --open.http://localhost:5173 in your browser.email and name rendered.Common pitfalls and tips
secure: false are required; in production use HTTPS and set secure: true.