import { env as env_priv } from '$env/dynamic/private'; import { env } from '$env/dynamic/public'; import * as client from 'openid-client'; import oncePromise from './oncePromise'; import type { Cookies } from '@sveltejs/kit'; const server = new URL(env.PUBLIC_AUTH_KEYCLOAK_ISSUER); const clientId = env_priv.PRIVATE_AUTH_KEYCLOAK_ID; const clientSecret = env_priv.PRIVATE_AUTH_KEYCLOAK_SECRET; const redirectPath = '/login/callback'; // Only trigger discovery on first client.discovery (resetting the function after a failed discovery) export const getConfig = oncePromise(() => client.discovery(server, clientId, clientSecret).then((config) => { client.useJwtResponseMode(config); return config; }) ); const codeVerifier = client.randomPKCECodeVerifier(); export const getAuthorizeUrl = async ( currentUrl: URL | string, scope: string[] ) => { if (!scope.includes('openid')) scope.unshift('openid'); // do same for `email` maybe? const config = await getConfig(); const redirectUri = new URL(redirectPath, currentUrl); const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier); const codeChallengeMethod = 'S256'; let nonce: string | undefined = undefined; // redirect user to as.authorization_endpoint let parameters: Record = { redirect_uri: redirectUri.href, scope: scope.join(' '), code_challenge: codeChallenge, code_challenge_method: codeChallengeMethod, }; /** * We cannot be sure the AS supports PKCE so we're going to use nonce too. Use * of PKCE is backwards compatible even if the AS doesn't support it which is * why we're using it regardless. */ if (!config.serverMetadata().supportsPKCE()) { nonce = client.randomNonce(); parameters.nonce = nonce; } const redirectTo = client.buildAuthorizationUrl(config, parameters); return { /** Defined if PKCE isnt supported */ nonce, /** Redirect Target URL */ redirectTo, /** Where we get the user back on */ returnURI: redirectUri, }; }; /** Throws on failure */ export const authorizeNewSession = async ( currentUrl: URL, nonce: string | undefined ) => { const config = await getConfig(); let tokens = await client.authorizationCodeGrant(config, currentUrl, { pkceCodeVerifier: codeVerifier, expectedNonce: nonce, idTokenExpected: true, }); return tokens; }; export const unsetCookies = (cookies: Cookies) => { for (const v of [ 'oid__access_token', 'oid__refresh_token', 'oid__token_type', 'oid__expires_at', 'oid__scopes', ]) if (cookies.get(v)) cookies.delete(v, { path: '/' }); }; export const setCookies = ( cookies: Cookies, tokens: client.TokenEndpointResponse & client.TokenEndpointResponseHelpers ) => { for (const [k, v] of Object.entries({ oid__access_token: tokens.access_token, oid__refresh_token: tokens.refresh_token, oid__token_type: tokens.token_type, oid__expires_at: '' + (Date.now() + (tokens.expiresIn() ?? 0) * 1000), oid__scopes: tokens.scope, })) if (v) cookies.set(k, v, { path: '/', secure: true, httpOnly: true, sameSite: true, }); };