aboutsummaryrefslogtreecommitdiffstats
path: root/src/hooks.server.ts
blob: 05b8b184b8ab87014e43c18fbabe3e6aa3296361 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
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 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 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<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);
};