From 7fdaea73c5c67565202e19d6182fc215427919c3 Mon Sep 17 00:00:00 2001 From: memdmp Date: Tue, 19 Aug 2025 20:40:19 +0000 Subject: feat: oidc attempt 1 --- src/lib/assets/favicon.svg | 1 + src/lib/auth.server.ts | 73 ++++++++++++++++++++++++++++++++++++++++++++++ src/lib/index.ts | 1 + src/lib/oncePromise.ts | 30 +++++++++++++++++++ 4 files changed, 105 insertions(+) create mode 100644 src/lib/assets/favicon.svg create mode 100644 src/lib/auth.server.ts create mode 100644 src/lib/index.ts create mode 100644 src/lib/oncePromise.ts (limited to 'src/lib') diff --git a/src/lib/assets/favicon.svg b/src/lib/assets/favicon.svg new file mode 100644 index 0000000..cc5dc66 --- /dev/null +++ b/src/lib/assets/favicon.svg @@ -0,0 +1 @@ +svelte-logo \ No newline at end of file diff --git a/src/lib/auth.server.ts b/src/lib/auth.server.ts new file mode 100644 index 0000000..77e0dd7 --- /dev/null +++ b/src/lib/auth.server.ts @@ -0,0 +1,73 @@ +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'; + +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) +); +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; +}; diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/src/lib/oncePromise.ts b/src/lib/oncePromise.ts new file mode 100644 index 0000000..f6ce775 --- /dev/null +++ b/src/lib/oncePromise.ts @@ -0,0 +1,30 @@ +const ensurePromise = (maybePromise: T | PromiseLike): Promise => + typeof maybePromise === 'object' && + maybePromise !== null && + 'then' in maybePromise && + typeof maybePromise.then === 'function' && + 'catch' in maybePromise && + typeof maybePromise.catch === 'function' && + 'finally' in maybePromise && + typeof maybePromise.finally === 'function' + ? (maybePromise as Promise) + : Promise.resolve(maybePromise); +/** Returns a function that caches successful promises until time runs out, and throws away unsuccessful ones */ +export const oncePromise = (create: () => Promise, timeout = -1) => { + let getPromise = (): Promise => { + const oldGetPromise = getPromise, + promise = ensurePromise(create()).catch((e) => { + getPromise = oldGetPromise; + throw e; + }), + expires = timeout > 0 ? performance.now() + timeout : 0; + return (getPromise = expires + ? ((() => + performance.now() > expires + ? oldGetPromise() + : promise) as () => Promise) + : () => promise)(); + }; + return () => getPromise(); +}; +export default oncePromise; -- cgit v1.2.3