|
import { Issuer, BaseClient, type UserinfoResponse, TokenSet } from "openid-client"; |
|
import { addHours, addYears } from "date-fns"; |
|
import { |
|
COOKIE_NAME, |
|
OPENID_CLIENT_ID, |
|
OPENID_CLIENT_SECRET, |
|
OPENID_PROVIDER_URL, |
|
OPENID_SCOPES, |
|
} from "$env/static/private"; |
|
import { sha256 } from "$lib/utils/sha256"; |
|
import { z } from "zod"; |
|
import { dev } from "$app/environment"; |
|
import type { Cookies } from "@sveltejs/kit"; |
|
|
|
export interface OIDCSettings { |
|
redirectURI: string; |
|
} |
|
|
|
export interface OIDCUserInfo { |
|
token: TokenSet; |
|
userData: UserinfoResponse; |
|
} |
|
|
|
export const requiresUser = !!OPENID_CLIENT_ID && !!OPENID_CLIENT_SECRET; |
|
|
|
export function refreshSessionCookie(cookies: Cookies, sessionId: string) { |
|
cookies.set(COOKIE_NAME, sessionId, { |
|
path: "/", |
|
|
|
sameSite: dev ? "lax" : "none", |
|
secure: !dev, |
|
httpOnly: true, |
|
expires: addYears(new Date(), 1), |
|
}); |
|
} |
|
|
|
export const authCondition = (locals: App.Locals) => { |
|
return locals.user |
|
? { userId: locals.user._id } |
|
: { sessionId: locals.sessionId, userId: { $exists: false } }; |
|
}; |
|
|
|
|
|
|
|
|
|
export async function generateCsrfToken(sessionId: string, redirectUrl: string): Promise<string> { |
|
const data = { |
|
expiration: addHours(new Date(), 1).getTime(), |
|
redirectUrl, |
|
}; |
|
|
|
return Buffer.from( |
|
JSON.stringify({ |
|
data, |
|
signature: await sha256(JSON.stringify(data) + "##" + sessionId), |
|
}) |
|
).toString("base64"); |
|
} |
|
|
|
async function getOIDCClient(settings: OIDCSettings): Promise<BaseClient> { |
|
const issuer = await Issuer.discover(OPENID_PROVIDER_URL); |
|
return new issuer.Client({ |
|
client_id: OPENID_CLIENT_ID, |
|
client_secret: OPENID_CLIENT_SECRET, |
|
redirect_uris: [settings.redirectURI], |
|
response_types: ["code"], |
|
}); |
|
} |
|
|
|
export async function getOIDCAuthorizationUrl( |
|
settings: OIDCSettings, |
|
params: { sessionId: string } |
|
): Promise<string> { |
|
const client = await getOIDCClient(settings); |
|
const csrfToken = await generateCsrfToken(params.sessionId, settings.redirectURI); |
|
const url = client.authorizationUrl({ |
|
scope: OPENID_SCOPES, |
|
state: csrfToken, |
|
}); |
|
|
|
return url; |
|
} |
|
|
|
export async function getOIDCUserData(settings: OIDCSettings, code: string): Promise<OIDCUserInfo> { |
|
const client = await getOIDCClient(settings); |
|
const token = await client.callback(settings.redirectURI, { code }); |
|
const userData = await client.userinfo(token); |
|
|
|
return { token, userData }; |
|
} |
|
|
|
export async function validateAndParseCsrfToken( |
|
token: string, |
|
sessionId: string |
|
): Promise<{ |
|
|
|
redirectUrl: string; |
|
} | null> { |
|
try { |
|
const { data, signature } = z |
|
.object({ |
|
data: z.object({ |
|
expiration: z.number().int(), |
|
redirectUrl: z.string().url(), |
|
}), |
|
signature: z.string().length(64), |
|
}) |
|
.parse(JSON.parse(token)); |
|
const reconstructSign = await sha256(JSON.stringify(data) + "##" + sessionId); |
|
|
|
if (data.expiration > Date.now() && signature === reconstructSign) { |
|
return { redirectUrl: data.redirectUrl }; |
|
} |
|
} catch (e) { |
|
console.error(e); |
|
} |
|
return null; |
|
} |
|
|