@sourceregistry/sveltekit-oidc 1.0.4 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,6 +33,7 @@ export const oidc = createOIDC({
33
33
  loginPath: '/auth/login',
34
34
  redirectPath: '/auth/callback',
35
35
  scope: ['openid', 'profile', 'email', 'offline_access'],
36
+ clockSkewSeconds: 30,
36
37
  fetchUserInfo: true,
37
38
  backChannelLogoutStore: createInMemoryBackChannelLogoutStore(),
38
39
  transformSession(session) {
@@ -175,6 +176,7 @@ Set these environment variables to enable it:
175
176
  ## Notes
176
177
 
177
178
  - `cookieSecret` should be a strong random secret and must stay stable across instances.
179
+ - `clockSkewSeconds` defaults to `30` and tolerates small clock drift between your app and the identity provider.
178
180
  - `createInMemoryBackChannelLogoutStore()` is suitable for local development or single-instance deployments. Use Redis, SQL, or another shared store for production.
179
181
  - The library validates `id_token` and `logout_token` values through `@sourceregistry/node-jwt` and provider JWKS metadata.
180
182
  - `groups` are normalized onto the session from `groups` and `roles` claims when present.
@@ -10,6 +10,15 @@ export function createOIDCCookieStore(cookieSecret, sessionCookieName, stateCook
10
10
  clearSession(cookies) {
11
11
  cookies.delete(sessionCookieName, cookieOptions);
12
12
  },
13
+ readSessionReference(cookies) {
14
+ return parseSignedCookie(cookies.get(sessionCookieName), cookieSecret);
15
+ },
16
+ writeSessionReference(cookies, reference) {
17
+ cookies.set(sessionCookieName, serializeSignedCookie(reference, cookieSecret), cookieOptions);
18
+ },
19
+ clearSessionReference(cookies) {
20
+ cookies.delete(sessionCookieName, cookieOptions);
21
+ },
13
22
  readState(cookies) {
14
23
  return parseSignedCookie(cookies.get(stateCookieName), cookieSecret);
15
24
  },
@@ -1,5 +1,5 @@
1
1
  import type { OIDCOptions, OIDCInstance } from './types.js';
2
2
  export type * from './types.js';
3
- export { createInMemoryBackChannelLogoutStore } from './store.js';
3
+ export { createInMemoryBackChannelLogoutStore, createInMemorySessionStore } from './store.js';
4
4
  export declare function createOIDC(options: OIDCOptions): OIDCInstance;
5
5
  export declare const OpenIDConnect: typeof createOIDC;
@@ -5,9 +5,10 @@ import { createOIDCCookieStore } from './cookies.js';
5
5
  import { asAuthorizationHeader, createClientSecretJwtAssertion, createPrivateKeyJwtAssertion, fetchJson } from './jwt.js';
6
6
  import { normalizeTokens, shouldRefresh } from './session.js';
7
7
  import { absoluteUrl, base64UrlEncode, buildCookieOptions, collectGroups, createPKCEPair, normalizeIssuer, normalizeScope, parseProviderError, toPublicSession } from './utils.js';
8
- export { createInMemoryBackChannelLogoutStore } from './store.js';
8
+ export { createInMemoryBackChannelLogoutStore, createInMemorySessionStore } from './store.js';
9
9
  export function createOIDC(options) {
10
10
  const cookieOptions = buildCookieOptions(options.cookieOptions);
11
+ const clockSkewSeconds = options.clockSkewSeconds ?? 30;
11
12
  const refreshToleranceSeconds = options.refreshToleranceSeconds ?? 30;
12
13
  const sessionCookieName = options.sessionCookieName ?? 'oidc_session';
13
14
  const stateCookieName = options.stateCookieName ?? 'oidc_auth_state';
@@ -18,6 +19,45 @@ export function createOIDC(options) {
18
19
  const cookieStore = createOIDCCookieStore(options.cookieSecret, sessionCookieName, stateCookieName, cookieOptions);
19
20
  let metadataPromise;
20
21
  let jwksPromise;
22
+ async function readPersistedSession(cookies) {
23
+ if (!options.sessionStore) {
24
+ const session = cookieStore.readSession(cookies);
25
+ return session ? { session } : null;
26
+ }
27
+ const reference = cookieStore.readSessionReference(cookies);
28
+ if (!reference?.id) {
29
+ return null;
30
+ }
31
+ const session = await options.sessionStore.get(reference.id);
32
+ if (!session) {
33
+ cookieStore.clearSessionReference(cookies);
34
+ return null;
35
+ }
36
+ return {
37
+ id: reference.id,
38
+ session
39
+ };
40
+ }
41
+ async function writePersistedSession(cookies, session, sessionId) {
42
+ if (!options.sessionStore) {
43
+ cookieStore.writeSession(cookies, session);
44
+ return;
45
+ }
46
+ const id = sessionId ?? base64UrlEncode(randomBytes(24));
47
+ await options.sessionStore.set(id, session);
48
+ cookieStore.writeSessionReference(cookies, { id });
49
+ }
50
+ async function clearPersistedSession(cookies, sessionId) {
51
+ if (!options.sessionStore) {
52
+ cookieStore.clearSession(cookies);
53
+ return;
54
+ }
55
+ const id = sessionId ?? cookieStore.readSessionReference(cookies)?.id;
56
+ if (id) {
57
+ await options.sessionStore.delete(id);
58
+ }
59
+ cookieStore.clearSessionReference(cookies);
60
+ }
21
61
  async function getMetadata() {
22
62
  if (!metadataPromise) {
23
63
  metadataPromise = (async () => {
@@ -156,7 +196,8 @@ export function createOIDC(options) {
156
196
  const claims = await verifyJwtWithJwks(idToken, {
157
197
  issuer: metadata.issuer,
158
198
  audience: options.clientId,
159
- algorithms: metadata.id_token_signing_alg_values_supported
199
+ algorithms: metadata.id_token_signing_alg_values_supported,
200
+ clockSkew: clockSkewSeconds
160
201
  });
161
202
  const transformedClaims = options.transformClaims ? await options.transformClaims(claims) : claims;
162
203
  if (transformedClaims.nonce !== nonce) {
@@ -168,7 +209,8 @@ export function createOIDC(options) {
168
209
  const metadata = await getMetadata();
169
210
  const claims = await verifyJwtWithJwks(logoutToken, {
170
211
  issuer: metadata.issuer,
171
- audience: options.clientId
212
+ audience: options.clientId,
213
+ clockSkew: clockSkewSeconds
172
214
  });
173
215
  if (!claims.events?.['http://schemas.openid.net/event/backchannel-logout']) {
174
216
  throw error(400, { message: 'Invalid logout_token events claim' });
@@ -198,12 +240,13 @@ export function createOIDC(options) {
198
240
  }
199
241
  return options.backChannelLogoutStore.isRevoked(session);
200
242
  }
201
- async function maybeRefreshSession(event, session) {
243
+ async function maybeRefreshSession(event, persisted) {
244
+ const session = persisted?.session ?? null;
202
245
  if (!session) {
203
246
  return null;
204
247
  }
205
248
  if (await isRevoked(session)) {
206
- cookieStore.clearSession(event.cookies);
249
+ await clearPersistedSession(event.cookies, persisted?.id);
207
250
  return null;
208
251
  }
209
252
  if (!shouldRefresh(session, refreshToleranceSeconds)) {
@@ -235,16 +278,16 @@ export function createOIDC(options) {
235
278
  isRefresh: true
236
279
  })
237
280
  : nextSession;
238
- cookieStore.writeSession(event.cookies, finalSession);
281
+ await writePersistedSession(event.cookies, finalSession, persisted?.id);
239
282
  return finalSession;
240
283
  }
241
284
  catch {
242
- cookieStore.clearSession(event.cookies);
285
+ await clearPersistedSession(event.cookies, persisted?.id);
243
286
  return null;
244
287
  }
245
288
  }
246
289
  async function getSession(event) {
247
- return maybeRefreshSession(event, cookieStore.readSession(event.cookies));
290
+ return maybeRefreshSession(event, await readPersistedSession(event.cookies));
248
291
  }
249
292
  async function signIn(event, loginOptions = {}) {
250
293
  const metadata = await getMetadata();
@@ -330,7 +373,7 @@ export function createOIDC(options) {
330
373
  isRefresh: false
331
374
  })
332
375
  : session;
333
- cookieStore.writeSession(event.cookies, finalSession);
376
+ await writePersistedSession(event.cookies, finalSession);
334
377
  return {
335
378
  session: finalSession,
336
379
  returnTo: stateCookie.returnTo
@@ -338,9 +381,10 @@ export function createOIDC(options) {
338
381
  }
339
382
  async function signOut(event, logoutOptions = {}) {
340
383
  const metadata = await getMetadata();
341
- const session = cookieStore.readSession(event.cookies);
384
+ const persisted = await readPersistedSession(event.cookies);
385
+ const session = persisted?.session ?? null;
342
386
  cookieStore.clearState(event.cookies);
343
- cookieStore.clearSession(event.cookies);
387
+ await clearPersistedSession(event.cookies, persisted?.id);
344
388
  if (logoutOptions.clearSessionOnly || !metadata.end_session_endpoint) {
345
389
  throw redirect(302, logoutOptions.postLogoutRedirectUri ??
346
390
  options.defaultLogoutRedirect ??
@@ -386,8 +430,8 @@ export function createOIDC(options) {
386
430
  });
387
431
  return new Response(null, { status: 200 });
388
432
  }
389
- function requireAuth(event, returnTo) {
390
- const session = cookieStore.readSession(event.cookies);
433
+ async function requireAuth(event, returnTo) {
434
+ const session = await getSession(event);
391
435
  if (session) {
392
436
  return session;
393
437
  }
@@ -400,12 +444,13 @@ export function createOIDC(options) {
400
444
  session,
401
445
  user: session?.user,
402
446
  claims: session?.claims,
403
- requireAuth: () => {
447
+ requireAuth: async () => {
404
448
  if (!session) {
405
449
  throw error(401, { message: 'Authentication required' });
406
450
  }
451
+ return session;
407
452
  },
408
- clearSession: () => cookieStore.clearSession(event.cookies)
453
+ clearSession: async () => clearPersistedSession(event.cookies)
409
454
  };
410
455
  return resolve(event);
411
456
  };
@@ -499,7 +544,7 @@ export function createOIDC(options) {
499
544
  backChannelLogoutHandler,
500
545
  createActions,
501
546
  requireAuth,
502
- clearSession: cookieStore.clearSession
547
+ clearSession: async (cookies) => clearPersistedSession(cookies)
503
548
  };
504
549
  }
505
550
  export const OpenIDConnect = createOIDC;
@@ -1,2 +1,3 @@
1
- import type { OIDCBackChannelLogoutStore } from './types.js';
1
+ import type { OIDCBackChannelLogoutStore, OIDCSessionStore } from './types.js';
2
2
  export declare function createInMemoryBackChannelLogoutStore(): OIDCBackChannelLogoutStore;
3
+ export declare function createInMemorySessionStore(): OIDCSessionStore;
@@ -16,3 +16,17 @@ export function createInMemoryBackChannelLogoutStore() {
16
16
  }
17
17
  };
18
18
  }
19
+ export function createInMemorySessionStore() {
20
+ const sessions = new Map();
21
+ return {
22
+ async get(sessionId) {
23
+ return sessions.get(sessionId) ?? null;
24
+ },
25
+ async set(sessionId, session) {
26
+ sessions.set(sessionId, session);
27
+ },
28
+ async delete(sessionId) {
29
+ sessions.delete(sessionId);
30
+ }
31
+ };
32
+ }
@@ -118,6 +118,11 @@ export type OIDCBackChannelLogoutStore = {
118
118
  revoke(record: OIDCBackChannelLogoutRecord): MaybePromise<void>;
119
119
  isRevoked(session: OIDCSession): MaybePromise<boolean>;
120
120
  };
121
+ export type OIDCSessionStore = {
122
+ get(sessionId: string): MaybePromise<OIDCSession | null>;
123
+ set(sessionId: string, session: OIDCSession): MaybePromise<void>;
124
+ delete(sessionId: string): MaybePromise<void>;
125
+ };
121
126
  export type OIDCSessionManagementConfig = {
122
127
  clientId: string;
123
128
  loginPath: string;
@@ -146,9 +151,11 @@ export type OIDCOptions = {
146
151
  stateCookieName?: string;
147
152
  cookieSecret: string;
148
153
  cookieOptions?: Partial<CookieOptions>;
154
+ clockSkewSeconds?: number;
149
155
  refreshToleranceSeconds?: number;
150
156
  defaultLoginRedirect?: string;
151
157
  defaultLogoutRedirect?: string;
158
+ sessionStore?: OIDCSessionStore;
152
159
  backChannelLogoutStore?: OIDCBackChannelLogoutStore;
153
160
  transformClaims?: (claims: OIDCUserClaims) => MaybePromise<OIDCUserClaims>;
154
161
  transformUser?: (user: OIDCUserClaims | undefined, context: {
@@ -183,8 +190,8 @@ export type OIDCHandleLocals = {
183
190
  session: OIDCSession | null;
184
191
  user?: OIDCUserClaims;
185
192
  claims?: OIDCUserClaims;
186
- requireAuth: () => void;
187
- clearSession: () => void;
193
+ requireAuth: () => Promise<OIDCSession>;
194
+ clearSession: () => Promise<void>;
188
195
  };
189
196
  export type OIDCStateCookie = {
190
197
  state: string;
@@ -211,10 +218,21 @@ export type OIDCCookies = {
211
218
  readSession(cookies: Cookies): OIDCSession | null;
212
219
  writeSession(cookies: Cookies, session: OIDCSession): void;
213
220
  clearSession(cookies: Cookies): void;
221
+ readSessionReference(cookies: Cookies): {
222
+ id: string;
223
+ } | null;
224
+ writeSessionReference(cookies: Cookies, reference: {
225
+ id: string;
226
+ }): void;
227
+ clearSessionReference(cookies: Cookies): void;
214
228
  readState(cookies: Cookies): OIDCStateCookie | null;
215
229
  writeState(cookies: Cookies, state: OIDCStateCookie): void;
216
230
  clearState(cookies: Cookies): void;
217
231
  };
232
+ export type OIDCPersistedSession = {
233
+ id?: string;
234
+ session: OIDCSession;
235
+ };
218
236
  export type OIDCInstance = {
219
237
  handle: Handle;
220
238
  getMetadata: () => Promise<OIDCDiscoveryDocument>;
@@ -233,6 +251,6 @@ export type OIDCInstance = {
233
251
  login: Action;
234
252
  logout: Action;
235
253
  }>;
236
- requireAuth: (event: RequestEvent, returnTo?: string) => OIDCSession;
237
- clearSession: (cookies: Cookies) => void;
254
+ requireAuth: (event: RequestEvent, returnTo?: string) => Promise<OIDCSession>;
255
+ clearSession: (cookies: Cookies) => Promise<void>;
238
256
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sourceregistry/sveltekit-oidc",
3
- "version": "1.0.4",
3
+ "version": "1.1.1",
4
4
  "description": "OIDC authentication helpers for SvelteKit applications.",
5
5
  "license": "Apache-2.0",
6
6
  "scripts": {