feat(sso): issue one-time tokens for iframe apps
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:
+15
@@ -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-*
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
Reference in New Issue
Block a user