@sourceregistry/sveltekit-oidc 1.8.0 → 2.0.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.
@@ -1,2 +1,2 @@
1
- import type { CookieOptions, OIDCCookies, OIDCSession, OIDCUserClaims } from './types.js';
2
- export declare function createOIDCCookieStore<TClaims extends OIDCUserClaims = OIDCUserClaims, TSession extends OIDCSession<TClaims> = OIDCSession<TClaims>>(cookieSecret: string, sessionCookieName: string, stateCookieName: string, cookieOptions: CookieOptions): OIDCCookies<TClaims, TSession>;
1
+ import type { CookieOptions, OIDCCookies, OIDCUserClaims } from './types.js';
2
+ export declare function createOIDCCookieStore<TIdentity extends OIDCUserClaims = OIDCUserClaims>(cookieSecret: string, sessionCookieName: string, stateCookieName: string, cookieOptions: CookieOptions): OIDCCookies<TIdentity>;
@@ -1,5 +1,5 @@
1
- import type { OIDCInstance, OIDCOptions, OIDCSession, OIDCUserClaims } from './types.js';
1
+ import type { OIDCInstance, OIDCOptions, OIDCUserClaims } from './types.js';
2
2
  export type * from './types.js';
3
3
  export { createInMemoryBackChannelLogoutStore, createInMemorySessionStore } from './store.js';
4
- export declare function createOIDC<TClaims extends OIDCUserClaims = OIDCUserClaims, TSession extends OIDCSession<TClaims> = OIDCSession<TClaims>>(options: OIDCOptions<TClaims, TSession>): OIDCInstance<TClaims, TSession>;
4
+ export declare function createOIDC<TIdentity extends OIDCUserClaims = OIDCUserClaims, TRequestData = undefined>(options: OIDCOptions<TIdentity, TRequestData>): OIDCInstance<TIdentity, TRequestData>;
5
5
  export declare const OpenIDConnect: typeof createOIDC;
@@ -1,4 +1,4 @@
1
- import { error, redirect, } from '@sveltejs/kit';
1
+ import { error, redirect } from '@sveltejs/kit';
2
2
  import { randomBytes } from 'node:crypto';
3
3
  import { decode, fromWeb, verify } from '@sourceregistry/node-jwt/promises';
4
4
  import { createOIDCCookieStore } from './cookies.js';
@@ -9,8 +9,7 @@ import { createInMemoryBackChannelLogoutStore, createInMemorySessionStore } from
9
9
  export { createInMemoryBackChannelLogoutStore, createInMemorySessionStore } from './store.js';
10
10
  const OIDC_SESSION_REVALIDATION_DEPENDENCY = 'oidc:session';
11
11
  function buildLogger(logger) {
12
- const noop = () => {
13
- };
12
+ const noop = () => { };
14
13
  if (logger === false)
15
14
  return { debug: noop, info: noop, warn: noop, error: noop };
16
15
  if (logger)
@@ -18,21 +17,19 @@ function buildLogger(logger) {
18
17
  debug: (...a) => logger.debug?.(...a),
19
18
  info: (...a) => logger.info?.(...a),
20
19
  warn: (...a) => logger.warn?.(...a),
21
- error: (...a) => logger.error?.(...a),
20
+ error: (...a) => logger.error?.(...a)
22
21
  };
23
22
  const p = '[sveltekit-oidc]';
24
23
  return {
25
24
  debug: (...a) => console.debug(p, ...a),
26
25
  info: (...a) => console.info(p, ...a),
27
26
  warn: (...a) => console.warn(p, ...a),
28
- error: (...a) => console.error(p, ...a),
27
+ error: (...a) => console.error(p, ...a)
29
28
  };
30
29
  }
