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);
};
|