diff options
feat: oidc attempt 82845345
Diffstat (limited to 'src/hooks.server.ts')
| -rw-r--r-- | src/hooks.server.ts | 118 |
1 files changed, 95 insertions, 23 deletions
diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 2fe3744..05b8b18 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,32 +1,104 @@ +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 handle = ({ event, resolve }) => { - event.locals.auth = async () => { - const accessToken = event.cookies.get('oid__access_token'); - const sub = event.cookies.get('oid__sub'); - console.warn({ accessToken, sub }); - if (accessToken && sub) { +export const authHandler = ( + event: RequestEvent<Partial<Record<string, string>>, 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 userInfo = await client - .fetchUserInfo(await auth.getConfig(), accessToken, sub) - .catch((e) => { - console.warn(e); - - return null; - }); - console.warn({ - userInfo, - accessToken, - sub, - }); - } catch (error) {} - } else if (accessToken || sub) { - event.cookies.delete('access-token', { path: '/' }); - event.cookies.delete('sub', { path: '/' }); + 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; + } } - 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<string>, + 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<ReturnType<ReturnType<typeof authHandler>>> +>; +export type ClientSession = Omit<Session, 'tokens'> & { + tokens: { + scope?: string; + token_type?: Lowercase<string>; + 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 = <T extends Session | undefined | null>( + 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); }; |