SvelteKit Integration
SvelteKit Integration
This guide covers how to integrate Skycloak authentication into SvelteKit applications using @auth/sveltekit (Auth.js) with a generic OpenID Connect provider configuration.
Prerequisites
- SvelteKit 2.x application
- Node.js 18+
- Skycloak cluster with configured realm and client
- Basic understanding of SvelteKit hooks and load functions
Quick Start
1. Create an Application in Skycloak
- In Skycloak, navigate to your cluster → Applications → Create Application
- Set Application Type to
Confidential(Auth.js exchanges the authorization code server-side) - Add Redirect URIs:
http://localhost:5173/auth/callback/skycloakfor local dev, plus your production URL - After creation, copy the Client ID and Client Secret from the credentials tab
2. Install Dependencies
npm install @auth/sveltekit @auth/core3. Configure Environment Variables
Create .env:
AUTH_SECRET=a-random-32-char-string-generated-with-openssl-rand
AUTH_TRUST_HOST=true
SKYCLOAK_CLIENT_ID=your-sveltekit-app
SKYCLOAK_CLIENT_SECRET=your-client-secret
SKYCLOAK_ISSUER=https://your-cluster-id.app.skycloak.io/realms/your-realmGenerate AUTH_SECRET with:
openssl rand -base64 324. Configure Auth.js with a Generic OIDC Provider
Create src/auth.ts:
import { SvelteKitAuth } from '@auth/sveltekit'
import type { Provider } from '@auth/core/providers'
import { env } from '$env/dynamic/private'
function SkycloakProvider(): Provider {
return {
id: 'skycloak',
name: 'Skycloak',
type: 'oidc',
issuer: env.SKYCLOAK_ISSUER,
clientId: env.SKYCLOAK_CLIENT_ID,
clientSecret: env.SKYCLOAK_CLIENT_SECRET,
authorization: { params: { scope: 'openid profile email' } },
checks: ['pkce', 'state'],
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
realmRoles: profile.realm_access?.roles ?? [],
}
},
}
}
export const { handle, signIn, signOut } = SvelteKitAuth({
providers: [SkycloakProvider()],
trustHost: true,
callbacks: {
async jwt({ token, account, profile }) {
if (account) {
token.accessToken = account.access_token
token.idToken = account.id_token
token.refreshToken = account.refresh_token
token.realmRoles = (profile as { realm_access?: { roles?: string[] } })
?.realm_access?.roles ?? []
}
return token
},
async session({ session, token }) {
session.accessToken = token.accessToken as string
session.realmRoles = (token.realmRoles as string[]) ?? []
return session
},
},
})5. Wire Up the Server Hook
// src/hooks.server.ts
import { handle as authHandle } from './auth'
export const handle = authHandle6. Add Login/Logout Controls
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import { signIn, signOut } from '@auth/sveltekit/client'
export let data
</script>
{#if data.session?.user}
<span>Welcome, {data.session.user.name}</span>
<button on:click={() => signOut()}>Sign Out</button>
{:else}
<button on:click={() => signIn('skycloak')}>Sign In with Skycloak</button>
{/if}// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types'
export const load: LayoutServerLoad = async (event) => {
return { session: await event.locals.auth() }
}Protecting Routes
Server Load Function Guard
// src/routes/dashboard/+layout.server.ts
import { redirect } from '@sveltejs/kit'
import type { LayoutServerLoad } from './$types'
export const load: LayoutServerLoad = async (event) => {
const session = await event.locals.auth()
if (!session?.user) {
throw redirect(303, '/auth/signin')
}
return { session }
}Role-Based Guard in hooks.server.ts
// src/hooks.server.ts
import { sequence } from '@sveltejs/kit/hooks'
import { handle as authHandle } from './auth'
import { redirect, type Handle } from '@sveltejs/kit'
const requireAdmin: Handle = async ({ event, resolve }) => {
if (event.url.pathname.startsWith('/admin')) {
const session = await event.locals.auth()
const roles = (session as { realmRoles?: string[] })?.realmRoles ?? []
if (!roles.includes('admin')) {
throw redirect(303, '/unauthorized')
}
}
return resolve(event)
}
export const handle = sequence(authHandle, requireAdmin)Protected API Endpoint
// src/routes/api/orders/+server.ts
import { json, error } from '@sveltejs/kit'
import type { RequestHandler } from './$types'
export const GET: RequestHandler = async (event) => {
const session = await event.locals.auth()
if (!session?.user) {
throw error(401, 'Unauthorized')
}
const response = await fetch('https://api.example.com/orders', {
headers: { Authorization: `Bearer ${session.accessToken}` },
})
return json(await response.json())
}Production Considerations
- Set
AUTH_SECRET,SKYCLOAK_CLIENT_SECRET, andSKYCLOAK_ISSUERthrough your hosting platform’s secret store, not committed.envfiles. -
AUTH_TRUST_HOST=trueis required behind most reverse proxies (Vercel, Fly.io, containers) so Auth.js trusts the forwarded host header — only enable it once your deployment terminates TLS and controls that header. - Redirect URIs must match exactly what’s registered on the Application in Skycloak, including the
/auth/callback/skycloakpath Auth.js expects for theskycloakprovider id.
Troubleshooting
-
OAuthCallbackError/ redirect URI mismatch — the callback path is always/auth/callback/<provider id>; since the provider id here isskycloak, the registered Redirect URI must end in/auth/callback/skycloak. -
session.accessTokenisundefined— confirm thejwtcallback insrc/auth.tsruns on first sign-in (accountis only present on that first call) and thatsessionmapping reads fromtoken, notaccount. -
Roles missing from session — realm roles arrive in the ID token’s
realm_access.rolesclaim only if the client’s scope includes it by default in Skycloak; check the client’s assigned scopes ifprofile.realm_accessisundefined.
Next Steps
Last updated on