import oncePromise from '$lib/oncePromise'; import type { Defined, Unpromise } from '$lib/util-types'; import type { RequestEvent, Transport } from '@sveltejs/kit'; import * as auth from './lib/auth.server'; import * as client from 'openid-client'; // https://svelte.dev/docs/kit/hooks#Server-hooks-handle export const authHandler = ( event: RequestEvent>, string | null> ) => oncePromise(async () => { let refreshToken = event.cookies.get('oid__refresh_token'); let accessToken = event.cookies.get('oid__access_token'); let expiry = Number(event.cookies.get('oid__expires_at')); if ( refreshToken && (!accessToken || isNaN(expiry) || expiry - 60 * 1000 >= Date.now()) ) { try { const tokens = await client.refreshTokenGrant( await auth.getConfig(), refreshToken ); auth.setCookies(event.cookies, tokens); accessToken = tokens.access_token; refreshToken = tokens.refresh_token ?? refreshToken; expiry = tokens.expires_in ? Date.now() + tokens.expires_in * 1000 : expiry; } catch (error) { return null; } } if (accessToken) { try { const introspectionResponse = await client.tokenIntrospection( await auth.getConfig(), accessToken ); if (!introspectionResponse.active) { auth.unsetCookies(event.cookies); return null; } return { tokens: { scope: introspectionResponse.scope, token_type: introspectionResponse.token_type as Lowercase, expires_at: expiry, }, userInfo: await client .fetchUserInfo( await auth.getConfig(), accessToken, introspectionResponse.sub ?? '' ) .catch((e) => { auth.unsetCookies(event.cookies); throw e; }), __is_session: 1, }; } catch (error) { return null; } } else auth.unsetCookies(event.cookies); return undefined; }); export type Session = Defined< Unpromise>> >; export type ClientSession = Omit & { tokens: { scope?: string; token_type?: Lowercase; expires_at?: number; }; }; () => { // just a type sanity check to ensure ClientSession is always a subset of Session let session!: Session; let clientSession: ClientSession = session satisfies ClientSession; void clientSession; }; export const filterSession = ( value: T ): T extends Session ? ClientSession : T => { type RT = T extends Session ? ClientSession : T; if (value === null || value === undefined) return value as RT; // clients probably shouldnt get tokens in js land (we trust the client with the token, but only over HTTP; we want to maximize the annoyance of CSRF successes) const v = structuredClone(value) as ClientSession; v.tokens = { expires_at: value.tokens.expires_at, scope: value.tokens.scope, token_type: value.tokens.token_type, }; return v as RT; }; export const handle = ({ event, resolve }) => { event.locals.auth = authHandler(event); event.locals.logout = () => auth.unsetCookies(event.cookies); return resolve(event); };