@sourceregistry/sveltekit-oidc 1.0.3

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.
@@ -0,0 +1,26 @@
1
+ import { parseSignedCookie, serializeSignedCookie } from './utils.js';
2
+ export function createOIDCCookieStore(cookieSecret, sessionCookieName, stateCookieName, cookieOptions) {
3
+ return {
4
+ readSession(cookies) {
5
+ return parseSignedCookie(cookies.get(sessionCookieName), cookieSecret);
6
+ },
7
+ writeSession(cookies, session) {
8
+ cookies.set(sessionCookieName, serializeSignedCookie(session, cookieSecret), cookieOptions);
9
+ },
10
+ clearSession(cookies) {
11
+ cookies.delete(sessionCookieName, cookieOptions);
12
+ },
13
+ readState(cookies) {
14
+ return parseSignedCookie(cookies.get(stateCookieName), cookieSecret);
15
+ },
16
+ writeState(cookies, state) {
17
+ cookies.set(stateCookieName, serializeSignedCookie(state, cookieSecret), {
18
+ ...cookieOptions,
19
+ maxAge: 60 * 10
20
+ });
21
+ },
22
+ clearState(cookies) {
23
+ cookies.delete(stateCookieName, cookieOptions);
24
+ }
25
+ };
26
+ }
@@ -0,0 +1,5 @@
1
+ import type { OIDCOptions, OIDCInstance } from './types.js';
2
+ export type * from './types.js';
3
+ export { createInMemoryBackChannelLogoutStore } from './store.js';
4
+ export declare function createOIDC(options: OIDCOptions): OIDCInstance;
5
+ export declare const OpenIDConnect: typeof createOIDC;
@@ -0,0 +1,505 @@
1
+ import { error, redirect } from '@sveltejs/kit';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { decode, fromWeb, verify } from '@sourceregistry/node-jwt/promises';
4
+ import { createOIDCCookieStore } from './cookies.js';
5
+ import { asAuthorizationHeader, createClientSecretJwtAssertion, createPrivateKeyJwtAssertion, fetchJson } from './jwt.js';
6
+ import { normalizeTokens, shouldRefresh } from './session.js';
7
+ import { absoluteUrl, base64UrlEncode, buildCookieOptions, collectGroups, createPKCEPair, normalizeIssuer, normalizeScope, parseProviderError, toPublicSession } from './utils.js';
8
+ export { createInMemoryBackChannelLogoutStore } from './store.js';
9
+ export function createOIDC(options) {
10
+ const cookieOptions = buildCookieOptions(options.cookieOptions);
11
+ const refreshToleranceSeconds = options.refreshToleranceSeconds ?? 30;
12
+ const sessionCookieName = options.sessionCookieName ?? 'oidc_session';
13
+ const stateCookieName = options.stateCookieName ?? 'oidc_auth_state';
14
+ const defaultScope = normalizeScope(options.scope);
15
+ const loginPath = options.loginPath ?? '/auth/login';
16
+ const redirectPath = options.redirectPath ?? '/auth/callback';
17
+ const clientAuthMethod = options.clientAuthMethod ?? (options.clientSecret ? 'client_secret_basic' : 'none');
18
+ const cookieStore = createOIDCCookieStore(options.cookieSecret, sessionCookieName, stateCookieName, cookieOptions);
19
+ let metadataPromise;
20
+ let jwksPromise;
21
+ async function getMetadata() {
22
+ if (!metadataPromise) {
23
+ metadataPromise = (async () => {
24
+ if (options.endpoints?.authorization_endpoint && options.endpoints?.token_endpoint) {
25
+ return {
26
+ issuer: normalizeIssuer(options.issuer ?? options.endpoints.issuer ?? ''),
27
+ jwks_uri: options.endpoints.jwks_uri ?? '',
28
+ ...options.endpoints
29
+ };
30
+ }
31
+ const issuer = options.issuer ? normalizeIssuer(options.issuer) : undefined;
32
+ const discoveryUrl = options.discoveryUrl ?? (issuer ? `${issuer}/.well-known/openid-configuration` : undefined);
33
+ if (!discoveryUrl) {
34
+ throw error(500, { message: 'OIDC issuer or discoveryUrl must be configured' });
35
+ }
36
+ const document = await fetchJson(discoveryUrl);
37
+ return {
38
+ ...document,
39
+ ...options.endpoints,
40
+ issuer: normalizeIssuer(document.issuer)
41
+ };
42
+ })();
43
+ }
44
+ return metadataPromise;
45
+ }
46
+ async function getJwks() {
47
+ if (!jwksPromise) {
48
+ jwksPromise = getMetadata().then((metadata) => {
49
+ if (!metadata.jwks_uri) {
50
+ throw error(500, { message: 'OIDC jwks_uri is required to validate id_token values' });
51
+ }
52
+ return fromWeb(metadata.jwks_uri);
53
+ });
54
+ }
55
+ return jwksPromise;
56
+ }
57
+ async function verifyJwtWithJwks(token, verifyOptions) {
58
+ let decoded;
59
+ try {
60
+ decoded = await decode(token);
61
+ }
62
+ catch {
63
+ throw error(400, { message: 'Invalid JWT format' });
64
+ }
65
+ const jwks = await getJwks();
66
+ const key = decoded.header.kid ? await jwks.key(decoded.header.kid) : (await jwks.list())[0];
67
+ if (!key) {
68
+ throw error(401, { message: 'Unable to resolve a signing key from JWKS' });
69
+ }
70
+ try {
71
+ const result = await verify(token, key.toKeyObject(), verifyOptions);
72
+ return result.payload;
73
+ }
74
+ catch (err) {
75
+ throw error(401, {
76
+ message: typeof err === 'object' && err && 'reason' in err
77
+ ? String(err.reason)
78
+ : 'JWT verification failed'
79
+ });
80
+ }
81
+ }
82
+ async function buildTokenRequestAuth(tokenEndpoint) {
83
+ const headers = {
84
+ 'content-type': 'application/x-www-form-urlencoded'
85
+ };
86
+ const body = new URLSearchParams();
87
+ switch (clientAuthMethod) {
88
+ case 'none':
89
+ body.set('client_id', options.clientId);
90
+ return { headers, body };
91
+ case 'client_secret_basic':
92
+ if (!options.clientSecret) {
93
+ throw error(500, { message: 'clientSecret is required for client_secret_basic' });
94
+ }
95
+ headers.authorization = asAuthorizationHeader(options.clientId, options.clientSecret);
96
+ return { headers, body };
97
+ case 'client_secret_post':
98
+ if (!options.clientSecret) {
99
+ throw error(500, { message: 'clientSecret is required for client_secret_post' });
100
+ }
101
+ body.set('client_id', options.clientId);
102
+ body.set('client_secret', options.clientSecret);
103
+ return { headers, body };
104
+ case 'client_secret_jwt':
105
+ body.set('client_id', options.clientId);
106
+ body.set('client_assertion', await createClientSecretJwtAssertion({
107
+ tokenEndpoint,
108
+ clientId: options.clientId,
109
+ clientSecret: options.clientSecret,
110
+ ...options.clientSecretJwt
111
+ }));
112
+ body.set('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
113
+ return { headers, body };
114
+ case 'private_key_jwt':
115
+ if (!options.privateKeyJwt?.privateKey) {
116
+ throw error(500, { message: 'privateKeyJwt.privateKey is required for private_key_jwt' });
117
+ }
118
+ body.set('client_id', options.clientId);
119
+ body.set('client_assertion', await createPrivateKeyJwtAssertion({
120
+ tokenEndpoint,
121
+ clientId: options.clientId,
122
+ ...options.privateKeyJwt
123
+ }));
124
+ body.set('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
125
+ return { headers, body };
126
+ default:
127
+ throw error(500, { message: `Unsupported client authentication method '${clientAuthMethod}'` });
128
+ }
129
+ }
130
+ async function exchangeCode(params) {
131
+ const metadata = await getMetadata();
132
+ const auth = await buildTokenRequestAuth(metadata.token_endpoint);
133
+ auth.body.set('grant_type', 'authorization_code');
134
+ auth.body.set('code', params.code);
135
+ auth.body.set('redirect_uri', params.redirectUri);
136
+ auth.body.set('code_verifier', params.codeVerifier);
137
+ return fetchJson(metadata.token_endpoint, {
138
+ method: 'POST',
139
+ headers: auth.headers,
140
+ body: auth.body
141
+ });
142
+ }
143
+ async function refreshTokens(refreshToken) {
144
+ const metadata = await getMetadata();
145
+ const auth = await buildTokenRequestAuth(metadata.token_endpoint);
146
+ auth.body.set('grant_type', 'refresh_token');
147
+ auth.body.set('refresh_token', refreshToken);
148
+ return fetchJson(metadata.token_endpoint, {
149
+ method: 'POST',
150
+ headers: auth.headers,
151
+ body: auth.body
152
+ });
153
+ }
154
+ async function validateIdToken(idToken, nonce) {
155
+ const metadata = await getMetadata();
156
+ const claims = await verifyJwtWithJwks(idToken, {
157
+ issuer: metadata.issuer,
158
+ audience: options.clientId,
159
+ algorithms: metadata.id_token_signing_alg_values_supported
160
+ });
161
+ const transformedClaims = options.transformClaims ? await options.transformClaims(claims) : claims;
162
+ if (transformedClaims.nonce !== nonce) {
163
+ throw error(401, { message: 'Invalid id_token nonce' });
164
+ }
165
+ return transformedClaims;
166
+ }
167
+ async function validateBackChannelLogoutToken(logoutToken) {
168
+ const metadata = await getMetadata();
169
+ const claims = await verifyJwtWithJwks(logoutToken, {
170
+ issuer: metadata.issuer,
171
+ audience: options.clientId
172
+ });
173
+ if (!claims.events?.['http://schemas.openid.net/event/backchannel-logout']) {
174
+ throw error(400, { message: 'Invalid logout_token events claim' });
175
+ }
176
+ if (!claims.sid && !claims.sub) {
177
+ throw error(400, { message: 'logout_token must contain sid or sub' });
178
+ }
179
+ if ('nonce' in claims && claims.nonce !== undefined) {
180
+ throw error(400, { message: 'logout_token must not contain nonce' });
181
+ }
182
+ return claims;
183
+ }
184
+ async function fetchUserInfo(accessToken) {
185
+ const metadata = await getMetadata();
186
+ if (!metadata.userinfo_endpoint) {
187
+ return undefined;
188
+ }
189
+ return fetchJson(metadata.userinfo_endpoint, {
190
+ headers: {
191
+ authorization: `Bearer ${accessToken}`
192
+ }
193
+ });
194
+ }
195
+ async function isRevoked(session) {
196
+ if (!session || !options.backChannelLogoutStore) {
197
+ return false;
198
+ }
199
+ return options.backChannelLogoutStore.isRevoked(session);
200
+ }
201
+ async function maybeRefreshSession(event, session) {
202
+ if (!session) {
203
+ return null;
204
+ }
205
+ if (await isRevoked(session)) {
206
+ cookieStore.clearSession(event.cookies);
207
+ return null;
208
+ }
209
+ if (!shouldRefresh(session, refreshToleranceSeconds)) {
210
+ return session;
211
+ }
212
+ try {
213
+ const tokenResponse = await refreshTokens(session.tokens.refreshToken);
214
+ const claims = tokenResponse.id_token
215
+ ? await validateIdToken(tokenResponse.id_token, session.nonce)
216
+ : session.claims;
217
+ const rawUser = options.fetchUserInfo !== false ? await fetchUserInfo(tokenResponse.access_token) : session.user;
218
+ const user = options.transformUser ? await options.transformUser(rawUser, { claims }) : rawUser;
219
+ const nextSession = {
220
+ ...session,
221
+ sub: claims?.sub ?? session.sub,
222
+ sid: claims?.sid ?? session.sid,
223
+ groups: collectGroups(claims, user, session.user, session.claims),
224
+ claims,
225
+ user,
226
+ sessionState: tokenResponse.session_state ?? session.sessionState,
227
+ tokens: normalizeTokens(tokenResponse, defaultScope, session.tokens),
228
+ refreshedAt: Math.floor(Date.now() / 1000)
229
+ };
230
+ const finalSession = options.transformSession
231
+ ? await options.transformSession(nextSession, {
232
+ tokenResponse,
233
+ claims,
234
+ user,
235
+ isRefresh: true
236
+ })
237
+ : nextSession;
238
+ cookieStore.writeSession(event.cookies, finalSession);
239
+ return finalSession;
240
+ }
241
+ catch {
242
+ cookieStore.clearSession(event.cookies);
243
+ return null;
244
+ }
245
+ }
246
+ async function getSession(event) {
247
+ return maybeRefreshSession(event, cookieStore.readSession(event.cookies));
248
+ }
249
+ async function signIn(event, loginOptions = {}) {
250
+ const metadata = await getMetadata();
251
+ const pkce = createPKCEPair();
252
+ const state = base64UrlEncode(randomBytes(24));
253
+ const nonce = base64UrlEncode(randomBytes(24));
254
+ const returnTo = loginOptions.returnTo ?? options.defaultLoginRedirect ?? '/';
255
+ cookieStore.writeState(event.cookies, {
256
+ state,
257
+ nonce,
258
+ codeVerifier: pkce.verifier,
259
+ returnTo,
260
+ createdAt: Math.floor(Date.now() / 1000)
261
+ });
262
+ const redirectUri = absoluteUrl(event, redirectPath);
263
+ const authorizationUrl = new URL(metadata.authorization_endpoint);
264
+ authorizationUrl.searchParams.set('client_id', options.clientId);
265
+ authorizationUrl.searchParams.set('response_type', 'code');
266
+ authorizationUrl.searchParams.set('redirect_uri', redirectUri);
267
+ authorizationUrl.searchParams.set('scope', normalizeScope(loginOptions.scope ?? defaultScope).join(' '));
268
+ authorizationUrl.searchParams.set('state', state);
269
+ authorizationUrl.searchParams.set('nonce', nonce);
270
+ authorizationUrl.searchParams.set('code_challenge', pkce.challenge);
271
+ authorizationUrl.searchParams.set('code_challenge_method', 'S256');
272
+ if (options.audience) {
273
+ authorizationUrl.searchParams.set('audience', options.audience);
274
+ }
275
+ if (loginOptions.prompt) {
276
+ authorizationUrl.searchParams.set('prompt', loginOptions.prompt);
277
+ }
278
+ for (const [key, value] of Object.entries(loginOptions.extraParams ?? {})) {
279
+ authorizationUrl.searchParams.set(key, value);
280
+ }
281
+ throw redirect(302, authorizationUrl.toString());
282
+ }
283
+ async function handleCallback(event) {
284
+ const providerError = parseProviderError(event);
285
+ if (providerError) {
286
+ throw providerError;
287
+ }
288
+ const stateCookie = cookieStore.readState(event.cookies);
289
+ const state = event.url.searchParams.get('state');
290
+ const code = event.url.searchParams.get('code');
291
+ if (!stateCookie || !state || !code || stateCookie.state !== state) {
292
+ cookieStore.clearState(event.cookies);
293
+ throw error(400, { message: 'Invalid or expired OIDC callback state' });
294
+ }
295
+ cookieStore.clearState(event.cookies);
296
+ const tokenResponse = await exchangeCode({
297
+ code,
298
+ redirectUri: absoluteUrl(event, redirectPath),
299
+ codeVerifier: stateCookie.codeVerifier
300
+ });
301
+ const claims = tokenResponse.id_token
302
+ ? await validateIdToken(tokenResponse.id_token, stateCookie.nonce)
303
+ : undefined;
304
+ const rawUser = options.fetchUserInfo === false
305
+ ? undefined
306
+ : await fetchUserInfo(tokenResponse.access_token).catch(() => undefined);
307
+ const user = options.transformUser ? await options.transformUser(rawUser, { claims }) : rawUser;
308
+ const metadata = await getMetadata();
309
+ const now = Math.floor(Date.now() / 1000);
310
+ const session = {
311
+ issuer: metadata.issuer,
312
+ clientId: options.clientId,
313
+ nonce: stateCookie.nonce,
314
+ sub: claims?.sub,
315
+ sid: claims?.sid,
316
+ sessionState: tokenResponse.session_state ?? event.url.searchParams.get('session_state') ?? undefined,
317
+ groups: collectGroups(claims, user),
318
+ user,
319
+ claims,
320
+ tokens: normalizeTokens(tokenResponse, defaultScope),
321
+ createdAt: now,
322
+ refreshedAt: now
323
+ };
324
+ const finalSession = options.transformSession
325
+ ? await options.transformSession(session, {
326
+ event,
327
+ tokenResponse,
328
+ claims,
329
+ user,
330
+ isRefresh: false
331
+ })
332
+ : session;
333
+ cookieStore.writeSession(event.cookies, finalSession);
334
+ return {
335
+ session: finalSession,
336
+ returnTo: stateCookie.returnTo
337
+ };
338
+ }
339
+ async function signOut(event, logoutOptions = {}) {
340
+ const metadata = await getMetadata();
341
+ const session = cookieStore.readSession(event.cookies);
342
+ cookieStore.clearState(event.cookies);
343
+ cookieStore.clearSession(event.cookies);
344
+ if (logoutOptions.clearSessionOnly || !metadata.end_session_endpoint) {
345
+ throw redirect(302, logoutOptions.postLogoutRedirectUri ??
346
+ options.defaultLogoutRedirect ??
347
+ options.postLogoutRedirectUri ??
348
+ '/');
349
+ }
350
+ const url = new URL(metadata.end_session_endpoint);
351
+ if (session?.tokens.idToken) {
352
+ url.searchParams.set('id_token_hint', session.tokens.idToken);
353
+ }
354
+ url.searchParams.set('post_logout_redirect_uri', absoluteUrl(event, logoutOptions.postLogoutRedirectUri ??
355
+ options.postLogoutRedirectUri ??
356
+ options.defaultLogoutRedirect ??
357
+ '/'));
358
+ if (logoutOptions.state) {
359
+ url.searchParams.set('state', logoutOptions.state);
360
+ }
361
+ throw redirect(302, url.toString());
362
+ }
363
+ async function handleBackChannelLogout(event) {
364
+ const metadata = await getMetadata();
365
+ if (!metadata.backchannel_logout_supported) {
366
+ throw error(400, { message: 'Provider does not advertise back-channel logout support' });
367
+ }
368
+ if (!options.backChannelLogoutStore) {
369
+ throw error(500, {
370
+ message: 'backChannelLogoutStore is required for back-channel logout with cookie sessions'
371
+ });
372
+ }
373
+ const form = await event.request.formData();
374
+ const logoutToken = form.get('logout_token')?.toString();
375
+ if (!logoutToken) {
376
+ throw error(400, { message: 'logout_token is required' });
377
+ }
378
+ const claims = await validateBackChannelLogoutToken(logoutToken);
379
+ await options.backChannelLogoutStore.revoke({
380
+ issuer: claims.iss,
381
+ clientId: options.clientId,
382
+ sid: claims.sid,
383
+ sub: claims.sub,
384
+ jti: claims.jti,
385
+ iat: claims.iat
386
+ });
387
+ return new Response(null, { status: 200 });
388
+ }
389
+ function requireAuth(event, returnTo) {
390
+ const session = cookieStore.readSession(event.cookies);
391
+ if (session) {
392
+ return session;
393
+ }
394
+ throw redirect(302, `${absoluteUrl(event, loginPath)}?returnTo=${encodeURIComponent(returnTo ?? `${event.url.pathname}${event.url.search}`)}`);
395
+ }
396
+ const handle = async ({ event, resolve }) => {
397
+ const session = await getSession(event);
398
+ event.locals.oidc = {
399
+ isAuthenticated: Boolean(session),
400
+ session,
401
+ user: session?.user,
402
+ claims: session?.claims,
403
+ requireAuth: () => {
404
+ if (!session) {
405
+ throw error(401, { message: 'Authentication required' });
406
+ }
407
+ },
408
+ clearSession: () => cookieStore.clearSession(event.cookies)
409
+ };
410
+ return resolve(event);
411
+ };
412
+ function loginHandler(defaults = {}) {
413
+ return async (event) => {
414
+ const returnTo = event.url.searchParams.get('returnTo') ?? defaults.returnTo;
415
+ return signIn(event, { ...defaults, returnTo });
416
+ };
417
+ }
418
+ function callbackHandler(handlerOptions = {}) {
419
+ return async (event) => {
420
+ try {
421
+ const result = await handleCallback(event);
422
+ const response = await handlerOptions.onsuccess?.(event, result);
423
+ if (response) {
424
+ return response;
425
+ }
426
+ throw redirect(302, handlerOptions.redirectTo ?? result.returnTo);
427
+ }
428
+ catch (err) {
429
+ const response = await handlerOptions.onfailure?.(event, err);
430
+ if (response) {
431
+ return response;
432
+ }
433
+ throw err;
434
+ }
435
+ };
436
+ }
437
+ function logoutHandler(defaults = {}) {
438
+ return async (event) => {
439
+ const postLogoutRedirectUri = event.url.searchParams.get('postLogoutRedirectUri') ?? defaults.postLogoutRedirectUri;
440
+ const clearSessionOnly = event.url.searchParams.get('clearSessionOnly') === '1' ||
441
+ event.url.searchParams.get('clearSessionOnly') === 'true' ||
442
+ defaults.clearSessionOnly;
443
+ return signOut(event, { ...defaults, postLogoutRedirectUri, clearSessionOnly });
444
+ };
445
+ }
446
+ function backChannelLogoutHandler() {
447
+ return async (event) => handleBackChannelLogout(event);
448
+ }
449
+ function createActions(actionOptions = {}) {
450
+ return {
451
+ login: (async (event) => {
452
+ const form = await event.request.formData();
453
+ const returnTo = (form.get('returnTo')?.toString() || actionOptions.defaultReturnTo || undefined) ?? undefined;
454
+ return signIn(event, { returnTo });
455
+ }),
456
+ logout: (async (event) => {
457
+ const form = await event.request.formData();
458
+ const postLogoutRedirectUri = (form.get('postLogoutRedirectUri')?.toString() ||
459
+ actionOptions.defaultPostLogoutRedirectUri ||
460
+ undefined) ?? undefined;
461
+ const clearSessionOnly = form.get('clearSessionOnly')?.toString() === '1' ||
462
+ form.get('clearSessionOnly')?.toString() === 'true';
463
+ return signOut(event, { postLogoutRedirectUri, clearSessionOnly });
464
+ })
465
+ };
466
+ }
467
+ async function getSessionManagementConfig() {
468
+ const metadata = await getMetadata();
469
+ return {
470
+ clientId: options.clientId,
471
+ loginPath,
472
+ redirectPath,
473
+ metadata: {
474
+ issuer: metadata.issuer,
475
+ check_session_iframe: metadata.check_session_iframe,
476
+ end_session_endpoint: metadata.end_session_endpoint,
477
+ backchannel_logout_supported: metadata.backchannel_logout_supported,
478
+ backchannel_logout_session_supported: metadata.backchannel_logout_session_supported
479
+ },
480
+ checkSessionIframe: metadata.check_session_iframe,
481
+ supportsSessionIframe: Boolean(metadata.check_session_iframe),
482
+ backChannelLogoutSupported: Boolean(metadata.backchannel_logout_supported),
483
+ backChannelLogoutSessionSupported: Boolean(metadata.backchannel_logout_session_supported)
484
+ };
485
+ }
486
+ return {
487
+ handle,
488
+ getMetadata,
489
+ getSession,
490
+ getPublicSession: async (event) => toPublicSession(await getSession(event)),
491
+ getSessionManagementConfig,
492
+ login: signIn,
493
+ logout: signOut,
494
+ handleCallback,
495
+ handleBackChannelLogout,
496
+ loginHandler,
497
+ callbackHandler,
498
+ logoutHandler,
499
+ backChannelLogoutHandler,
500
+ createActions,
501
+ requireAuth,
502
+ clearSession: cookieStore.clearSession
503
+ };
504
+ }
505
+ export const OpenIDConnect = createOIDC;
@@ -0,0 +1,5 @@
1
+ import type { OIDCClientAssertionOptions, OIDCClientSecretJwtOptions, OIDCPrivateKeyJwtOptions } from './types.js';
2
+ export declare function fetchJson<T>(url: string, init?: RequestInit): Promise<T>;
3
+ export declare function asAuthorizationHeader(clientId: string, clientSecret: string): string;
4
+ export declare function createClientSecretJwtAssertion(options: OIDCClientAssertionOptions & OIDCClientSecretJwtOptions): Promise<string>;
5
+ export declare function createPrivateKeyJwtAssertion(options: OIDCClientAssertionOptions & OIDCPrivateKeyJwtOptions): Promise<string>;
@@ -0,0 +1,43 @@
1
+ import { error } from '@sveltejs/kit';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { sign } from '@sourceregistry/node-jwt/promises';
4
+ import { base64UrlEncode } from './utils.js';
5
+ export async function fetchJson(url, init) {
6
+ const response = await fetch(url, init);
7
+ if (!response.ok) {
8
+ throw error(response.status, {
9
+ message: `OIDC request failed for '${url}' with status ${response.status}`
10
+ });
11
+ }
12
+ return (await response.json());
13
+ }
14
+ export function asAuthorizationHeader(clientId, clientSecret) {
15
+ return `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`;
16
+ }
17
+ export async function createClientSecretJwtAssertion(options) {
18
+ if (!options.clientSecret) {
19
+ throw error(500, { message: 'clientSecret is required for client_secret_jwt' });
20
+ }
21
+ const algorithm = options.algorithm ?? 'HS256';
22
+ const now = Math.floor(Date.now() / 1000);
23
+ return sign({
24
+ iss: options.clientId,
25
+ sub: options.clientId,
26
+ aud: options.tokenEndpoint,
27
+ jti: base64UrlEncode(randomBytes(24)),
28
+ iat: now,
29
+ exp: now + (options.expiresInSeconds ?? 60)
30
+ }, options.clientSecret, { alg: algorithm, typ: 'JWT' });
31
+ }
32
+ export async function createPrivateKeyJwtAssertion(options) {
33
+ const algorithm = options.algorithm ?? 'RS256';
34
+ const now = Math.floor(Date.now() / 1000);
35
+ return sign({
36
+ iss: options.clientId,
37
+ sub: options.clientId,
38
+ aud: options.tokenEndpoint,
39
+ jti: base64UrlEncode(randomBytes(24)),
40
+ iat: now,
41
+ exp: now + (options.expiresInSeconds ?? 60)
42
+ }, options.privateKey, { alg: algorithm, typ: 'JWT', ...(options.keyId ? { kid: options.keyId } : {}) });
43
+ }
@@ -0,0 +1,3 @@
1
+ import type { OIDCSession, OIDCSessionTokens, OIDCTokenResponse } from './types.js';
2
+ export declare function normalizeTokens(tokenResponse: OIDCTokenResponse, defaultScope: string[], existing?: OIDCSessionTokens, now?: number): OIDCSessionTokens;
3
+ export declare function shouldRefresh(session: OIDCSession, refreshToleranceSeconds: number, now?: number): boolean;
@@ -0,0 +1,19 @@
1
+ export function normalizeTokens(tokenResponse, defaultScope, existing, now = Math.floor(Date.now() / 1000)) {
2
+ return {
3
+ accessToken: tokenResponse.access_token,
4
+ tokenType: tokenResponse.token_type,
5
+ idToken: tokenResponse.id_token ?? existing?.idToken,
6
+ refreshToken: tokenResponse.refresh_token ?? existing?.refreshToken,
7
+ scope: tokenResponse.scope ? tokenResponse.scope.split(' ') : (existing?.scope ?? defaultScope),
8
+ expiresAt: tokenResponse.expires_in ? now + tokenResponse.expires_in : existing?.expiresAt,
9
+ refreshExpiresAt: tokenResponse.refresh_expires_in
10
+ ? now + tokenResponse.refresh_expires_in
11
+ : existing?.refreshExpiresAt
12
+ };
13
+ }
14
+ export function shouldRefresh(session, refreshToleranceSeconds, now = Math.floor(Date.now() / 1000)) {
15
+ if (!session.tokens.expiresAt || !session.tokens.refreshToken) {
16
+ return false;
17
+ }
18
+ return session.tokens.expiresAt - refreshToleranceSeconds <= now;
19
+ }
@@ -0,0 +1,2 @@
1
+ import type { OIDCBackChannelLogoutStore } from './types.js';
2
+ export declare function createInMemoryBackChannelLogoutStore(): OIDCBackChannelLogoutStore;
@@ -0,0 +1,18 @@
1
+ export function createInMemoryBackChannelLogoutStore() {
2
+ const revokedBySid = new Set();
3
+ const revokedBySub = new Set();
4
+ return {
5
+ async revoke(record) {
6
+ if (record.sid) {
7
+ revokedBySid.add(`${record.issuer}:${record.clientId}:${record.sid}`);
8
+ }
9
+ if (record.sub) {
10
+ revokedBySub.add(`${record.issuer}:${record.clientId}:${record.sub}`);
11
+ }
12
+ },
13
+ async isRevoked(session) {
14
+ return Boolean((session.sid && revokedBySid.has(`${session.issuer}:${session.clientId}:${session.sid}`)) ||
15
+ (session.sub && revokedBySub.has(`${session.issuer}:${session.clientId}:${session.sub}`)));
16
+ }
17
+ };
18
+ }