diff options
| author | 2025-08-19 20:40:19 +0000 | |
|---|---|---|
| committer | 2025-08-19 20:40:19 +0000 | |
| commit | 7fdaea73c5c67565202e19d6182fc215427919c3 (patch) | |
| tree | c69e266fe672cba5f8bffd5f53e93b0efab65e9c /src/routes | |
| download | crunched-7fdaea73c5c67565202e19d6182fc215427919c3.tar.gz crunched-7fdaea73c5c67565202e19d6182fc215427919c3.tar.bz2 crunched-7fdaea73c5c67565202e19d6182fc215427919c3.tar.lz crunched-7fdaea73c5c67565202e19d6182fc215427919c3.zip | |
feat: oidc attempt 1
Diffstat (limited to 'src/routes')
| -rw-r--r-- | src/routes/+error.svelte | 6 | ||||
| -rw-r--r-- | src/routes/+layout.server.ts | 5 | ||||
| -rw-r--r-- | src/routes/+layout.svelte | 23 | ||||
| -rw-r--r-- | src/routes/+page.svelte | 26 | ||||
| -rw-r--r-- | src/routes/login/+server.ts | 55 | ||||
| -rw-r--r-- | src/routes/login/callback/+server.ts | 80 |
6 files changed, 195 insertions, 0 deletions
diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte new file mode 100644 index 0000000..a19e467 --- /dev/null +++ b/src/routes/+error.svelte @@ -0,0 +1,6 @@ +<script lang="ts"> + import { page } from '$app/state'; +</script> + +<h2 class="text-2xl">HTTP {page.status}</h2> +<p>{page.error?.message}</p> diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts new file mode 100644 index 0000000..afdac71 --- /dev/null +++ b/src/routes/+layout.server.ts @@ -0,0 +1,5 @@ +export const load = async ({ locals }) => { + return { + // session: await locals.auth(), + }; +}; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..1980c3a --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,23 @@ +<script lang="ts"> + import '../app.css'; + import favicon from '$lib/assets/favicon.svg'; + import { page } from '$app/state'; + + let { children } = $props(); +</script> + +<svelte:head> + <link rel="icon" href={favicon} /> +</svelte:head> + +<nav class="header"> + <h1 class="text-4xl">crunched</h1> + <p> + <a href="/">home</a> - {#if page.data.session}<a href="/vms">vms</a + >{:else}<a href="/login?scope=profile%20vm-own-read">login</a>{/if} + </p> + <div class="my-2"> + <hr /> + </div> +</nav> +{@render children?.()} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000..2634622 --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,26 @@ +<script> + import { page } from '$app/state'; + let s = $derived(page.data.session); +</script> + +{@debug s} + +<h1>SvelteKit Auth Example</h1> +<div> + {#if page.data.session} + {#if page.data.session.user?.image} + <img + src={page.data.session.user.image} + class="avatar" + alt="User Avatar" + /> + {/if} + <span> + <small>Signed in as</small><br /> + <strong>{page.data.session.user?.name ?? 'User'}</strong> + </span> + <!-- <div slot="submitButton" class="buttonPrimary">Sign out</div> --> + {:else} + <span class="notSignedInText">You are not signed in</span> + {/if} +</div> diff --git a/src/routes/login/+server.ts b/src/routes/login/+server.ts new file mode 100644 index 0000000..4a032d4 --- /dev/null +++ b/src/routes/login/+server.ts @@ -0,0 +1,55 @@ +import { getAuthorizeUrl } from '$lib/auth.server.js'; +import { error, redirect } from '@sveltejs/kit'; + +export const GET = async (event) => { + let target = event.url.searchParams.get('next') ?? '/'; + let desiredScopes = + event.url.searchParams.get('scope') ?? 'profile vm-own-read'; + if (new URL(target, event.url.href).host !== event.url.host) target = '/'; + const existingScopes = (event.cookies.get('oid__scopes') ?? '').split(' '); + const authed = await event.locals.auth(); + const missingScopes = !!desiredScopes + .split(' ') + .find((v) => !existingScopes.includes(v)); + if ( + // if we're not authenticated + !authed || + // or we're missing scopes + missingScopes + ) { + const { nonce, redirectTo } = await getAuthorizeUrl( + event.url.href, + desiredScopes.split(' ') + ); + if (nonce) { + let existingNonces = []; + try { + const n = JSON.parse(event.cookies.get('pending-auth-nonces') ?? '[]'); + if (Array.isArray(n) && n.length && typeof n[0] === 'string') + existingNonces = n; + } catch (error) { + // revoke all existing nonces + } + event.cookies.set( + 'pending-auth-nonces', + JSON.stringify([...existingNonces, nonce]), + { + path: '/', + httpOnly: true, + secure: true, + sameSite: true, + } + ); + } else + event.cookies.delete('pending-auth-nonces', { + path: '/', + }); + event.cookies.delete('next', { + path: target, + }); + throw redirect(303, redirectTo); + } else { + throw redirect(303, target); + } +}; +export const POST = GET; diff --git a/src/routes/login/callback/+server.ts b/src/routes/login/callback/+server.ts new file mode 100644 index 0000000..32b1647 --- /dev/null +++ b/src/routes/login/callback/+server.ts @@ -0,0 +1,80 @@ +import * as auth from '$lib/auth.server.js'; +import { error, json, redirect } from '@sveltejs/kit'; +import * as client from 'openid-client'; + +// Pre-checker for nonce, not the primary implementation +const handleNonce = (nonce: string | null, nonceCookie: string | undefined) => { + if (nonce) { + try { + const n = JSON.parse(nonceCookie ?? '[]'); + if (Array.isArray(n) && n.length && typeof n[0] === 'string') { + if (!n.includes(nonce)) throw error(400, 'Nonce not in array'); + else return n.filter((v) => v !== nonce); + } else throw error(400, 'Nonce provided, but nonce cookie not found'); + } catch (e) { + throw error(400, `Failed parsing nonce: ${e}`); + } + } else if (nonceCookie) throw error(400, 'Missing Nonce'); +}; +export const GET = async (event) => { + const sp = event.url.searchParams; + const params = { + sessionState: sp.get('session_state'), + iss: sp.get('iss'), + code: sp.get('code'), + nonce: sp.get('nonce'), + }; + if (!params.sessionState || !params.iss || !params.code) + throw error(400, 'Missing one of session_state, iss, code'); + + const remainingNonces = handleNonce( + params.nonce, + event.cookies.get('pending-auth-nonces') + ); + + try { + const tk = await auth.authorizeNewSession( + new URL(event.url.href), + params.nonce ?? undefined + ); + + for (const [k, v] of Object.entries({ + oid__access_token: tk.access_token, + oid__token_type: tk.token_type, + oid__expires_at: '' + (Date.now() + (tk.expiresIn() ?? 0) * 1000), + oid__refresh_token: tk.refresh_token, + oid__sub: tk.claims()!.sub, + 'pending-auth-nonces': JSON.stringify(remainingNonces), + })) + if (v) + event.cookies.set(k, v, { + path: '/', + secure: true, + httpOnly: true, + sameSite: true, + }); + if (tk.scope) + event.cookies.set('oid__scopes', tk.scope, { + path: '/', + secure: true, + httpOnly: true, + sameSite: true, + }); + + console.warn( + 'New Session:', + await client.fetchUserInfo( + await auth.getConfig(), + tk.access_token, + tk.claims()!.sub + ) + ); + + return json({ + sub: tk.claims()!.sub, + at: tk.access_token, + }); + } catch (e) { + throw redirect(307, '/login'); + } +}; |