@sourceregistry/sveltekit-oidc 1.6.3 → 1.6.5

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
@@ -1,11 +1,18 @@
1
1
  # sveltekit-oidc
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@sourceregistry/sveltekit-oidc.svg)](https://www.npmjs.com/package/@sourceregistry/sveltekit-oidc)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@sourceregistry/sveltekit-oidc.svg)](https://www.npmjs.com/package/@sourceregistry/sveltekit-oidc)
5
+ [![license](https://img.shields.io/npm/l/@sourceregistry/sveltekit-oidc.svg)](LICENSE)
6
+ [![types](https://img.shields.io/npm/types/@sourceregistry/sveltekit-oidc.svg)](https://www.npmjs.com/package/@sourceregistry/sveltekit-oidc)
7
+ [![Svelte](https://img.shields.io/badge/Svelte-5-ff3e00.svg)](https://svelte.dev/)
8
+ [![publint](https://img.shields.io/badge/publint-passing-brightgreen.svg)](https://publint.dev/@sourceregistry/sveltekit-oidc)
9
+
3
10
  OIDC authentication helpers for SvelteKit with:
4
11
 
5
12
  - server-side login, callback, logout, and session refresh flows
6
13
  - token endpoint auth support for `none`, `client_secret_basic`, `client_secret_post`, `client_secret_jwt`, and `private_key_jwt`
7
14
  - back-channel logout support through a revocation store
8
- - signed cookie-backed sessions and PKCE/state protection
15
+ - encrypted cookie-backed sessions and PKCE/state protection
9
16
  - a `handle` hook for attaching auth state to `event.locals`
10
17
  - a small `OIDCContext` provider for client-side auth state and session lifecycle handling
11
18
 
@@ -64,7 +71,6 @@ export const GET = oidc.callbackHandler({
64
71
  // src/routes/auth/logout/+server.ts
65
72
  import { oidc } from '$lib/server/auth';
66
73
 
67
- export const GET = oidc.logoutHandler();
68
74
  export const POST = oidc.logoutHandler();
69
75
  ```
70
76
 
@@ -290,6 +296,7 @@ Set these environment variables to enable it:
290
296
  ## Notes
291
297
 
292
298
  - `cookieSecret` should be a strong random secret and must stay stable across instances.
299
+ - Built-in `returnTo` and logout redirect values are restricted to same-origin paths. Use your own callback `onsuccess` logic if you need custom redirect policy.
293
300
  - `clockSkewSeconds` defaults to `30` and tolerates small clock drift between your app and the identity provider.
294
301
  - `createInMemoryBackChannelLogoutStore()` is suitable for local development or single-instance deployments. Use Redis, SQL, or another shared store for production.
295
302
  - The library validates `id_token` and `logout_token` values through `@sourceregistry/node-jwt` and provider JWKS metadata.
@@ -4,7 +4,7 @@ import { decode, fromWeb, verify } from '@sourceregistry/node-jwt/promises';
4
4
  import { createOIDCCookieStore } from './cookies.js';
5
5
  import { asAuthorizationHeader, createClientSecretJwtAssertion, createPrivateKeyJwtAssertion, fetchJson } from './jwt.js';
6
6
  import { normalizeTokens, shouldRefresh } from './session.js';
7
- import { absoluteUrl, base64UrlEncode, buildCookieOptions, collectGroups, createPKCEPair, normalizeIssuer, normalizeScope, parseProviderError, toPublicSession } from './utils.js';
7
+ import { absoluteUrl, base64UrlEncode, buildCookieOptions, collectGroups, createPKCEPair, internalRedirectPath, normalizeIssuer, normalizeScope, parseProviderError, toPublicSession } from './utils.js';
8
8
  export { createInMemoryBackChannelLogoutStore, createInMemorySessionStore } from './store.js';
9
9
  import { createInMemoryBackChannelLogoutStore, createInMemorySessionStore } from './store.js';
10
10
  function buildLogger(logger) {
@@ -233,6 +233,9 @@ export function createOIDC(options) {
233
233
  if (transformedClaims.nonce !== nonce) {
234
234
  throw error(401, { message: 'Invalid id_token nonce' });
235
235
  }
236
+ if (!transformedClaims.sub) {
237
+ throw error(401, { message: 'id_token subject is required' });
238
+ }
236
239
  return transformedClaims;
237
240
  }
238
241
  async function validateBackChannelLogoutToken(logoutToken) {
@@ -327,7 +330,7 @@ export function createOIDC(options) {
327
330
  const pkce = createPKCEPair();
328
331
  const state = base64UrlEncode(randomBytes(24));
329
332
  const nonce = base64UrlEncode(randomBytes(24));
330
- const returnTo = loginOptions.returnTo ?? options.defaultLoginRedirect ?? '/';
333
+ const returnTo = internalRedirectPath(event, loginOptions.returnTo ?? options.defaultLoginRedirect, '/');
331
334
  cookieStore.writeState(event.cookies, {
332
335
  state,
333
336
  nonce,
@@ -375,9 +378,10 @@ export function createOIDC(options) {
375
378
  redirectUri: absoluteUrl(event, redirectPath),
376
379
  codeVerifier: stateCookie.codeVerifier
377
380
  });
378
- const claims = tokenResponse.id_token
379
- ? await validateIdToken(tokenResponse.id_token, stateCookie.nonce)
380
- : undefined;
381
+ if (!tokenResponse.id_token) {
382
+ throw error(401, { message: 'OIDC callback response must include an id_token' });
383
+ }
384
+ const claims = await validateIdToken(tokenResponse.id_token, stateCookie.nonce);
381
385
  const rawUser = options.fetchUserInfo === false
382
386
  ? undefined
383
387
  : await fetchUserInfo(tokenResponse.access_token).catch(() => undefined);
@@ -421,20 +425,15 @@ export function createOIDC(options) {
421
425
  const session = persisted?.session ?? null;
422
426
  cookieStore.clearState(event.cookies);
423
427
  await clearPersistedSession(event.cookies, persisted?.id);
428
+ const postLogoutRedirectPath = internalRedirectPath(event, logoutOptions.postLogoutRedirectUri ?? options.defaultLogoutRedirect ?? options.postLogoutRedirectUri, '/');
424
429
  if (logoutOptions.clearSessionOnly || !metadata.end_session_endpoint) {
425
- throw redirect(302, logoutOptions.postLogoutRedirectUri ??
426
- options.defaultLogoutRedirect ??
427
- options.postLogoutRedirectUri ??
428
- '/');
430
+ throw redirect(302, postLogoutRedirectPath);
429
431
  }
430
432
  const url = new URL(metadata.end_session_endpoint);
431
433
  if (session?.tokens.idToken) {
432
434
  url.searchParams.set('id_token_hint', session.tokens.idToken);
433
435
  }
434
- url.searchParams.set('post_logout_redirect_uri', absoluteUrl(event, logoutOptions.postLogoutRedirectUri ??
435
- options.postLogoutRedirectUri ??
436
- options.defaultLogoutRedirect ??
437
- '/'));
436
+ url.searchParams.set('post_logout_redirect_uri', absoluteUrl(event, postLogoutRedirectPath));
438
437
  if (logoutOptions.state) {
439
438
  url.searchParams.set('state', logoutOptions.state);
440
439
  }
@@ -471,7 +470,7 @@ export function createOIDC(options) {
471
470
  if (session) {
472
471
  return session;
473
472
  }
474
- throw redirect(302, `${absoluteUrl(event, loginPath)}?returnTo=${encodeURIComponent(returnTo ?? `${event.url.pathname}${event.url.search}`)}`);
473
+ throw redirect(302, `${absoluteUrl(event, loginPath)}?returnTo=${encodeURIComponent(internalRedirectPath(event, returnTo ?? `${event.url.pathname}${event.url.search}`, '/'))}`);
475
474
  }
476
475
  const handle = async ({ event, resolve }) => {
477
476
  const { oidc } = await hook(event);
@@ -510,7 +509,7 @@ export function createOIDC(options) {
510
509
  if (response) {
511
510
  return response;
512
511
  }
513
- throw redirect(302, handlerOptions.redirectTo ?? result.returnTo);
512
+ throw redirect(302, internalRedirectPath(event, handlerOptions.redirectTo ?? result.returnTo, '/'));
514
513
  }
515
514
  catch (err) {
516
515
  const response = await handlerOptions.onfailure?.(event, err);
@@ -523,6 +522,9 @@ export function createOIDC(options) {
523
522
  }
524
523
  function logoutHandler(defaults = {}) {
525
524
  return async (event) => {
525
+ if (event.request.method !== 'POST') {
526
+ throw error(405, { message: 'Logout requires POST' });
527
+ }
526
528
  const postLogoutRedirectUri = event.url.searchParams.get('postLogoutRedirectUri') ?? defaults.postLogoutRedirectUri;
527
529
  const clearSessionOnly = event.url.searchParams.get('clearSessionOnly') === '1' ||
528
530
  event.url.searchParams.get('clearSessionOnly') === 'true' ||
@@ -263,7 +263,10 @@ export type OIDCInstance<TClaims extends OIDCUserClaims = OIDCUserClaims, TSessi
263
263
  login: Action;
264
264
  logout: Action;
265
265
  }>;
266
- requireAuth: (event: RequestEvent, returnTo?: string) => Promise<TSession>;
266
+ requireAuth: (event: {
267
+ cookies: Cookies;
268
+ url: URL;
269
+ }, returnTo?: string) => Promise<TSession>;
267
270
  clearSession: (cookies: Cookies) => Promise<void>;
268
271
  };
269
272
  export type OIDCInferClaims<T> = T extends OIDCInstance<infer TClaims> ? TClaims : OIDCUserClaims;
@@ -1,4 +1,3 @@
1
- import { type RequestEvent } from '@sveltejs/kit';
2
1
  import type { CookieOptions, OIDCPublicSession, OIDCSession, OIDCUserClaims } from './types.js';
3
2
  export declare function base64UrlEncode(value: string | Uint8Array): string;
4
3
  export declare function createPKCEPair(length?: number): {
@@ -14,6 +13,13 @@ export declare function verifySignedValue(value: string, secret: string): string
14
13
  export declare function serializeSignedCookie(payload: unknown, secret: string): string;
15
14
  export declare function parseSignedCookie<T>(value: string | undefined, secret: string): T | null;
16
15
  export declare function buildCookieOptions(options?: Partial<CookieOptions>): CookieOptions;
17
- export declare function parseProviderError(event: RequestEvent): null;
18
- export declare function absoluteUrl(event: RequestEvent, pathOrUrl: string): string;
16
+ export declare function parseProviderError(event: {
17
+ url: URL;
18
+ }): null;
19
+ export declare function absoluteUrl(event: {
20
+ url: URL;
21
+ }, pathOrUrl: string): string;
22
+ export declare function internalRedirectPath(event: {
23
+ url: URL;
24
+ }, pathOrUrl: string | undefined, fallback?: string): string;
19
25
  export declare function toPublicSession<TClaims extends OIDCUserClaims = OIDCUserClaims>(session: OIDCSession<TClaims> | null): OIDCPublicSession<TClaims> | null;
@@ -1,5 +1,5 @@
1
1
  import { error } from '@sveltejs/kit';
2
- import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
2
+ import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
3
3
  export function base64UrlEncode(value) {
4
4
  const buffer = typeof value === 'string' ? Buffer.from(value, 'utf8') : Buffer.from(value);
5
5
  return buffer.toString('base64url');
@@ -51,17 +51,34 @@ export function verifySignedValue(value, secret) {
51
51
  }
52
52
  return payload;
53
53
  }
54
+ function cookieEncryptionKey(secret) {
55
+ return createHash('sha256').update(`sveltekit-oidc-cookie:${secret}`).digest();
56
+ }
54
57
  export function serializeSignedCookie(payload, secret) {
55
- return createSignedValue(base64UrlEncode(JSON.stringify(payload)), secret);
58
+ const iv = randomBytes(12);
59
+ const cipher = createCipheriv('aes-256-gcm', cookieEncryptionKey(secret), iv);
60
+ const ciphertext = Buffer.concat([
61
+ cipher.update(JSON.stringify(payload), 'utf8'),
62
+ cipher.final()
63
+ ]);
64
+ const tag = cipher.getAuthTag();
65
+ return `v2.${iv.toString('base64url')}.${tag.toString('base64url')}.${ciphertext.toString('base64url')}`;
56
66
  }
57
67
  export function parseSignedCookie(value, secret) {
58
68
  if (!value)
59
69
  return null;
60
- const payload = verifySignedValue(value, secret);
61
- if (!payload)
70
+ const parts = value.split('.');
71
+ if (parts.length !== 4 || parts[0] !== 'v2')
62
72
  return null;
63
73
  try {
64
- return JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
74
+ const [, iv, tag, ciphertext] = parts;
75
+ const decipher = createDecipheriv('aes-256-gcm', cookieEncryptionKey(secret), Buffer.from(iv, 'base64url'));
76
+ decipher.setAuthTag(Buffer.from(tag, 'base64url'));
77
+ const plaintext = Buffer.concat([
78
+ decipher.update(Buffer.from(ciphertext, 'base64url')),
79
+ decipher.final()
80
+ ]);
81
+ return JSON.parse(plaintext.toString('utf8'));
65
82
  }
66
83
  catch {
67
84
  return null;
@@ -89,6 +106,19 @@ export function absoluteUrl(event, pathOrUrl) {
89
106
  return pathOrUrl;
90
107
  return new URL(pathOrUrl, event.url).toString();
91
108
  }
109
+ export function internalRedirectPath(event, pathOrUrl, fallback = '/') {
110
+ if (!pathOrUrl)
111
+ return fallback;
112
+ try {
113
+ const url = new URL(pathOrUrl, event.url);
114
+ if (url.origin !== event.url.origin)
115
+ return fallback;
116
+ return `${url.pathname}${url.search}${url.hash}`;
117
+ }
118
+ catch {
119
+ return fallback;
120
+ }
121
+ }
92
122
  export function toPublicSession(session) {
93
123
  if (!session)
94
124
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sourceregistry/sveltekit-oidc",
3
- "version": "1.6.3",
3
+ "version": "1.6.5",
4
4
  "description": "OIDC authentication helpers for SvelteKit applications",
5
5
  "license": "Apache-2.0",
6
6
  "scripts": {
@@ -41,7 +41,8 @@
41
41
  "exports": {
42
42
  ".": {
43
43
  "types": "./dist/index.d.ts",
44
- "svelte": "./dist/index.js"
44
+ "svelte": "./dist/index.js",
45
+ "default": "./dist/index.js"
45
46
  },
46
47
  "./client": {
47
48
  "types": "./dist/client/index.d.ts",