diff options
| author | 2025-08-19 20:40:19 +0000 | |
|---|---|---|
| committer | 2025-08-19 20:40:19 +0000 | |
| commit | 7fdaea73c5c67565202e19d6182fc215427919c3 (patch) | |
| tree | c69e266fe672cba5f8bffd5f53e93b0efab65e9c /src/lib | |
| download | crunched-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.svg | 1 | ||||
| -rw-r--r-- | src/lib/auth.server.ts | 73 | ||||
| -rw-r--r-- | src/lib/index.ts | 1 | ||||
| -rw-r--r-- | src/lib/oncePromise.ts | 30 |
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; |