@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 +9 -2
- package/dist/server/index.js +17 -15
- package/dist/server/types.d.ts +4 -1
- package/dist/server/utils.d.ts +9 -3
- package/dist/server/utils.js +35 -5
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
# sveltekit-oidc
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@sourceregistry/sveltekit-oidc)
|
|
4
|
+
[](https://www.npmjs.com/package/@sourceregistry/sveltekit-oidc)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://www.npmjs.com/package/@sourceregistry/sveltekit-oidc)
|
|
7
|
+
[](https://svelte.dev/)
|
|
8
|
+
[](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
|
-
-
|
|
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.
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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,
|
|
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,
|
|
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' ||
|
package/dist/server/types.d.ts
CHANGED
|
@@ -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:
|
|
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;
|
package/dist/server/utils.d.ts
CHANGED
|
@@ -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:
|
|
18
|
-
|
|
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;
|
package/dist/server/utils.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
61
|
-
if (
|
|
70
|
+
const parts = value.split('.');
|
|
71
|
+
if (parts.length !== 4 || parts[0] !== 'v2')
|
|
62
72
|
return null;
|
|
63
73
|
try {
|
|
64
|
-
|
|
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
|
+
"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",
|