OIDC authentication and session management for SvelteKit.
The library keeps three concerns separate:
It implements the protocol itself and does not depend on openid-client.
npm install @sourceregistry/sveltekit-oidc
// src/lib/server/auth.ts
import {createOIDC} from '@sourceregistry/sveltekit-oidc/server';
type Identity = {
sub: string;
email?: string;
name?: string;
roles: string[];
permissions?: string[];
};
type RequestData = {
permissions: string[];
};
export const oidc = createOIDC<Identity, RequestData>({
issuer: 'https://identity.example.com',
clientId: process.env.OIDC_CLIENT_ID!,
clientSecret: process.env.OIDC_CLIENT_SECRET!,
clientAuthMethod: 'client_secret_basic',
cookieSecret: process.env.OIDC_COOKIE_SECRET!,
scope: ['openid', 'profile', 'email', 'offline_access'],
resolveIdentity: ({idTokenClaims, userInfo}) => ({
sub: idTokenClaims.sub,
email: userInfo?.email ?? idTokenClaims.email,
name: userInfo?.name ?? idTokenClaims.name,
roles: Array.isArray(userInfo?.roles ?? idTokenClaims.roles)
? ((userInfo?.roles ?? idTokenClaims.roles) as string[])
: []
}),
beforeSessionPersist: async ({session, reason}) => {
await synchronizeUser(session.identity, reason);
},
loadRequestData: async ({session, event}) => ({
permissions: await loadPermissions(session.sub!, event)
}),
createPublicSession: ({base, data}) => ({
...base,
identity: {
...base.identity,
permissions: data?.permissions ?? []
}
})
});
The extension points have deliberately literal names:
| Extension point | When it runs | Persisted |
|---|---|---|
resolveIdentity |
After provider data is validated, on login and refresh | Its result is persisted |
beforeSessionPersist |
Immediately before a login or refreshed session is written | Side effects only |
loadRequestData |
Once while handle builds an authenticated request context |
Never |
createPublicSession |
When getPublicSession or toPublicSession projects a session |
Never |
Both login and refresh are explicit in the callback context:
beforeSessionPersist: async ({session, reason}) => {
if (reason === 'login') {
await recordLogin(session.identity);
}
};
// src/hooks.server.ts
import {oidc} from '$lib/server/auth';
export const handle = oidc.handle;
For every request, handle exposes:
event.locals.oidc.session; // persisted OIDC session
event.locals.oidc.identity; // resolved identity
event.locals.oidc.data; // request-only application data
Type the locals directly from the configured instance:
// src/app.d.ts
import type {OIDCLocals} from '@sourceregistry/sveltekit-oidc/server';
import type {oidc} from '$lib/server/auth';
declare global {
namespace App {
interface Locals {
oidc?: OIDCLocals<typeof oidc>;
}
}
}
export {};
// src/routes/auth/login/+server.ts
import {oidc} from '$lib/server/auth';
export const GET = oidc.loginHandler();
// src/routes/auth/callback/+server.ts
import {oidc} from '$lib/server/auth';
export const GET = oidc.callbackHandler();
// src/routes/auth/logout/+server.ts
import {oidc} from '$lib/server/auth';
export const POST = oidc.logoutHandler();
// src/routes/auth/backchannel-logout/+server.ts
import {oidc} from '$lib/server/auth';
export const POST = oidc.backChannelLogoutHandler();
The underlying operations are also available directly when a route needs custom behavior:
login(event, options)handleCallback(event)logout(event, options)handleBackChannelLogout(event)getSession(event)requireAuth(event)clearSession(cookies)Load a token-free session for the browser:
// src/routes/+layout.server.ts
import {oidc} from '$lib/server/auth';
export async function load(event) {
return {
session: oidc.toPublicSession(event.locals.oidc, event.depends),
sessionManagement: await oidc.getSessionManagementConfig()
};
}
toPublicSession projects the request context already loaded by handle. It does not read the
store, refresh tokens, or load application data again. createPublicSession receives both the
persisted session and loadRequestData result, but only exposes what the application explicitly
returns. getPublicSession(event) is available when the hook has not already loaded the context.
{@render children()}
{#if oidc.isAuthenticated}
Signed in as {oidc.identity?.email ?? oidc.identity?.name}
{/if}
OIDCContext supports local expiry handling, targeted SvelteKit revalidation,
check_session_iframe monitoring, and local or provider logout.
When the OP iframe reports changed, the component first performs the Session Management 1.0
prompt=none authorization check in a hidden iframe. The login handler supplies the current ID token
as id_token_hint; a matching End-User refreshes the local session, while an OP error or a different
End-User clears it. Applications using the standard loginHandler() and callbackHandler() routes do
not need an additional endpoint.
Without sessionStore, the encrypted session is stored in the cookie. For server-side sessions:
import type {OIDCSessionStore} from '@sourceregistry/sveltekit-oidc/server';
const sessionStore: OIDCSessionStore<Identity> = {
get: (id) => redis.get(`session:${id}`),
set: async (id, session) => {
await redis.set(`session:${id}`, session);
},
delete: async (id) => {
await redis.delete(`session:${id}`);
}
};
Use a shared backChannelLogoutStore when back-channel logout must work across multiple instances.
The built-in 'memory' stores are intended for local development or single-process deployments.
exp, and iat.sub must match the validated ID token subject.none, client_secret_basic, client_secret_post, client_secret_jwt, and private_key_jwt.Application code can normalize provider-specific data in resolveIdentity, but cannot replace the
validated ID token claims used by the protocol implementation.