feat(sso): issue one-time tokens for iframe apps
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m24s

Generate short-lived SSO tokens in the site iframe route and add an exchange API to let child apps bootstrap their PocketBase session.
This commit is contained in:
eewing
2026-02-19 09:35:58 -06:00
parent 5c68a42703
commit 121a6b5844
5 changed files with 89 additions and 4 deletions
+15
View File
@@ -0,0 +1,15 @@
.DS_Store
Thumbs.db
node_modules
/.svelte-kit
/build
/.output
.env
.env.*
!.env.example
!.env.test
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
+32
View File
@@ -0,0 +1,32 @@
/**
* In-memory store for one-time SSO tokens.
* Token → { auth, expiresAt }. Exchanged once then deleted.
*/
const TTL_MS = 2 * 60 * 1000; // 2 minutes
type AuthPayload = { token: string; model: Record<string, unknown> | null };
const store = new Map<string, { auth: AuthPayload; expiresAt: number }>();
function prune(): void {
const now = Date.now();
for (const [key, entry] of store.entries()) {
if (entry.expiresAt <= now) store.delete(key);
}
}
export function set(token: string, auth: AuthPayload): void {
prune();
store.set(token, { auth, expiresAt: Date.now() + TTL_MS });
}
export function get(token: string): AuthPayload | null {
const entry = store.get(token);
if (!entry) return null;
if (entry.expiresAt <= Date.now()) {
store.delete(token);
return null;
}
store.delete(token); // one-time use
return entry.auth;
}
+17 -2
View File
@@ -1,7 +1,9 @@
import { sites, type SiteSlug } from '$lib/sites';
import { error } from '@sveltejs/kit';
import * as ssoStore from '$lib/sso-store';
import { randomUUID } from 'crypto';
export function load({ params }) {
export function load({ params, locals }) {
const slug = params.site?.toLowerCase() as SiteSlug | undefined;
const site = slug && slug in sites ? sites[slug as SiteSlug] : null;
@@ -9,9 +11,22 @@ export function load({ params }) {
throw error(404, `Unknown site: ${params.site}`);
}
let ssoToken: string | undefined;
if (locals.user && locals.pb?.authStore?.token) {
try {
const token = locals.pb.authStore.token;
const model = locals.pb.authStore.record as Record<string, unknown> | null;
ssoToken = randomUUID();
ssoStore.set(ssoToken, { token, model });
} catch {
// no sso if export fails
}
}
return {
slug: slug as SiteSlug,
url: site.url,
title: site.title
title: site.title,
ssoToken
};
}
+8 -2
View File
@@ -2,8 +2,14 @@
import { Button } from '$lib/components/ui/button';
let { data }: {
data: { slug: string; url: string; title: string };
data: { slug: string; url: string; title: string; ssoToken?: string };
} = $props();
const iframeSrc = $derived(
data.ssoToken
? `${data.url}${data.url.includes('?') ? '&' : '?'}sso=${data.ssoToken}`
: data.url
);
</script>
<svelte:head>
@@ -14,7 +20,7 @@
<iframe
id="app-iframe"
title={data.title}
src={data.url}
src={iframeSrc}
class="absolute inset-0 w-full h-full border-0 rounded-none"
/>
<div
+17
View File
@@ -0,0 +1,17 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import * as ssoStore from '$lib/sso-store';
export const GET: RequestHandler = async ({ url }) => {
const token = url.searchParams.get('token');
if (!token) {
return json({ error: 'Missing token' }, { status: 400 });
}
const auth = ssoStore.get(token);
if (!auth) {
return json({ error: 'Invalid or expired token' }, { status: 401 });
}
return json({ token: auth.token, model: auth.model });
};