aboutsummaryrefslogtreecommitdiffstats
path: root/src/hooks.server.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/hooks.server.ts')
-rw-r--r--src/hooks.server.ts118
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);
};