31
30
  export function createOIDC(options) {
32
31
  const log = buildLogger(options.logger);
33
- const sessionStore = options.sessionStore === 'memory'
34
- ? createInMemorySessionStore()
35
- : options.sessionStore;
32
+ const sessionStore = options.sessionStore === 'memory' ? createInMemorySessionStore() : options.sessionStore;
36
33
  const backChannelLogoutStore = options.backChannelLogoutStore === 'memory'
37
34
  ? createInMemoryBackChannelLogoutStore()
38
35
  : options.backChannelLogoutStore;
@@ -54,10 +51,20 @@ export function createOIDC(options) {
54
51
  const cookieStore = createOIDCCookieStore(options.cookieSecret, sessionCookieName, stateCookieName, cookieOptions);
55
52
  let metadataPromise;
56
53
  let jwksPromise;
54
+ function hasCurrentSessionShape(session) {
55
+ return Boolean(session?.identity?.sub && session.idTokenClaims?.sub);
56
+ }
57
57
  async function readPersistedSession(cookies) {
58
58
  if (!sessionStore) {
59
59
  const session = cookieStore.readSession(cookies);
60
- return session ? { session } : null;
60
+ if (!session)
61
+ return null;
62
+ if (!hasCurrentSessionShape(session)) {
63
+ log.debug('Discarding an incompatible OIDC session');
64
+ cookieStore.clearSession(cookies);
65
+ return null;
66
+ }
67
+ return { session };
61
68
  }
62
69
  const reference = cookieStore.readSessionReference(cookies);
63
70
  if (!reference?.id) {
@@ -70,6 +77,12 @@ export function createOIDC(options) {
70
77
  cookieStore.clearSessionReference(cookies);
71
78
  return null;
72
79
  }
80
+ if (!hasCurrentSessionShape(session)) {
81
+ log.debug('Discarding an incompatible persisted OIDC session');
82
+ await sessionStore.delete(reference.id);
83
+ cookieStore.clearSessionReference(cookies);
84
+ return null;
85
+ }
73
86
  return {
74
87
  id: reference.id,
75
88
  session
@@ -108,7 +121,9 @@ export function createOIDC(options) {
108
121
  const issuer = options.issuer ? normalizeIssuer(options.issuer) : undefined;
109
122
  const discoveryUrl = options.discoveryUrl ?? (issuer ? `${issuer}/.well-known/openid-configuration` : undefined);
110
123
  if (!discoveryUrl) {
111
- throw error(500, { message: 'OIDC issuer or discoveryUrl must be configured' });
124
+ throw error(500, {
125
+ message: 'OIDC issuer or discoveryUrl must be configured'
126
+ });
112
127
  }
113
128
  const document = await fetchJson(discoveryUrl, undefined, fetchImpl);
114
129
  return {
@@ -124,9 +139,14 @@ export function createOIDC(options) {
124
139
  if (!jwksPromise) {
125
140
  jwksPromise = getMetadata().then((metadata) => {
126
141
  if (!metadata.jwks_uri) {
127
- throw error(500, { message: 'OIDC jwks_uri is required to validate id_token values' });
142
+ throw error(500, {
143
+ message: 'OIDC jwks_uri is required to validate id_token values'
144
+ });
128
145
  }
129
- return fromWeb(metadata.jwks_uri, { overrideEndpointCheck: true, fetch: fetchImpl });
146
+ return fromWeb(metadata.jwks_uri, {
147
+ overrideEndpointCheck: true,
148
+ fetch: fetchImpl
149
+ });
130
150
  });
131
151
  }
132
152
  return jwksPromise;
@@ -142,7 +162,9 @@ export function createOIDC(options) {
142
162
  const jwks = await getJwks();
143
163
  const key = decoded.header.kid ? await jwks.key(decoded.header.kid) : (await jwks.list())[0];
144
164
  if (!key) {
145
- throw error(401, { message: 'Unable to resolve a signing key from JWKS' });
165
+ throw error(401, {
166
+ message: 'Unable to resolve a signing key from JWKS'
167
+ });
146
168
  }
147
169
  try {
148
170
  const result = await verify(token, key.toKeyObject(), verifyOptions);
@@ -150,9 +172,7 @@ export function createOIDC(options) {
150
172
  }
151
173
  catch (err) {
152
174
  throw error(401, {
153
- message: typeof err === 'object' && err && 'reason' in err
154
- ? String(err.reason)
155
- : 'JWT verification failed'
175
+ message: typeof err === 'object' && err && 'reason' in err ? String(err.reason) : 'JWT verification failed'
156
176
  });
157
177
  }
158
178
  }
@@ -167,13 +187,17 @@ export function createOIDC(options) {
167
187
  return { headers, body };
168
188
  case 'client_secret_basic':
169
189
  if (!options.clientSecret) {
170
- throw error(500, { message: 'clientSecret is required for client_secret_basic' });
190
+ throw error(500, {
191
+ message: 'clientSecret is required for client_secret_basic'
192
+ });
171
193
  }
172
194
  headers.authorization = asAuthorizationHeader(options.clientId, options.clientSecret);
173
195
  return { headers, body };
174
196
  case 'client_secret_post':
175
197
  if (!options.clientSecret) {
176
- throw error(500, { message: 'clientSecret is required for client_secret_post' });
198
+ throw error(500, {
199
+ message: 'clientSecret is required for client_secret_post'
200
+ });
177
201
  }
178
202
  body.set('client_id', options.clientId);
179
203
  body.set('client_secret', options.clientSecret);
@@ -190,7 +214,9 @@ export function createOIDC(options) {
190
214
  return { headers, body };
191
215
  case 'private_key_jwt':
192
216
  if (!options.privateKeyJwt?.privateKey) {
193
- throw error(500, { message: 'privateKeyJwt.privateKey is required for private_key_jwt' });
217
+ throw error(500, {
218
+ message: 'privateKeyJwt.privateKey is required for private_key_jwt'
219
+ });
194
220
  }
195
221
  body.set('client_id', options.clientId);
196
222
  body.set('client_assertion', await createPrivateKeyJwtAssertion({
@@ -201,7 +227,9 @@ export function createOIDC(options) {
201
227
  body.set('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
202
228
  return { headers, body };
203
229
  default:
204
- throw error(500, { message: `Unsupported client authentication method '${clientAuthMethod}'` });
230
+ throw error(500, {
231
+ message: `Unsupported client authentication method '${clientAuthMethod}'`
232
+ });
205
233
  }
206
234
  }
207
235
  async function exchangeCode(params) {
@@ -237,9 +265,17 @@ export function createOIDC(options) {
237
265
  clockSkew: clockSkewSeconds
238
266
  });
239
267
  validateIdTokenClaims(claims, nonce);
240
- return options.transformClaims
241
- ? await options.transformClaims(claims)
242
- : claims;
268
+ return claims;
269
+ }
270
+ async function resolveIdentity(idTokenClaims, userInfo, reason) {
271
+ if (options.resolveIdentity) {
272
+ return options.resolveIdentity({ idTokenClaims, userInfo, reason });
273
+ }
274
+ return {
275
+ ...idTokenClaims,
276
+ ...userInfo,
277
+ groups: userInfo?.groups ?? idTokenClaims.groups
278
+ };
243
279
  }
244
280
  async function validateBackChannelLogoutToken(logoutToken) {
245
281
  const metadata = await getMetadata();
@@ -276,7 +312,7 @@ export function createOIDC(options) {
276
312
  }
277
313
  return backChannelLogoutStore.isRevoked(session);
278
314
  }
279
- async function maybeRefreshSession(cookies, persisted) {
315
+ async function maybeRefreshSession(cookies, persisted, event) {
280
316
  const session = persisted?.session ?? null;
281
317
  if (!session) {
282
318
  return null;
@@ -312,42 +348,37 @@ export function createOIDC(options) {
312
348
  refreshExpiresAt: session.tokens.refreshExpiresAt
313
349
  });
314
350
  const tokenResponse = await refreshTokens(session.tokens.refreshToken);
315
- const claims = tokenResponse.id_token
351
+ const idTokenClaims = tokenResponse.id_token
316
352
  ? await validateIdToken(tokenResponse.id_token, session.nonce)
317
- : session.claims;
318
- const rawUser = options.fetchUserInfo !== false ? await fetchUserInfo(tokenResponse.access_token) : session.user;
319
- if (claims) {
320
- validateUserInfoSubject(claims, rawUser);
321
- }
322
- const user = options.transformUser
323
- ? await options.transformUser(rawUser, { claims })
324
- : rawUser;
353
+ : session.idTokenClaims;
354
+ const userInfo = options.fetchUserInfo !== false ? await fetchUserInfo(tokenResponse.access_token) : session.userInfo;
355
+ validateUserInfoSubject(idTokenClaims, userInfo);
356
+ const identity = await resolveIdentity(idTokenClaims, userInfo, 'refresh');
325
357
  const nextSession = {
326
358
  ...session,
327
- sub: claims?.sub ?? session.sub,
328
- sid: claims?.sid ?? session.sid,
329
- groups: collectGroups(claims, user, session.user, session.claims),
330
- claims,
331
- user,
359
+ sub: idTokenClaims.sub,
360
+ sid: idTokenClaims.sid ?? session.sid,
361
+ groups: collectGroups(idTokenClaims, userInfo, identity),
362
+ idTokenClaims,
363
+ userInfo,
364
+ identity,
332
365
  sessionState: tokenResponse.session_state ?? session.sessionState,
333
366
  tokens: normalizeTokens(tokenResponse, defaultScope, session.tokens),
334
367
  refreshedAt: Math.floor(Date.now() / 1000)
335
368
  };
336
- const finalSession = options.transformSession
337
- ? await options.transformSession(nextSession, {
338
- tokenResponse,
339
- claims,
340
- user,
341
- isRefresh: true
342
- })
343
- : nextSession;
344
- await writePersistedSession(cookies, finalSession, persisted?.id);
369
+ await options.beforeSessionPersist?.({
370
+ session: nextSession,
371
+ reason: 'refresh',
372
+ event,
373
+ tokenResponse
374
+ });
375
+ await writePersistedSession(cookies, nextSession, persisted?.id);
345
376
  log.debug('OIDC session tokens refreshed', {
346
- expiresAt: finalSession.tokens.expiresAt,
347
- refreshExpiresAt: finalSession.tokens.refreshExpiresAt,
348
- hasRefreshToken: Boolean(finalSession.tokens.refreshToken)
377
+ expiresAt: nextSession.tokens.expiresAt,
378
+ refreshExpiresAt: nextSession.tokens.refreshExpiresAt,
379
+ hasRefreshToken: Boolean(nextSession.tokens.refreshToken)
349
380
  });
350
- return finalSession;
381
+ return nextSession;
351
382
  }
352
383
  catch (err) {
353
384
  log.error('Token refresh failed — clearing session', err);
@@ -356,11 +387,7 @@ export function createOIDC(options) {
356
387
  }
357
388
  }
358
389
  async function getSession(event) {
359
- const session = await maybeRefreshSession(event.cookies, await readPersistedSession(event.cookies));
360
- if (!session || !options.enrichSession) {
361
- return session;
362
- }
363
- return options.enrichSession(session, { event: event });
390
+ return maybeRefreshSession(event.cookies, await readPersistedSession(event.cookies), 'url' in event && 'request' in event ? event : undefined);
364
391
  }
365
392
  async function signIn(event, loginOptions = {}) {
366
393
  const metadata = await getMetadata();
@@ -416,44 +443,42 @@ export function createOIDC(options) {
416
443
  codeVerifier: stateCookie.codeVerifier
417
444
  });
418
445
  if (!tokenResponse.id_token) {
419
- throw error(401, { message: 'OIDC callback response must include an id_token' });
446
+ throw error(401, {
447
+ message: 'OIDC callback response must include an id_token'
448
+ });
420
449
  }
421
- const claims = await validateIdToken(tokenResponse.id_token, stateCookie.nonce);
422
- const rawUser = options.fetchUserInfo === false
450
+ const idTokenClaims = await validateIdToken(tokenResponse.id_token, stateCookie.nonce);
451
+ const userInfo = options.fetchUserInfo === false
423
452
  ? undefined
424
453
  : await fetchUserInfo(tokenResponse.access_token).catch(() => undefined);
425
- validateUserInfoSubject(claims, rawUser);
426
- const user = options.transformUser
427
- ? await options.transformUser(rawUser, { claims })
428
- : rawUser;
454
+ validateUserInfoSubject(idTokenClaims, userInfo);
455
+ const identity = await resolveIdentity(idTokenClaims, userInfo, 'login');
429
456
  const metadata = await getMetadata();
430
457
  const now = Math.floor(Date.now() / 1000);
431
458
  const session = {
432
459
  issuer: metadata.issuer,
433
460
  clientId: options.clientId,
434
461
  nonce: stateCookie.nonce,
435
- sub: claims?.sub,
436
- sid: claims?.sid,
462
+ sub: idTokenClaims.sub,
463
+ sid: idTokenClaims.sid,
437
464
  sessionState: tokenResponse.session_state ?? event.url.searchParams.get('session_state') ?? undefined,
438
- groups: collectGroups(claims, user),
439
- user,
440
- claims,
465
+ groups: collectGroups(idTokenClaims, userInfo, identity),
466
+ idTokenClaims,
467
+ userInfo,
468
+ identity,
441
469
  tokens: normalizeTokens(tokenResponse, defaultScope),
442
470
  createdAt: now,
443
471
  refreshedAt: now
444
472
  };
445
- const finalSession = options.transformSession
446
- ? await options.transformSession(session, {
447
- event: event,
448
- tokenResponse,
449
- claims,
450
- user,
451
- isRefresh: false
452
- })
453
- : session;
454
- await writePersistedSession(event.cookies, finalSession);
473
+ await options.beforeSessionPersist?.({
474
+ session,
475
+ reason: 'login',
476
+ event: event,
477
+ tokenResponse
478
+ });
479
+ await writePersistedSession(event.cookies, session);
455
480
  return {
456
- session: finalSession,
481
+ session,
457
482
  returnTo: stateCookie.returnTo
458
483
  };
459
484
  }
@@ -483,7 +508,9 @@ export function createOIDC(options) {
483
508
  async function handleBackChannelLogout(event) {
484
509
  const metadata = await getMetadata();
485
510
  if (!metadata.backchannel_logout_supported) {
486
- throw error(400, { message: 'Provider does not advertise back-channel logout support' });
511
+ throw error(400, {
512
+ message: 'Provider does not advertise back-channel logout support'
513
+ });
487
514
  }
488
515
  if (!backChannelLogoutStore) {
489
516
  throw error(500, {
@@ -514,29 +541,27 @@ export function createOIDC(options) {
514
541
  throw redirect(302, `${absoluteUrl(event, loginPath)}?returnTo=${encodeURIComponent(internalRedirectPath(event, returnTo ?? `${event.url.pathname}${event.url.search}`, '/'))}`);
515
542
  }
516
543
  const handle = async ({ event, resolve }) => {
517
- const { oidc } = await hook(event);
544
+ const oidc = await createRequestContext(event);
518
545
  event.locals.oidc = oidc;
519
546
  return resolve(event);
520
547
  };
521
- async function hook(event) {
548
+ async function createRequestContext(event) {
522
549
  const session = await getSession(event);
523
- const locals = {
524
- oidc: {
525
- isAuthenticated: Boolean(session),
526
- session,
527
- user: session?.user,
528
- claims: session?.claims,
529
- requireAuth: async () => {
530
- if (!session) {
531
- throw error(401, { message: 'Authentication required' });
532
- }
533
- return session;
534
- },
535
- clearSession: async () => clearPersistedSession(event.cookies)
536
- }
550
+ const data = session && options.loadRequestData ? await options.loadRequestData({ session, event }) : null;
551
+ const context = {
552
+ isAuthenticated: Boolean(session),
553
+ session,
554
+ identity: session?.identity,
555
+ data,
556
+ requireAuth: async () => {
557
+ if (!session) {
558
+ throw error(401, { message: 'Authentication required' });
559
+ }
560
+ return session;
561
+ },
562
+ clearSession: async () => clearPersistedSession(event.cookies)
537
563
  };
538
- event.locals.oidc = locals.oidc;
539
- return locals;
564
+ return context;
540
565
  }
541
566
  function loginHandler(defaults = {}) {
542
567
  return async (event) => {
@@ -573,7 +598,11 @@ export function createOIDC(options) {
573
598
  form?.get('clearSessionOnly')?.toString() === '1' ||
574
599
  form?.get('clearSessionOnly')?.toString() === 'true' ||
575
600
  defaults.clearSessionOnly;
576
- return signOut(event, { ...defaults, postLogoutRedirectUri, clearSessionOnly });
601
+ return signOut(event, {
602
+ ...defaults,
603
+ postLogoutRedirectUri,
604
+ clearSessionOnly
605
+ });
577
606
  };
578
607
  }
579
608
  function backChannelLogoutHandler() {
@@ -590,7 +619,8 @@ export function createOIDC(options) {
590
619
  const form = await event.request.formData();
591
620
  const postLogoutRedirectUri = (form.get('postLogoutRedirectUri')?.toString() ||
592
621
  actionOptions.defaultPostLogoutRedirectUri ||
593
- undefined) ?? undefined;
622
+ undefined) ??
623
+ undefined;
594
624
  const clearSessionOnly = form.get('clearSessionOnly')?.toString() === '1' ||
595
625
  form.get('clearSessionOnly')?.toString() === 'true';
596
626
  return signOut(event, { postLogoutRedirectUri, clearSessionOnly });
@@ -618,20 +648,34 @@ export function createOIDC(options) {
618
648
  backChannelLogoutSessionSupported: Boolean(metadata.backchannel_logout_session_supported)
619
649
  };
620
650
  }
621
- function createPublicSession(session, depends) {
651
+ function projectPublicSession(context, depends) {
622
652
  depends?.(OIDC_SESSION_REVALIDATION_DEPENDENCY);
623
- const publicSession = toPublicSession(session);
624
- return publicSession && depends
625
- ? { ...publicSession, revalidationDependency: OIDC_SESSION_REVALIDATION_DEPENDENCY }
653
+ if (!context?.session)
654
+ return null;
655
+ const base = toPublicSession(context.session);
656
+ if (!base)
657
+ return null;
658
+ const publicSession = options.createPublicSession
659
+ ? options.createPublicSession({
660
+ session: context.session,
661
+ data: context.data,
662
+ base
663
+ })
664
+ : base;
665
+ return depends
666
+ ? {
667
+ ...publicSession,
668
+ revalidationDependency: OIDC_SESSION_REVALIDATION_DEPENDENCY
669
+ }
626
670
  : publicSession;
627
671
  }
628
672
  return {
629
673
  handle,
630
- hook,
674
+ createRequestContext,
631
675
  getMetadata,
632
676
  getSession,
633
- getPublicSession: async (event) => createPublicSession(await getSession(event), event.depends),
634
- toPublicSession: createPublicSession,
677
+ getPublicSession: async (event) => projectPublicSession(await createRequestContext(event), event.depends),
678
+ toPublicSession: projectPublicSession,
635
679
  getSessionManagementConfig,
636
680
  login: signIn,
637
681
  logout: signOut,
@@ -1,3 +1,3 @@
1
- import type { OIDCBackChannelLogoutStore, OIDCSession, OIDCSessionStore, OIDCUserClaims } from './types.js';
2
- export declare function createInMemoryBackChannelLogoutStore<TClaims extends OIDCUserClaims = OIDCUserClaims, TSession extends OIDCSession<TClaims> = OIDCSession<TClaims>>(): OIDCBackChannelLogoutStore<TClaims, TSession>;
3
- export declare function createInMemorySessionStore<TClaims extends OIDCUserClaims = OIDCUserClaims, TSession extends OIDCSession<TClaims> = OIDCSession<TClaims>>(): OIDCSessionStore<TClaims, TSession>;
1
+ import type { OIDCBackChannelLogoutStore, OIDCSessionStore, OIDCUserClaims } from './types.js';
2
+ export declare function createInMemoryBackChannelLogoutStore<TIdentity extends OIDCUserClaims = OIDCUserClaims>(): OIDCBackChannelLogoutStore<TIdentity>;
3
+ export declare function createInMemorySessionStore<TIdentity extends OIDCUserClaims = OIDCUserClaims>(): OIDCSessionStore<TIdentity>;