aboutsummaryrefslogtreecommitdiffstats
path: root/src/lib
diff options
context:
space:
mode:
authorLibravatarLarge Libravatar memdmp <memdmpestrogenzone>2025-08-19 20:40:19 +0000
committerLibravatarLarge Libravatar memdmp <memdmpestrogenzone>2025-08-19 20:40:19 +0000
commit7fdaea73c5c67565202e19d6182fc215427919c3 (patch)
treec69e266fe672cba5f8bffd5f53e93b0efab65e9c /src/lib
downloadcrunched-7fdaea73c5c67565202e19d6182fc215427919c3.tar.gz
crunched-7fdaea73c5c67565202e19d6182fc215427919c3.tar.bz2
crunched-7fdaea73c5c67565202e19d6182fc215427919c3.tar.lz
crunched-7fdaea73c5c67565202e19d6182fc215427919c3.zip

feat: oidc attempt 1

Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/assets/favicon.svg1
-rw-r--r--src/lib/auth.server.ts73
-rw-r--r--src/lib/index.ts1
-rw-r--r--src/lib/oncePromise.ts30
4 files changed, 105 insertions, 0 deletions
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 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg> \ 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<string, string> = {
+ 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 = <T>(maybePromise: T | PromiseLike<T>): Promise<T> =>
+ 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<T>)
+ : Promise.resolve(maybePromise);
+/** Returns a function that caches successful promises until time runs out, and throws away unsuccessful ones */
+export const oncePromise = <T>(create: () => Promise<T>, timeout = -1) => {
+ let getPromise = (): Promise<T> => {
+ 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<T>)
+ : () => promise)();
+ };
+ return () => getPromise();
+};
+export default oncePromise;