From 91ea5c6b67d2dd14d8207490c4fe2a4e733cf08c Mon Sep 17 00:00:00 2001 From: KeeganBeuthin Date: Fri, 9 May 2025 10:27:00 +0200 Subject: [PATCH 01/14] added sveltekit cloudflare docs --- .../kinde-sveltekit-with-cloudflare.mdx | 251 ++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx diff --git a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx new file mode 100644 index 000000000..75f688e3a --- /dev/null +++ b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx @@ -0,0 +1,251 @@ +--- +page_id: 4f3a9e2a-12eb-4b4c-8790-48b6e09a224d +title: "Integrating Kinde Authentication with SvelteKit on Cloudflare Pages" +description: "A step-by-step guide to implement Kinde authentication in a SvelteKit application deployed to Cloudflare Pages" +--- + + +This guide walks you through implementing Kinde authentication in a SvelteKit application deployed to Cloudflare Pages using KV storage for state management. + +## Prerequisites + +- A Cloudflare account +- A Kinde account (register at [kinde.com](https://kinde.com)) +- SvelteKit project ready for Cloudflare Pages deployment + +## 1. Set Up a Kinde Application + +1. Log in to your Kinde account +2. Go to **Settings > Applications** +3. Create a new Sveltekit application +4. Configure the callback URLs: + - Allowed callback URL: `https://your-domain.pages.dev/api/auth/kinde_callback` + - Allowed logout redirect URL: `https://your-domain.pages.dev` +5. Note your Client ID and Client Secret + +## 2. Install Required Dependencies + +```bash +npm install @kinde-oss/kinde-auth-sveltekit +``` + +## 3. Configure Cloudflare KV Storage + +1. Go to **Workers & Pages > KV** +2. Create a new namespace (e.g., `AUTH_STORAGE`) +3. Copy the namespace ID + +## 4. Set Up Wrangler Configuration + +Update your `wrangler.toml`: + +```toml +name = "your-project-name" +compatibility_date = "2023-06-28" +compatibility_flags = ["nodejs_compat_v2"] +pages_build_output_dir = "./svelte-kit/cloudflare" + +kv_namespaces = [ + { binding = "AUTH_STORAGE", id = "your-namespace-id" } +] + +[vars] +KINDE_ISSUER_URL = "https://your-kinde-domain.kinde.com" +KINDE_CLIENT_ID = "your-client-id" +KINDE_CLIENT_SECRET = "your-client-secret" +KINDE_REDIRECT_URL = "https://your-domain.pages.dev/api/auth/kinde_callback" +KINDE_POST_LOGOUT_REDIRECT_URL = "https://your-domain.pages.dev" +KINDE_POST_LOGIN_REDIRECT_URL = "https://your-domain.pages.dev/dashboard" +KINDE_AUTH_WITH_PKCE = "true" +``` + +## 5. Create a KV Storage Adapter + +Create `src/lib/kindeCloudflareStorage.ts`: + +```typescript +import type { RequestEvent } from '@sveltejs/kit'; + +export const createKindeStorage = (event: RequestEvent) => { + const platform = event.platform as any; + const env = platform?.env; + const AUTH_STORAGE = env?.AUTH_STORAGE; + + if (!AUTH_STORAGE) { + return null; + } + + return { + setState: async (stateId: string, stateData: any) => { + try { + const key = `kinde:state:${stateId}`; + const value = typeof stateData === 'string' + ? stateData + : JSON.stringify(stateData); + + await AUTH_STORAGE.put(key, value, { expirationTtl: 600 }); + return true; + } catch (error) { + return false; + } + }, + + getState: async (stateId: string) => { + try { + const key = `kinde:state:${stateId}`; + const value = await AUTH_STORAGE.get(key); + + if (!value) return null; + + try { + return JSON.parse(value); + } catch { + return value; + } + } catch (error) { + return null; + } + }, + + deleteState: async (stateId: string) => { + try { + const key = `kinde:state:${stateId}`; + await AUTH_STORAGE.delete(key); + return true; + } catch (error) { + return false; + } + } + }; +}; +``` + +## 6. Set Up SvelteKit Hooks + +Update `src/hooks.server.ts`: + +```typescript +import { sessionHooks, type Handler } from '@kinde-oss/kinde-auth-sveltekit'; +import { createKindeStorage } from '$lib/kindeCloudflareStorage'; + +export const handle: Handler = async ({ event, resolve }) => { + const storage = createKindeStorage(event); + + sessionHooks({ + event, + ...(storage ? { storage } : {}) + }); + + return await resolve(event); +}; +``` + +## 7. Implement Authentication Routes + +Create `src/routes/api/auth/[...kindeAuth]/+server.ts`: + +```typescript +import { json, redirect } from '@sveltejs/kit'; +import type { RequestEvent } from "@sveltejs/kit"; +import { createKindeStorage } from '$lib/kindeCloudflareStorage'; + +export async function GET(event: RequestEvent) { + // Get environment variables from platform + const platform = event.platform as any; + const env = platform?.env; + + // Simplified config access + const config = { + issuerUrl: env?.KINDE_ISSUER_URL || process.env.KINDE_ISSUER_URL, + clientId: env?.KINDE_CLIENT_ID || process.env.KINDE_CLIENT_ID, + clientSecret: env?.KINDE_CLIENT_SECRET || process.env.KINDE_CLIENT_SECRET, + redirectUrl: env?.KINDE_REDIRECT_URL || process.env.KINDE_REDIRECT_URL, + postLoginUrl: env?.KINDE_POST_LOGIN_REDIRECT_URL || process.env.KINDE_POST_LOGIN_REDIRECT_URL, + postLogoutUrl: env?.KINDE_POST_LOGOUT_REDIRECT_URL || process.env.KINDE_POST_LOGOUT_REDIRECT_URL, + scope: 'openid profile email offline', + usePkce: (env?.KINDE_AUTH_WITH_PKCE || process.env.KINDE_AUTH_WITH_PKCE) === 'true' + }; + + const storage = createKindeStorage(event); + const path = new URL(event.request.url).pathname.split('/').pop() || ''; + + if (!storage) { + return json({ error: 'KV storage not available' }, { status: 500 }); + } + + // Handle authentication routes + switch (path) { + case 'login': + return handleLogin(config.issuerUrl, config.clientId, config.redirectUrl, + config.scope, config.postLoginUrl, config.usePkce, + event, storage, false); + case 'register': + return handleLogin(config.issuerUrl, config.clientId, config.redirectUrl, + config.scope, config.postLoginUrl, config.usePkce, + event, storage, true); + case 'kinde_callback': + return handleCallback(config.issuerUrl, config.clientId, config.clientSecret, + config.redirectUrl, config.usePkce, event, storage); + case 'logout': + return handleLogout(config.issuerUrl, config.clientId, config.postLogoutUrl, + event, storage); + default: + return json({ error: 'Unknown auth endpoint' }, { status: 404 }); + } +} + +// Include implementations of handleLogin, handleCallback, and handleLogout functions +``` +## 8. Check Authentication Status + +Create `src/routes/+layout.server.ts`: + +```typescript +import type { LayoutServerLoad } from './$types'; +import { createKindeStorage } from '$lib/kindeCloudflareStorage'; + +export const load: LayoutServerLoad = async (event) => { + const storage = createKindeStorage(event); + + if (!storage) { + return { authenticated: false }; + } + + try { + const tokens = await storage.getState('tokens'); + return { authenticated: !!tokens?.access_token }; + } catch (error) { + return { authenticated: false }; + } +}; +``` + +## 9. Protect Routes + +For protected routes like a dashboard: + +```typescript +// src/routes/dashboard/+page.server.ts +import type { PageServerLoad } from './$types'; +import { createKindeStorage } from '$lib/kindeCloudflareStorage'; +import { redirect } from '@sveltejs/kit'; + +export const load: PageServerLoad = async (event) => { + const storage = createKindeStorage(event); + + if (!storage) { + throw redirect(302, '/login'); + } + + const tokens = await storage.getState('tokens'); + + if (!tokens?.access_token) { + throw redirect(302, '/login'); + } + + return { authenticated: true }; +}; +``` + +Your Kinde authentication is now working with SvelteKit on Cloudflare Pages! + From 76c0e0f0dc24166794d291b4999858131febf250 Mon Sep 17 00:00:00 2001 From: KeeganBeuthin Date: Fri, 9 May 2025 16:50:12 +0200 Subject: [PATCH 02/14] working docs. currently very verose --- .../kinde-sveltekit-with-cloudflare.mdx | 330 ++++++++++++++++-- 1 file changed, 301 insertions(+), 29 deletions(-) diff --git a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx index 75f688e3a..1ef010d19 100644 --- a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx +++ b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx @@ -7,6 +7,9 @@ description: "A step-by-step guide to implement Kinde authentication in a Svelte This guide walks you through implementing Kinde authentication in a SvelteKit application deployed to Cloudflare Pages using KV storage for state management. + +> **Note**: This implementation is more complex than ideal due to the unique constraints of Cloudflare's serverless environment. The most challenging aspect is handling environment variables, which work differently on Cloudflare Pages than in standard SvelteKit deployments. We're actively exploring more elegant solutions, but this guide provides a robust working implementation for your immediate needs. + ## Prerequisites - A Cloudflare account @@ -27,6 +30,7 @@ This guide walks you through implementing Kinde authentication in a SvelteKit ap ```bash npm install @kinde-oss/kinde-auth-sveltekit +npm install -D @sveltejs/adapter-cloudflare ``` ## 3. Configure Cloudflare KV Storage @@ -57,6 +61,7 @@ KINDE_REDIRECT_URL = "https://your-domain.pages.dev/api/auth/kinde_callback" KINDE_POST_LOGOUT_REDIRECT_URL = "https://your-domain.pages.dev" KINDE_POST_LOGIN_REDIRECT_URL = "https://your-domain.pages.dev/dashboard" KINDE_AUTH_WITH_PKCE = "true" +KINDE_SCOPE = 'openid profile email offline' ``` ## 5. Create a KV Storage Adapter @@ -148,53 +153,320 @@ Create `src/routes/api/auth/[...kindeAuth]/+server.ts`: import { json, redirect } from '@sveltejs/kit'; import type { RequestEvent } from "@sveltejs/kit"; import { createKindeStorage } from '$lib/kindeCloudflareStorage'; +import { KINDE_ISSUER_URL, KINDE_CLIENT_ID, KINDE_CLIENT_SECRET, KINDE_REDIRECT_URL, KINDE_POST_LOGIN_REDIRECT_URL, KINDE_POST_LOGOUT_REDIRECT_URL, KINDE_AUTH_WITH_PKCE } from '$env/static/private'; + +// Get environment variables +const SECRET = KINDE_CLIENT_SECRET; +const ISSUER_URL = KINDE_ISSUER_URL; +const CLIENT_ID = KINDE_CLIENT_ID; +const REDIRECT_URL = KINDE_REDIRECT_URL; +const POST_LOGIN_REDIRECT_URL = KINDE_POST_LOGIN_REDIRECT_URL; +const POST_LOGOUT_REDIRECT_URL = KINDE_POST_LOGOUT_REDIRECT_URL; +const SCOPE = 'openid profile email offline' +const USE_PKCE = KINDE_AUTH_WITH_PKCE === 'true'; export async function GET(event: RequestEvent) { - // Get environment variables from platform - const platform = event.platform as any; - const env = platform?.env; - - // Simplified config access - const config = { - issuerUrl: env?.KINDE_ISSUER_URL || process.env.KINDE_ISSUER_URL, - clientId: env?.KINDE_CLIENT_ID || process.env.KINDE_CLIENT_ID, - clientSecret: env?.KINDE_CLIENT_SECRET || process.env.KINDE_CLIENT_SECRET, - redirectUrl: env?.KINDE_REDIRECT_URL || process.env.KINDE_REDIRECT_URL, - postLoginUrl: env?.KINDE_POST_LOGIN_REDIRECT_URL || process.env.KINDE_POST_LOGIN_REDIRECT_URL, - postLogoutUrl: env?.KINDE_POST_LOGOUT_REDIRECT_URL || process.env.KINDE_POST_LOGOUT_REDIRECT_URL, - scope: 'openid profile email offline', - usePkce: (env?.KINDE_AUTH_WITH_PKCE || process.env.KINDE_AUTH_WITH_PKCE) === 'true' - }; - const storage = createKindeStorage(event); - const path = new URL(event.request.url).pathname.split('/').pop() || ''; + const url = new URL(event.request.url); + const path = url.pathname.split('/').pop() || ''; + + console.log(`Auth request: ${path}`, { + hasStorage: !!storage, + hasState: !!url.searchParams.get('state'), + hasCode: !!url.searchParams.get('code') + }); if (!storage) { + console.error('KV storage not available'); return json({ error: 'KV storage not available' }, { status: 500 }); } - // Handle authentication routes + // Handle various auth endpoints switch (path) { case 'login': - return handleLogin(config.issuerUrl, config.clientId, config.redirectUrl, - config.scope, config.postLoginUrl, config.usePkce, - event, storage, false); + return handleLogin(event, storage, false); + case 'register': - return handleLogin(config.issuerUrl, config.clientId, config.redirectUrl, - config.scope, config.postLoginUrl, config.usePkce, - event, storage, true); + return handleLogin(event, storage, true); + case 'kinde_callback': - return handleCallback(config.issuerUrl, config.clientId, config.clientSecret, - config.redirectUrl, config.usePkce, event, storage); + return handleCallback(event, storage); + case 'logout': - return handleLogout(config.issuerUrl, config.clientId, config.postLogoutUrl, - event, storage); + return handleLogout(event, storage); + default: return json({ error: 'Unknown auth endpoint' }, { status: 404 }); } } -// Include implementations of handleLogin, handleCallback, and handleLogout functions +// Generate crypto-secure random string for state +function generateRandomString(length = 32) { + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let text = ''; + for (let i = 0; i < length; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} + +// Add this at the top of your file +async function sha256(plain: string): Promise { + const encoder = new TextEncoder(); + const data = encoder.encode(plain); + return await crypto.subtle.digest('SHA-256', data); +} + +function base64URLEncode(buffer: ArrayBuffer): string { + return btoa(String.fromCharCode(...new Uint8Array(buffer))) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); +} + +// Handle login or registration +async function handleLogin(event: RequestEvent, storage: any, isRegister: boolean) { + // Generate state parameter + const state = generateRandomString(24); + console.log(KINDE_CLIENT_ID, KINDE_CLIENT_SECRET, KINDE_ISSUER_URL, KINDE_REDIRECT_URL, KINDE_POST_LOGIN_REDIRECT_URL, KINDE_POST_LOGOUT_REDIRECT_URL, KINDE_AUTH_WITH_PKCE, SCOPE) + // For PKCE, generate code challenge + let codeVerifier: string | undefined; + let codeChallenge: string | undefined; + + if (USE_PKCE) { + codeVerifier = generateRandomString(64); + + // Create proper code challenge with SHA-256 + const challengeBuffer = await sha256(codeVerifier); + codeChallenge = base64URLEncode(challengeBuffer); + } + + // Store state (and code verifier if using PKCE) + await storage.setState(state, codeVerifier || 'true'); + + // Get additional parameters from URL + const url = new URL(event.request.url); + const orgCode = url.searchParams.get('org_code'); + const postLoginRedirect = url.searchParams.get('post_login_redirect_url') || POST_LOGIN_REDIRECT_URL; + + // Build auth URL + const authUrl = new URL(isRegister ? '/oauth2/auth/register' : '/oauth2/auth', ISSUER_URL); + + // Add standard OAuth parameters + authUrl.searchParams.append('client_id', CLIENT_ID); + authUrl.searchParams.append('redirect_uri', REDIRECT_URL); + authUrl.searchParams.append('response_type', 'code'); + authUrl.searchParams.append('scope', SCOPE); + authUrl.searchParams.append('state', state); + + // Add optional parameters + if (orgCode) { + authUrl.searchParams.append('org_code', orgCode); + } + + // Store post-login redirect + await storage.setState(`redirect:${state}`, postLoginRedirect); + + // Add PKCE parameters if enabled + if (USE_PKCE && codeChallenge) { + authUrl.searchParams.append('code_challenge', codeChallenge); + authUrl.searchParams.append('code_challenge_method', 'S256'); + } + + // Redirect to Kinde auth URL + console.log(`Redirecting to Kinde auth: ${authUrl.toString()}`); + return redirect(302, authUrl.toString()); +} + +// Handle OAuth callback +async function handleCallback(event: RequestEvent, storage: any) { + const url = new URL(event.request.url); + const code = url.searchParams.get('code'); + const state = url.searchParams.get('state'); + const error = url.searchParams.get('error'); + + // Check for OAuth errors + if (error) { + console.error('OAuth error:', error); + return json({ error: `OAuth error: ${error}` }, { status: 400 }); + } + + // Validate required parameters + if (!code || !state) { + console.error('Missing code or state parameter'); + return json({ error: 'Missing code or state parameter' }, { status: 400 }); + } + + // Verify state parameter + const storedState = await storage.getState(state); + if (!storedState) { + console.error('State not found:', state); + + // Store error for debugging + await storage.setState('last_error', { + time: new Date().toISOString(), + error: 'State not found', + state + }); + + return json({ error: 'Invalid state parameter' }, { status: 401 }); + } + + // Get code verifier for PKCE if it exists + const codeVerifier = storedState === 'true' ? undefined : storedState; + + // Get post-login redirect URL + const redirectUrl = await storage.getState(`redirect:${state}`) || POST_LOGIN_REDIRECT_URL; + + // Clean up stored state + await storage.deleteState(state); + await storage.deleteState(`redirect:${state}`); + + try { + // Exchange code for tokens + const tokenResponse = await fetchTokens(code, codeVerifier); + + // Store tokens in KV storage + await storage.setState('tokens', { + access_token: tokenResponse.access_token, + refresh_token: tokenResponse.refresh_token || null, + id_token: tokenResponse.id_token || null, + expires_in: tokenResponse.expires_in || 3600, + timestamp: Date.now() + }); + + console.log('Tokens stored successfully'); + + // Log the redirect URL for debugging + console.log('Redirecting to:', redirectUrl); + + // Create a redirect response with proper headers + return new Response(null, { + status: 302, + headers: { + 'Location': redirectUrl, + 'Cache-Control': 'no-store' + } + }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + console.error('Token exchange error:', errorMessage); + + // Store error for debugging + await storage.setState('token_error', { + time: new Date().toISOString(), + error: safeStringify(error) + }); + + return json({ error: 'Token exchange failed' }, { status: 500 }); + } +} + +// Exchange authorization code for tokens +async function fetchTokens(code: string, codeVerifier?: string) { + const tokenUrl = new URL('/oauth2/token', ISSUER_URL); + const params = new URLSearchParams(); + + params.append('grant_type', 'authorization_code'); + params.append('code', code); + params.append('redirect_uri', REDIRECT_URL); + params.append('client_id', CLIENT_ID); + + const headers: Record = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }; + + // The key fix: Always include client_secret with Kinde, even with PKCE + if (USE_PKCE && codeVerifier && codeVerifier !== 'true') { + console.log('Using PKCE flow with code verifier'); + params.append('code_verifier', codeVerifier); + + // CRITICAL FIX: Kinde requires client_secret even with PKCE flow for server-side apps + params.append('client_secret', SECRET); + } else { + console.log('Using standard authorization code flow with client secret'); + params.append('client_secret', SECRET); + } + + console.log('Token exchange request:', { + url: tokenUrl.toString(), + isPKCE: USE_PKCE && codeVerifier && codeVerifier !== 'true', + hasClientSecret: true + }); + + try { + const response = await fetch(tokenUrl.toString(), { + method: 'POST', + headers, + body: params + }); + + const responseText = await response.text(); + console.log('Token response status:', response.status); + + // Try to parse the response as JSON + let responseData; + try { + responseData = JSON.parse(responseText); + console.log('Token response parsed successfully'); + } catch (parseError) { + console.error('Failed to parse token response as JSON:', responseText); + throw new Error(`Invalid JSON response: ${responseText}`); + } + + // Check for errors in the response + if (!response.ok) { + console.error('Token exchange error details:', { + status: response.status, + error: responseData.error, + description: responseData.error_description + }); + + throw new Error(`Token exchange failed: ${response.status} - ${responseData.error}: ${responseData.error_description}`); + } + + // Check if the response has the expected tokens + if (!responseData.access_token) { + console.error('Token response missing access_token:', responseData); + throw new Error('Token response missing required fields'); + } + + // Log success + console.log('Token exchange successful'); + + return responseData; + } catch (error) { + console.error('Token exchange error:', error instanceof Error ? error.message : 'Unknown error'); + throw error; + } +} + +// Handle logout +async function handleLogout(event: RequestEvent, storage: any) { + // Clear tokens from KV storage + await storage.deleteState('tokens'); + console.log('Tokens deleted during logout'); + + // Redirect to Kinde's logout endpoint + const logoutUrl = new URL('/logout', ISSUER_URL); + logoutUrl.searchParams.append('redirect', POST_LOGOUT_REDIRECT_URL); + + return redirect(302, logoutUrl.toString()); +} + +// Helper function to safely stringify errors +function safeStringify(obj: any): string { + try { + if (obj instanceof Error) { + return obj.message + (obj.stack ? `\n${obj.stack}` : ''); + } + + return JSON.stringify(obj); + } catch (e) { + return `[Unstringifiable object: ${typeof obj}]`; + } +} ``` ## 8. Check Authentication Status From fd4203e23ee1ee0c3b7f31cad4477d388ce89226 Mon Sep 17 00:00:00 2001 From: KeeganBeuthin Date: Fri, 9 May 2025 16:52:22 +0200 Subject: [PATCH 03/14] fixed typo --- .../third-party-tools/kinde-sveltekit-with-cloudflare.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx index 1ef010d19..1a4088c8d 100644 --- a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx +++ b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx @@ -8,7 +8,7 @@ description: "A step-by-step guide to implement Kinde authentication in a Svelte This guide walks you through implementing Kinde authentication in a SvelteKit application deployed to Cloudflare Pages using KV storage for state management. -> **Note**: This implementation is more complex than ideal due to the unique constraints of Cloudflare's serverless environment. The most challenging aspect is handling environment variables, which work differently on Cloudflare Pages than in standard SvelteKit deployments. We're actively exploring more elegant solutions, but this guide provides a robust working implementation for your immediate needs. +> Note: This implementation is more complex than ideal due to Cloudflare's serverless environment constraints. We're working on more elegant solutions, but this guide provides a robust working implementation. ## Prerequisites From f7f8f38c40c11af3f0367d0a49f71815e1972dca Mon Sep 17 00:00:00 2001 From: KeeganBeuthin Date: Mon, 12 May 2025 11:13:41 +0200 Subject: [PATCH 04/14] addressing rabbit ai concerns --- .../kinde-sveltekit-with-cloudflare.mdx | 205 ++++++++++++++++-- 1 file changed, 183 insertions(+), 22 deletions(-) diff --git a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx index 1a4088c8d..b7a7f2026 100644 --- a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx +++ b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx @@ -170,11 +170,6 @@ export async function GET(event: RequestEvent) { const url = new URL(event.request.url); const path = url.pathname.split('/').pop() || ''; - console.log(`Auth request: ${path}`, { - hasStorage: !!storage, - hasState: !!url.searchParams.get('state'), - hasCode: !!url.searchParams.get('code') - }); if (!storage) { console.error('KV storage not available'); @@ -203,11 +198,9 @@ export async function GET(event: RequestEvent) { // Generate crypto-secure random string for state function generateRandomString(length = 32) { const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - let text = ''; - for (let i = 0; i < length; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; + const array = new Uint8Array(length); + crypto.getRandomValues(array); + return Array.from(array, b => possible.charAt(b % possible.length)).join(''); } // Add this at the top of your file @@ -228,7 +221,6 @@ function base64URLEncode(buffer: ArrayBuffer): string { async function handleLogin(event: RequestEvent, storage: any, isRegister: boolean) { // Generate state parameter const state = generateRandomString(24); - console.log(KINDE_CLIENT_ID, KINDE_CLIENT_SECRET, KINDE_ISSUER_URL, KINDE_REDIRECT_URL, KINDE_POST_LOGIN_REDIRECT_URL, KINDE_POST_LOGOUT_REDIRECT_URL, KINDE_AUTH_WITH_PKCE, SCOPE) // For PKCE, generate code challenge let codeVerifier: string | undefined; let codeChallenge: string | undefined; @@ -242,7 +234,7 @@ async function handleLogin(event: RequestEvent, storage: any, isRegister: boolea } // Store state (and code verifier if using PKCE) - await storage.setState(state, codeVerifier || 'true'); +await storage.setState(state, JSON.stringify({ codeVerifier, redirect: postLoginRedirect })); // Get additional parameters from URL const url = new URL(event.request.url); @@ -322,6 +314,80 @@ async function handleCallback(event: RequestEvent, storage: any) { await storage.deleteState(state); await storage.deleteState(`redirect:${state}`); + try { + // Exchange code for tokens + const tokenResponse = await fetchTokens(code, codeVerifier); + + // Generate a unique session ID for this user + const sessionId = generateRandomString(32); + + // Store tokens in KV storage with user-specific session ID + await storage.setState(`session:${sessionId}:tokens`, { + access_token: tokenResponse.access_token, + refresh_token: tokenResponse.refresh_token || null, + id_token: tokenResponse.id_token || null, + expires_in: tokenResponse.expires_in || 3600, + timestamp: Date.now() + }); + + if (KINDE_DEBUG === 'true') { + console.log('Tokens stored successfully for session:', sessionId); + console.log('Redirecting to:', redirectUrl); + } + + // Extract user ID from tokens if available + let userId = null; + if (tokenResponse.id_token) { + try { + // Parse the ID token to get user information + const idTokenParts = tokenResponse.id_token.split('.'); + if (idTokenParts.length >= 2) { + const payload = JSON.parse(atob(idTokenParts[1])); + userId = payload.sub || null; + } + } catch (e) { + console.error('Failed to extract user ID from token:', e); + } + } + + // If we have a user ID, create a mapping for easier lookups + if (userId) { + await storage.setState(`user:${userId}:session`, sessionId); + } + + // Create a redirect response with proper headers and set session cookie + return new Response(null, { + status: 302, + headers: { + 'Location': redirectUrl, + 'Cache-Control': 'no-store', + 'Set-Cookie': `kinde_session=${sessionId}; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000` // 30 days + } + }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + console.error('Token exchange error:', errorMessage); + + // Store error for debugging + await storage.setState('token_error', { + time: new Date().toISOString(), + error: safeStringify(error) + }); + + return json({ error: 'Token exchange failed' }, { status: 500 }); + } +} + + // Get code verifier for PKCE if it exists + const codeVerifier = storedState === 'true' ? undefined : storedState; + + // Get post-login redirect URL + const redirectUrl = await storage.getState(`redirect:${state}`) || POST_LOGIN_REDIRECT_URL; + + // Clean up stored state + await storage.deleteState(state); + await storage.deleteState(`redirect:${state}`); + try { // Exchange code for tokens const tokenResponse = await fetchTokens(code, codeVerifier); @@ -444,15 +510,32 @@ async function fetchTokens(code: string, codeVerifier?: string) { // Handle logout async function handleLogout(event: RequestEvent, storage: any) { - // Clear tokens from KV storage - await storage.deleteState('tokens'); - console.log('Tokens deleted during logout'); + // Get session ID from cookie + const cookies = event.request.headers.get('cookie') || ''; + const sessionMatch = cookies.match(/kinde_session=([^;]+)/); + const sessionId = sessionMatch ? sessionMatch[1] : null; + + if (sessionId) { + // Clear tokens for this specific session + await storage.deleteState(`session:${sessionId}:tokens`); + + if (KINDE_DEBUG === 'true') { + console.log(`Tokens deleted for session: ${sessionId}`); + } + } // Redirect to Kinde's logout endpoint const logoutUrl = new URL('/logout', ISSUER_URL); logoutUrl.searchParams.append('redirect', POST_LOGOUT_REDIRECT_URL); - return redirect(302, logoutUrl.toString()); + // Create a response with a Set-Cookie header to clear the session cookie + return new Response(null, { + status: 302, + headers: { + 'Location': logoutUrl.toString(), + 'Set-Cookie': 'kinde_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0' // Clear the cookie + } + }); } // Helper function to safely stringify errors @@ -473,23 +556,101 @@ function safeStringify(obj: any): string { Create `src/routes/+layout.server.ts`: ```typescript +import { kindeAuthClient } from '@kinde-oss/kinde-auth-sveltekit'; import type { LayoutServerLoad } from './$types'; import { createKindeStorage } from '$lib/kindeCloudflareStorage'; +import { KINDE_ISSUER_URL, KINDE_CLIENT_ID, KINDE_CLIENT_SECRET } from '$env/static/private'; export const load: LayoutServerLoad = async (event) => { const storage = createKindeStorage(event); - + if (!storage) { - return { authenticated: false }; + return { authenticated: false }; } - + try { - const tokens = await storage.getState('tokens'); - return { authenticated: !!tokens?.access_token }; + // Get session ID from cookie + const cookies = event.request.headers.get('cookie') || ''; + const sessionMatch = cookies.match(/kinde_session=([^;]+)/); + const sessionId = sessionMatch ? sessionMatch[1] : null; + + if (!sessionId) { + return { authenticated: false }; + } + + // Retrieve tokens for this specific session + const tokens = await storage.getState(`session:${sessionId}:tokens`); + + if (!tokens?.access_token) { + return { authenticated: false }; + } + + // Check token expiration + const now = Date.now(); + const tokenAge = now - (tokens.timestamp || 0); + const expiresIn = tokens.expires_in || 3600; // Default to 1 hour if not specified + const tokenExpiresInMs = expiresIn * 1000; + + // If token is still valid (with 60-second buffer) + if (tokenAge < tokenExpiresInMs - 60000) { + return { authenticated: true, sessionId }; + } + + // If token is expired but we have a refresh token, try to refresh + if (tokens.refresh_token) { + try { + const refreshedTokens = await refreshTokens(tokens.refresh_token); + + // Store refreshed tokens + await storage.setState(`session:${sessionId}:tokens`, { + access_token: refreshedTokens.access_token, + refresh_token: refreshedTokens.refresh_token || tokens.refresh_token, + id_token: refreshedTokens.id_token || tokens.id_token, + expires_in: refreshedTokens.expires_in || 3600, + timestamp: Date.now() + }); + + return { authenticated: true, sessionId }; + } catch (refreshError) { + console.error('Failed to refresh token:', refreshError); + // Token refresh failed, user needs to re-authenticate + return { authenticated: false }; + } + } + + return { authenticated: false }; } catch (error) { - return { authenticated: false }; + console.error('Error checking authentication:', error); + return { authenticated: false }; } }; + +// Function to refresh tokens +async function refreshTokens(refreshToken: string) { + const tokenUrl = new URL('/oauth2/token', KINDE_ISSUER_URL); + const params = new URLSearchParams(); + + params.append('grant_type', 'refresh_token'); + params.append('refresh_token', refreshToken); + params.append('client_id', KINDE_CLIENT_ID); + params.append('client_secret', KINDE_CLIENT_SECRET); + + const response = await fetch(tokenUrl.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + body: params + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + throw new Error(`Token refresh failed: ${error.error || response.status}`); + } + + return await response.json(); +} ``` ## 9. Protect Routes From fd95df97c16443517c312bdd697367314a5e2d0a Mon Sep 17 00:00:00 2001 From: KeeganBeuthin Date: Mon, 12 May 2025 11:30:41 +0200 Subject: [PATCH 05/14] more rabbit suggestions --- .../kinde-sveltekit-with-cloudflare.mdx | 74 +++---------------- 1 file changed, 11 insertions(+), 63 deletions(-) diff --git a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx index b7a7f2026..a091aa6a3 100644 --- a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx +++ b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx @@ -221,6 +221,12 @@ function base64URLEncode(buffer: ArrayBuffer): string { async function handleLogin(event: RequestEvent, storage: any, isRegister: boolean) { // Generate state parameter const state = generateRandomString(24); + + // Get additional parameters from URL (moved earlier to avoid undefined reference) + const url = new URL(event.request.url); + const orgCode = url.searchParams.get('org_code'); + const postLoginRedirect = url.searchParams.get('post_login_redirect_url') || POST_LOGIN_REDIRECT_URL; + // For PKCE, generate code challenge let codeVerifier: string | undefined; let codeChallenge: string | undefined; @@ -233,13 +239,11 @@ async function handleLogin(event: RequestEvent, storage: any, isRegister: boolea codeChallenge = base64URLEncode(challengeBuffer); } - // Store state (and code verifier if using PKCE) -await storage.setState(state, JSON.stringify({ codeVerifier, redirect: postLoginRedirect })); + // Store only the code verifier under the state key + await storage.setState(state, codeVerifier || ''); - // Get additional parameters from URL - const url = new URL(event.request.url); - const orgCode = url.searchParams.get('org_code'); - const postLoginRedirect = url.searchParams.get('post_login_redirect_url') || POST_LOGIN_REDIRECT_URL; + // Store redirect separately + await storage.setState(`redirect:${state}`, postLoginRedirect); // Build auth URL const authUrl = new URL(isRegister ? '/oauth2/auth/register' : '/oauth2/auth', ISSUER_URL); @@ -256,9 +260,6 @@ await storage.setState(state, JSON.stringify({ codeVerifier, redirect: postLogin authUrl.searchParams.append('org_code', orgCode); } - // Store post-login redirect - await storage.setState(`redirect:${state}`, postLoginRedirect); - // Add PKCE parameters if enabled if (USE_PKCE && codeChallenge) { authUrl.searchParams.append('code_challenge', codeChallenge); @@ -377,56 +378,6 @@ async function handleCallback(event: RequestEvent, storage: any) { return json({ error: 'Token exchange failed' }, { status: 500 }); } } - - // Get code verifier for PKCE if it exists - const codeVerifier = storedState === 'true' ? undefined : storedState; - - // Get post-login redirect URL - const redirectUrl = await storage.getState(`redirect:${state}`) || POST_LOGIN_REDIRECT_URL; - - // Clean up stored state - await storage.deleteState(state); - await storage.deleteState(`redirect:${state}`); - - try { - // Exchange code for tokens - const tokenResponse = await fetchTokens(code, codeVerifier); - - // Store tokens in KV storage - await storage.setState('tokens', { - access_token: tokenResponse.access_token, - refresh_token: tokenResponse.refresh_token || null, - id_token: tokenResponse.id_token || null, - expires_in: tokenResponse.expires_in || 3600, - timestamp: Date.now() - }); - - console.log('Tokens stored successfully'); - - // Log the redirect URL for debugging - console.log('Redirecting to:', redirectUrl); - - // Create a redirect response with proper headers - return new Response(null, { - status: 302, - headers: { - 'Location': redirectUrl, - 'Cache-Control': 'no-store' - } - }); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - console.error('Token exchange error:', errorMessage); - - // Store error for debugging - await storage.setState('token_error', { - time: new Date().toISOString(), - error: safeStringify(error) - }); - - return json({ error: 'Token exchange failed' }, { status: 500 }); - } -} // Exchange authorization code for tokens async function fetchTokens(code: string, codeVerifier?: string) { @@ -443,12 +394,9 @@ async function fetchTokens(code: string, codeVerifier?: string) { 'Accept': 'application/json' }; - // The key fix: Always include client_secret with Kinde, even with PKCE if (USE_PKCE && codeVerifier && codeVerifier !== 'true') { console.log('Using PKCE flow with code verifier'); params.append('code_verifier', codeVerifier); - - // CRITICAL FIX: Kinde requires client_secret even with PKCE flow for server-side apps params.append('client_secret', SECRET); } else { console.log('Using standard authorization code flow with client secret'); @@ -670,7 +618,7 @@ export const load: PageServerLoad = async (event) => { throw redirect(302, '/login'); } - const tokens = await storage.getState('tokens'); + const tokens = await storage.getState(`session:${sessionId}:tokens`); if (!tokens?.access_token) { throw redirect(302, '/login'); From f6add0584addc390ad91185f20fec9a2ca1e25b5 Mon Sep 17 00:00:00 2001 From: KeeganBeuthin Date: Tue, 13 May 2025 11:07:35 +0200 Subject: [PATCH 06/14] further rabbit corrections --- .../kinde-sveltekit-with-cloudflare.mdx | 61 ++++++++++++------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx index a091aa6a3..a06b5138e 100644 --- a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx +++ b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx @@ -62,6 +62,7 @@ KINDE_POST_LOGOUT_REDIRECT_URL = "https://your-domain.pages.dev" KINDE_POST_LOGIN_REDIRECT_URL = "https://your-domain.pages.dev/dashboard" KINDE_AUTH_WITH_PKCE = "true" KINDE_SCOPE = 'openid profile email offline' +KINDE_DEBUG = "false" ``` ## 5. Create a KV Storage Adapter @@ -153,7 +154,7 @@ Create `src/routes/api/auth/[...kindeAuth]/+server.ts`: import { json, redirect } from '@sveltejs/kit'; import type { RequestEvent } from "@sveltejs/kit"; import { createKindeStorage } from '$lib/kindeCloudflareStorage'; -import { KINDE_ISSUER_URL, KINDE_CLIENT_ID, KINDE_CLIENT_SECRET, KINDE_REDIRECT_URL, KINDE_POST_LOGIN_REDIRECT_URL, KINDE_POST_LOGOUT_REDIRECT_URL, KINDE_AUTH_WITH_PKCE } from '$env/static/private'; +import { KINDE_ISSUER_URL, KINDE_CLIENT_ID, KINDE_CLIENT_SECRET, KINDE_REDIRECT_URL, KINDE_POST_LOGIN_REDIRECT_URL, KINDE_POST_LOGOUT_REDIRECT_URL, KINDE_AUTH_WITH_PKCE,KINDE_SCOPE } from '$env/static/private'; // Get environment variables const SECRET = KINDE_CLIENT_SECRET; @@ -162,7 +163,7 @@ const CLIENT_ID = KINDE_CLIENT_ID; const REDIRECT_URL = KINDE_REDIRECT_URL; const POST_LOGIN_REDIRECT_URL = KINDE_POST_LOGIN_REDIRECT_URL; const POST_LOGOUT_REDIRECT_URL = KINDE_POST_LOGOUT_REDIRECT_URL; -const SCOPE = 'openid profile email offline' +const SCOPE = KINDE_SCOPE const USE_PKCE = KINDE_AUTH_WITH_PKCE === 'true'; export async function GET(event: RequestEvent) { @@ -239,8 +240,9 @@ async function handleLogin(event: RequestEvent, storage: any, isRegister: boolea codeChallenge = base64URLEncode(challengeBuffer); } - // Store only the code verifier under the state key - await storage.setState(state, codeVerifier || ''); +if (USE_PKCE && codeVerifier) { + await storage.setState(state, codeVerifier); + } // Store redirect separately await storage.setState(`redirect:${state}`, postLoginRedirect); @@ -305,8 +307,9 @@ async function handleCallback(event: RequestEvent, storage: any) { return json({ error: 'Invalid state parameter' }, { status: 401 }); } - // Get code verifier for PKCE if it exists - const codeVerifier = storedState === 'true' ? undefined : storedState; +const codeVerifier = typeof storedState === 'string' && storedState.length > 0 + ? storedState + : undefined; // Get post-login redirect URL const redirectUrl = await storage.getState(`redirect:${state}`) || POST_LOGIN_REDIRECT_URL; @@ -330,11 +333,7 @@ async function handleCallback(event: RequestEvent, storage: any) { expires_in: tokenResponse.expires_in || 3600, timestamp: Date.now() }); - - if (KINDE_DEBUG === 'true') { - console.log('Tokens stored successfully for session:', sessionId); - console.log('Redirecting to:', redirectUrl); - } + // Extract user ID from tokens if available let userId = null; @@ -464,12 +463,7 @@ async function handleLogout(event: RequestEvent, storage: any) { const sessionId = sessionMatch ? sessionMatch[1] : null; if (sessionId) { - // Clear tokens for this specific session await storage.deleteState(`session:${sessionId}:tokens`); - - if (KINDE_DEBUG === 'true') { - console.log(`Tokens deleted for session: ${sessionId}`); - } } // Redirect to Kinde's logout endpoint @@ -615,17 +609,40 @@ export const load: PageServerLoad = async (event) => { const storage = createKindeStorage(event); if (!storage) { - throw redirect(302, '/login'); + throw redirect(302, '/api/auth/login'); } - - const tokens = await storage.getState(`session:${sessionId}:tokens`); - + + const cookies = event.request.headers.get('cookie') || ''; + const sessionMatch = cookies.match(/kinde_session=([^;]+)/); + const sessionId = sessionMatch ? sessionMatch[1] : null; + + if (!sessionId) { + throw redirect(302, '/api/auth/login'); + } + + // Now we can safely access the tokens with the sessionId + const tokens = await storage.getState(`session:${sessionId}:tokens`); + if (!tokens?.access_token) { - throw redirect(302, '/login'); + throw redirect(302, '/api/auth/login'); } - return { authenticated: true }; + return { + authenticated: true, + userId: tokens.id_token ? getUserIdFromToken(tokens.id_token) : null + }; }; + +// Helper function to get user ID from ID token +function getUserIdFromToken(idToken) { + try { + const payload = JSON.parse(atob(idToken.split('.')[1])); + return payload.sub; + } catch (e) { + console.error('Failed to extract user ID from token'); + return null; + } +} ``` Your Kinde authentication is now working with SvelteKit on Cloudflare Pages! From f86ff6956c92ac6517ee6cde30ea11dbb762d8e9 Mon Sep 17 00:00:00 2001 From: KeeganBeuthin Date: Tue, 13 May 2025 11:17:15 +0200 Subject: [PATCH 07/14] removed logging --- .../kinde-sveltekit-with-cloudflare.mdx | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx index a06b5138e..725451c7e 100644 --- a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx +++ b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx @@ -267,9 +267,7 @@ if (USE_PKCE && codeVerifier) { authUrl.searchParams.append('code_challenge', codeChallenge); authUrl.searchParams.append('code_challenge_method', 'S256'); } - - // Redirect to Kinde auth URL - console.log(`Redirecting to Kinde auth: ${authUrl.toString()}`); + return redirect(302, authUrl.toString()); } @@ -361,7 +359,7 @@ const codeVerifier = typeof storedState === 'string' && storedState.length > 0 headers: { 'Location': redirectUrl, 'Cache-Control': 'no-store', - 'Set-Cookie': `kinde_session=${sessionId}; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000` // 30 days + 'Set-Cookie': `kinde_session=${sessionId}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=2592000` } }); } catch (error) { @@ -394,19 +392,12 @@ async function fetchTokens(code: string, codeVerifier?: string) { }; if (USE_PKCE && codeVerifier && codeVerifier !== 'true') { - console.log('Using PKCE flow with code verifier'); params.append('code_verifier', codeVerifier); params.append('client_secret', SECRET); } else { - console.log('Using standard authorization code flow with client secret'); params.append('client_secret', SECRET); } - console.log('Token exchange request:', { - url: tokenUrl.toString(), - isPKCE: USE_PKCE && codeVerifier && codeVerifier !== 'true', - hasClientSecret: true - }); try { const response = await fetch(tokenUrl.toString(), { @@ -416,13 +407,11 @@ async function fetchTokens(code: string, codeVerifier?: string) { }); const responseText = await response.text(); - console.log('Token response status:', response.status); // Try to parse the response as JSON let responseData; try { responseData = JSON.parse(responseText); - console.log('Token response parsed successfully'); } catch (parseError) { console.error('Failed to parse token response as JSON:', responseText); throw new Error(`Invalid JSON response: ${responseText}`); @@ -445,9 +434,6 @@ async function fetchTokens(code: string, codeVerifier?: string) { throw new Error('Token response missing required fields'); } - // Log success - console.log('Token exchange successful'); - return responseData; } catch (error) { console.error('Token exchange error:', error instanceof Error ? error.message : 'Unknown error'); From 10e3e756c2fd3bf2759d906f0bf17fbc85419b96 Mon Sep 17 00:00:00 2001 From: KeeganBeuthin Date: Tue, 13 May 2025 11:40:56 +0200 Subject: [PATCH 08/14] removed secret from wrangler declaration --- .../third-party-tools/kinde-sveltekit-with-cloudflare.mdx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx index 725451c7e..e848521c0 100644 --- a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx +++ b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx @@ -56,15 +56,18 @@ kv_namespaces = [ [vars] KINDE_ISSUER_URL = "https://your-kinde-domain.kinde.com" KINDE_CLIENT_ID = "your-client-id" -KINDE_CLIENT_SECRET = "your-client-secret" KINDE_REDIRECT_URL = "https://your-domain.pages.dev/api/auth/kinde_callback" KINDE_POST_LOGOUT_REDIRECT_URL = "https://your-domain.pages.dev" KINDE_POST_LOGIN_REDIRECT_URL = "https://your-domain.pages.dev/dashboard" KINDE_AUTH_WITH_PKCE = "true" -KINDE_SCOPE = 'openid profile email offline' +KINDE_SCOPE = "openid profile email offline" KINDE_DEBUG = "false" ``` +## 4.1 Add KINDE_CLIENT_SECRET Separately from a .dev.vars file to avoid leaking your secret. + +npx wrangler secret put KINDE_CLIENT_SECRET + ## 5. Create a KV Storage Adapter Create `src/lib/kindeCloudflareStorage.ts`: From 95fed3662c78f52a5ef80ce0273a91b74420127d Mon Sep 17 00:00:00 2001 From: KeeganBeuthin Date: Tue, 13 May 2025 11:46:44 +0200 Subject: [PATCH 09/14] prefixes error cookie --- .../third-party-tools/kinde-sveltekit-with-cloudflare.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx index e848521c0..7d551e2fc 100644 --- a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx +++ b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx @@ -298,8 +298,8 @@ async function handleCallback(event: RequestEvent, storage: any) { if (!storedState) { console.error('State not found:', state); - // Store error for debugging - await storage.setState('last_error', { + // Store error for debugging with namespaced key to avoid collisions + await storage.setState(`error:${state}`, { time: new Date().toISOString(), error: 'State not found', state @@ -464,7 +464,7 @@ async function handleLogout(event: RequestEvent, storage: any) { status: 302, headers: { 'Location': logoutUrl.toString(), - 'Set-Cookie': 'kinde_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0' // Clear the cookie + 'Set-Cookie': 'kinde_session=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0' // Clear the cookie } }); } From 4141782957648c0b03b07c42074c8542f60c20fb Mon Sep 17 00:00:00 2001 From: ClaireM <127452294+clairekinde11@users.noreply.github.com> Date: Tue, 20 May 2025 15:20:12 +1000 Subject: [PATCH 10/14] Update kinde-sveltekit-with-cloudflare.mdx A few heading fixes and punctuation updates. --- .../kinde-sveltekit-with-cloudflare.mdx | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx index 7d551e2fc..91ac74c12 100644 --- a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx +++ b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx @@ -16,17 +16,17 @@ This guide walks you through implementing Kinde authentication in a SvelteKit ap - A Kinde account (register at [kinde.com](https://kinde.com)) - SvelteKit project ready for Cloudflare Pages deployment -## 1. Set Up a Kinde Application +## 1. Set Up a Kinde application -1. Log in to your Kinde account -2. Go to **Settings > Applications** -3. Create a new Sveltekit application +1. Sign in to your Kinde account. +2. Go to **Settings > Applications**. +3. Create a new Sveltekit application. 4. Configure the callback URLs: - Allowed callback URL: `https://your-domain.pages.dev/api/auth/kinde_callback` - Allowed logout redirect URL: `https://your-domain.pages.dev` -5. Note your Client ID and Client Secret +5. Note your **Client ID** and **Client Secret**. -## 2. Install Required Dependencies +## 2. Install required dependencies ```bash npm install @kinde-oss/kinde-auth-sveltekit @@ -35,11 +35,11 @@ npm install -D @sveltejs/adapter-cloudflare ## 3. Configure Cloudflare KV Storage -1. Go to **Workers & Pages > KV** -2. Create a new namespace (e.g., `AUTH_STORAGE`) -3. Copy the namespace ID +1. In Cloudflare, go to **Workers & Pages > KV**. +2. Create a new namespace (e.g., `AUTH_STORAGE`). +3. Copy the namespace ID. -## 4. Set Up Wrangler Configuration +## 4. Set Up Wrangler configuration Update your `wrangler.toml`: @@ -129,7 +129,7 @@ export const createKindeStorage = (event: RequestEvent) => { }; ``` -## 6. Set Up SvelteKit Hooks +## 6. Set up SvelteKit Hooks Update `src/hooks.server.ts`: @@ -149,7 +149,7 @@ export const handle: Handler = async ({ event, resolve }) => { }; ``` -## 7. Implement Authentication Routes +## 7. Implement authentication routes Create `src/routes/api/auth/[...kindeAuth]/+server.ts`: @@ -584,7 +584,7 @@ async function refreshTokens(refreshToken: string) { } ``` -## 9. Protect Routes +## 9. Protect routes For protected routes like a dashboard: @@ -634,5 +634,5 @@ function getUserIdFromToken(idToken) { } ``` -Your Kinde authentication is now working with SvelteKit on Cloudflare Pages! +Your Kinde authentication should now be working with SvelteKit on Cloudflare Pages. From 1cb94d93361f28a36cc72b23e81a65e90903ef58 Mon Sep 17 00:00:00 2001 From: KeeganBeuthin Date: Tue, 20 May 2025 10:23:12 +0200 Subject: [PATCH 11/14] claire suggested changes --- .../kinde-sveltekit-with-cloudflare.mdx | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx index 91ac74c12..5e0da2ba2 100644 --- a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx +++ b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx @@ -1,22 +1,31 @@ --- page_id: 4f3a9e2a-12eb-4b4c-8790-48b6e09a224d -title: "Integrating Kinde Authentication with SvelteKit on Cloudflare Pages" -description: "A step-by-step guide to implement Kinde authentication in a SvelteKit application deployed to Cloudflare Pages" +title: Kinde with Sveltekit on Cloudflare Pages +sidebar: + order: 9 +relatedArticles: + - 855e5ca8-f2fb-4162-a594-10cee8a2ff8b + - f1ba22b9-b35f-478a-be09-4524d060fe36 + - 00d62179-e0e8-489c-90f7-9a593f3b058a --- This guide walks you through implementing Kinde authentication in a SvelteKit application deployed to Cloudflare Pages using KV storage for state management. -> Note: This implementation is more complex than ideal due to Cloudflare's serverless environment constraints. We're working on more elegant solutions, but this guide provides a robust working implementation. + + +## What you need - A Cloudflare account - A Kinde account (register at [kinde.com](https://kinde.com)) - SvelteKit project ready for Cloudflare Pages deployment -## 1. Set Up a Kinde application +## Step 1: Set Up a Kinde application 1. Sign in to your Kinde account. 2. Go to **Settings > Applications**. @@ -26,20 +35,20 @@ This guide walks you through implementing Kinde authentication in a SvelteKit ap - Allowed logout redirect URL: `https://your-domain.pages.dev` 5. Note your **Client ID** and **Client Secret**. -## 2. Install required dependencies +## Step 2: Install required dependencies ```bash npm install @kinde-oss/kinde-auth-sveltekit npm install -D @sveltejs/adapter-cloudflare ``` -## 3. Configure Cloudflare KV Storage +## Step 3: Configure Cloudflare KV Storage 1. In Cloudflare, go to **Workers & Pages > KV**. 2. Create a new namespace (e.g., `AUTH_STORAGE`). 3. Copy the namespace ID. -## 4. Set Up Wrangler configuration +## Step 4: Set Up Wrangler configuration Update your `wrangler.toml`: @@ -64,11 +73,11 @@ KINDE_SCOPE = "openid profile email offline" KINDE_DEBUG = "false" ``` -## 4.1 Add KINDE_CLIENT_SECRET Separately from a .dev.vars file to avoid leaking your secret. +### Add KINDE_CLIENT_SECRET separately from a .dev.vars file to avoid leaking your secret. npx wrangler secret put KINDE_CLIENT_SECRET -## 5. Create a KV Storage Adapter +## Step 5: Create a KV Storage Adapter Create `src/lib/kindeCloudflareStorage.ts`: @@ -129,7 +138,7 @@ export const createKindeStorage = (event: RequestEvent) => { }; ``` -## 6. Set up SvelteKit Hooks +## Step 6: Set up SvelteKit hooks Update `src/hooks.server.ts`: @@ -149,7 +158,7 @@ export const handle: Handler = async ({ event, resolve }) => { }; ``` -## 7. Implement authentication routes +## Step 7: Implement authentication routes Create `src/routes/api/auth/[...kindeAuth]/+server.ts`: @@ -482,7 +491,7 @@ function safeStringify(obj: any): string { } } ``` -## 8. Check Authentication Status +## Step 8: Check authentication status Create `src/routes/+layout.server.ts`: @@ -584,7 +593,7 @@ async function refreshTokens(refreshToken: string) { } ``` -## 9. Protect routes +## Step 9: Protect routes For protected routes like a dashboard: From ca5d150bdbf680601bc89868fe0b7ed5db5c9e35 Mon Sep 17 00:00:00 2001 From: KeeganBeuthin Date: Tue, 20 May 2025 11:19:30 +0200 Subject: [PATCH 12/14] fixed Kinde_Client_secret syntax --- .../third-party-tools/kinde-sveltekit-with-cloudflare.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx index 5e0da2ba2..f4908ad64 100644 --- a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx +++ b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx @@ -74,9 +74,9 @@ KINDE_DEBUG = "false" ``` ### Add KINDE_CLIENT_SECRET separately from a .dev.vars file to avoid leaking your secret. - +```bash npx wrangler secret put KINDE_CLIENT_SECRET - +``` ## Step 5: Create a KV Storage Adapter Create `src/lib/kindeCloudflareStorage.ts`: From e359991386dfca81222b1871b0cf28e12c695654 Mon Sep 17 00:00:00 2001 From: KeeganBeuthin Date: Thu, 26 Jun 2025 10:14:29 +0200 Subject: [PATCH 13/14] large changes to doc --- .../kinde-sveltekit-with-cloudflare.mdx | 716 +++++------------- 1 file changed, 205 insertions(+), 511 deletions(-) diff --git a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx index f4908ad64..c2dface33 100644 --- a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx +++ b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx @@ -1,6 +1,6 @@ --- page_id: 4f3a9e2a-12eb-4b4c-8790-48b6e09a224d -title: Kinde with Sveltekit on Cloudflare Pages +title: Kinde with SvelteKit on Cloudflare Pages sidebar: order: 9 relatedArticles: @@ -9,46 +9,38 @@ relatedArticles: - 00d62179-e0e8-489c-90f7-9a593f3b058a --- +# Kinde with SvelteKit on Cloudflare Pages -This guide walks you through implementing Kinde authentication in a SvelteKit application deployed to Cloudflare Pages using KV storage for state management. - +This guide walks you through implementing Kinde authentication in a SvelteKit application deployed to Cloudflare Pages using js-utils with KV storage. -This implementation is more complex than ideal due to Cloudflare's serverless environment constraints. We're working on more elegant solutions, but this guide provides a robust working implementation. - + ## What you need -- A Cloudflare account -- A Kinde account (register at [kinde.com](https://kinde.com)) +- A Cloudflare account with Pages and KV access +- A Kinde account with an SPA application configured - SvelteKit project ready for Cloudflare Pages deployment -## Step 1: Set Up a Kinde application - -1. Sign in to your Kinde account. -2. Go to **Settings > Applications**. -3. Create a new Sveltekit application. -4. Configure the callback URLs: - - Allowed callback URL: `https://your-domain.pages.dev/api/auth/kinde_callback` - - Allowed logout redirect URL: `https://your-domain.pages.dev` -5. Note your **Client ID** and **Client Secret**. - -## Step 2: Install required dependencies +## Step 1: Install dependencies ```bash -npm install @kinde-oss/kinde-auth-sveltekit +npm install @kinde/js-utils npm install -D @sveltejs/adapter-cloudflare ``` -## Step 3: Configure Cloudflare KV Storage +## Step 2: Configure Cloudflare KV storage -1. In Cloudflare, go to **Workers & Pages > KV**. -2. Create a new namespace (e.g., `AUTH_STORAGE`). -3. Copy the namespace ID. +1. In Cloudflare dashboard, go to **Workers & Pages > KV** +2. Create a new namespace (e.g., `AUTH_TOKENS`) +3. Copy the namespace ID -## Step 4: Set Up Wrangler configuration +## Step 3: Configure environment variables Update your `wrangler.toml`: @@ -56,7 +48,7 @@ Update your `wrangler.toml`: name = "your-project-name" compatibility_date = "2023-06-28" compatibility_flags = ["nodejs_compat_v2"] -pages_build_output_dir = "./svelte-kit/cloudflare" +pages_build_output_dir = "./build" kv_namespaces = [ { binding = "AUTH_STORAGE", id = "your-namespace-id" } @@ -66,582 +58,284 @@ kv_namespaces = [ KINDE_ISSUER_URL = "https://your-kinde-domain.kinde.com" KINDE_CLIENT_ID = "your-client-id" KINDE_REDIRECT_URL = "https://your-domain.pages.dev/api/auth/kinde_callback" -KINDE_POST_LOGOUT_REDIRECT_URL = "https://your-domain.pages.dev" KINDE_POST_LOGIN_REDIRECT_URL = "https://your-domain.pages.dev/dashboard" +KINDE_POST_LOGOUT_REDIRECT_URL = "https://your-domain.pages.dev" KINDE_AUTH_WITH_PKCE = "true" KINDE_SCOPE = "openid profile email offline" KINDE_DEBUG = "false" ``` -### Add KINDE_CLIENT_SECRET separately from a .dev.vars file to avoid leaking your secret. +Add your client secret securely: + ```bash npx wrangler secret put KINDE_CLIENT_SECRET ``` -## Step 5: Create a KV Storage Adapter -Create `src/lib/kindeCloudflareStorage.ts`: +## Step 4: Create hybrid storage adapter + +Create `src/lib/kindeAuth.ts`: ```typescript +import { + KvStorage, + setActiveStorage, + setInsecureStorage, + type SessionManager +} from '@kinde/js-utils'; import type { RequestEvent } from '@sveltejs/kit'; -export const createKindeStorage = (event: RequestEvent) => { +/** + * Cookie storage for temporary OAuth data (state, nonce, code verifier) + */ +class CookieStorage implements SessionManager { + constructor(private event: RequestEvent) {} + + async setSessionItem(key: string, value: unknown): Promise { + const cookieValue = typeof value === 'string' ? value : JSON.stringify(value); + this.event.cookies.set(`kinde_${key}`, cookieValue, { + path: '/', + maxAge: 3600, + httpOnly: true, + secure: true, + sameSite: 'lax' + }); + } + + async getSessionItem(key: string): Promise { + const value = this.event.cookies.get(`kinde_${key}`); + if (!value) return null; + + try { + return JSON.parse(value); + } catch { + return value; + } + } + + async removeSessionItem(key: string): Promise { + this.event.cookies.delete(`kinde_${key}`, { path: '/' }); + } + + async removeItems(keys: string[]): Promise { + for (const key of keys) { + await this.removeSessionItem(key); + } + } + + async clearSession(): Promise { + const cookiesToClear = ['state', 'nonce', 'codeVerifier']; + for (const key of cookiesToClear) { + this.event.cookies.delete(`kinde_${key}`, { path: '/' }); + } + } +} + +/** + * Initialize Kinde authentication with hybrid storage: + * - KV storage for tokens (eventual consistency is fine) + * - Cookie storage for OAuth temp data (immediate consistency required) + */ +export function initializeKindeAuth(event: RequestEvent): boolean { const platform = event.platform as any; const env = platform?.env; const AUTH_STORAGE = env?.AUTH_STORAGE; - + if (!AUTH_STORAGE) { - return null; + console.error('KV storage not available'); + return false; } - - return { - setState: async (stateId: string, stateData: any) => { - try { - const key = `kinde:state:${stateId}`; - const value = typeof stateData === 'string' - ? stateData - : JSON.stringify(stateData); - - await AUTH_STORAGE.put(key, value, { expirationTtl: 600 }); - return true; - } catch (error) { - return false; - } - }, - - getState: async (stateId: string) => { - try { - const key = `kinde:state:${stateId}`; - const value = await AUTH_STORAGE.get(key); - - if (!value) return null; - - try { - return JSON.parse(value); - } catch { - return value; - } - } catch (error) { - return null; - } - }, - - deleteState: async (stateId: string) => { - try { - const key = `kinde:state:${stateId}`; - await AUTH_STORAGE.delete(key); - return true; - } catch (error) { - return false; - } - } - }; -}; -``` -## Step 6: Set up SvelteKit hooks + // KV storage for long-term token storage + const tokenStorage = new KvStorage(AUTH_STORAGE, { defaultTtl: 2592000 }); -Update `src/hooks.server.ts`: + // Cookie storage for temporary OAuth data + const tempStorage = new CookieStorage(event); -```typescript -import { sessionHooks, type Handler } from '@kinde-oss/kinde-auth-sveltekit'; -import { createKindeStorage } from '$lib/kindeCloudflareStorage'; - -export const handle: Handler = async ({ event, resolve }) => { - const storage = createKindeStorage(event); - - sessionHooks({ - event, - ...(storage ? { storage } : {}) - }); - - return await resolve(event); -}; + // Set up js-utils storage + setActiveStorage(tokenStorage); // For tokens + setInsecureStorage(tempStorage); // For OAuth temp data + + return true; +} ``` -## Step 7: Implement authentication routes +## Step 5: Create authentication endpoints Create `src/routes/api/auth/[...kindeAuth]/+server.ts`: ```typescript import { json, redirect } from '@sveltejs/kit'; import type { RequestEvent } from "@sveltejs/kit"; -import { createKindeStorage } from '$lib/kindeCloudflareStorage'; -import { KINDE_ISSUER_URL, KINDE_CLIENT_ID, KINDE_CLIENT_SECRET, KINDE_REDIRECT_URL, KINDE_POST_LOGIN_REDIRECT_URL, KINDE_POST_LOGOUT_REDIRECT_URL, KINDE_AUTH_WITH_PKCE,KINDE_SCOPE } from '$env/static/private'; - -// Get environment variables -const SECRET = KINDE_CLIENT_SECRET; -const ISSUER_URL = KINDE_ISSUER_URL; -const CLIENT_ID = KINDE_CLIENT_ID; -const REDIRECT_URL = KINDE_REDIRECT_URL; -const POST_LOGIN_REDIRECT_URL = KINDE_POST_LOGIN_REDIRECT_URL; -const POST_LOGOUT_REDIRECT_URL = KINDE_POST_LOGOUT_REDIRECT_URL; -const SCOPE = KINDE_SCOPE -const USE_PKCE = KINDE_AUTH_WITH_PKCE === 'true'; +import { + generateAuthUrl, + exchangeAuthCode, + frameworkSettings, + IssuerRouteTypes, + Scopes, + type LoginOptions +} from '@kinde/js-utils'; +import { initializeKindeAuth } from '$lib/kindeAuth'; +import { + KINDE_ISSUER_URL, + KINDE_CLIENT_ID, + KINDE_REDIRECT_URL, + KINDE_POST_LOGIN_REDIRECT_URL, + KINDE_POST_LOGOUT_REDIRECT_URL, + KINDE_DEBUG +} from '$env/static/private'; + +// Configure js-utils +frameworkSettings.framework = 'sveltekit'; + +const getConfig = () => ({ + issuerURL: KINDE_ISSUER_URL, + clientID: KINDE_CLIENT_ID, + redirectURL: KINDE_REDIRECT_URL, + postLoginRedirectURL: KINDE_POST_LOGIN_REDIRECT_URL, + postLogoutRedirectURL: KINDE_POST_LOGOUT_REDIRECT_URL, + debug: KINDE_DEBUG === 'true' +}); export async function GET(event: RequestEvent) { - const storage = createKindeStorage(event); - const url = new URL(event.request.url); - const path = url.pathname.split('/').pop() || ''; - - - if (!storage) { - console.error('KV storage not available'); + // Initialize storage for every request + if (!initializeKindeAuth(event)) { return json({ error: 'KV storage not available' }, { status: 500 }); } + + const url = new URL(event.request.url); + const path = url.pathname.split('/').pop() || ''; + const config = getConfig(); - // Handle various auth endpoints switch (path) { case 'login': - return handleLogin(event, storage, false); + return handleLogin(event, config, { isRegister: false }); case 'register': - return handleLogin(event, storage, true); + return handleLogin(event, config, { isRegister: true }); case 'kinde_callback': - return handleCallback(event, storage); + return handleCallback(event, config); case 'logout': - return handleLogout(event, storage); + return handleLogout(config); default: return json({ error: 'Unknown auth endpoint' }, { status: 404 }); } } -// Generate crypto-secure random string for state -function generateRandomString(length = 32) { - const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - const array = new Uint8Array(length); - crypto.getRandomValues(array); - return Array.from(array, b => possible.charAt(b % possible.length)).join(''); -} - -// Add this at the top of your file -async function sha256(plain: string): Promise { - const encoder = new TextEncoder(); - const data = encoder.encode(plain); - return await crypto.subtle.digest('SHA-256', data); -} - -function base64URLEncode(buffer: ArrayBuffer): string { - return btoa(String.fromCharCode(...new Uint8Array(buffer))) - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=+$/, ''); -} - -// Handle login or registration -async function handleLogin(event: RequestEvent, storage: any, isRegister: boolean) { - // Generate state parameter - const state = generateRandomString(24); - - // Get additional parameters from URL (moved earlier to avoid undefined reference) +async function handleLogin( + event: RequestEvent, + config: ReturnType, + options: { isRegister: boolean } +) { const url = new URL(event.request.url); const orgCode = url.searchParams.get('org_code'); - const postLoginRedirect = url.searchParams.get('post_login_redirect_url') || POST_LOGIN_REDIRECT_URL; - - // For PKCE, generate code challenge - let codeVerifier: string | undefined; - let codeChallenge: string | undefined; - - if (USE_PKCE) { - codeVerifier = generateRandomString(64); - - // Create proper code challenge with SHA-256 - const challengeBuffer = await sha256(codeVerifier); - codeChallenge = base64URLEncode(challengeBuffer); - } - -if (USE_PKCE && codeVerifier) { - await storage.setState(state, codeVerifier); - } - - // Store redirect separately - await storage.setState(`redirect:${state}`, postLoginRedirect); - - // Build auth URL - const authUrl = new URL(isRegister ? '/oauth2/auth/register' : '/oauth2/auth', ISSUER_URL); - - // Add standard OAuth parameters - authUrl.searchParams.append('client_id', CLIENT_ID); - authUrl.searchParams.append('redirect_uri', REDIRECT_URL); - authUrl.searchParams.append('response_type', 'code'); - authUrl.searchParams.append('scope', SCOPE); - authUrl.searchParams.append('state', state); - - // Add optional parameters - if (orgCode) { - authUrl.searchParams.append('org_code', orgCode); - } - // Add PKCE parameters if enabled - if (USE_PKCE && codeChallenge) { - authUrl.searchParams.append('code_challenge', codeChallenge); - authUrl.searchParams.append('code_challenge_method', 'S256'); - } + const loginOptions: LoginOptions = { + issuerRouteType: options.isRegister ? IssuerRouteTypes.register : IssuerRouteTypes.login, + scopes: [Scopes.openid, Scopes.profile, Scopes.email, Scopes.offline], + ...(orgCode && { orgCode }) + }; - return redirect(302, authUrl.toString()); + // Let js-utils handle the complete auth URL generation and state management + const authUrl = await generateAuthUrl(loginOptions); + return redirect(302, authUrl); } -// Handle OAuth callback -async function handleCallback(event: RequestEvent, storage: any) { +async function handleCallback(event: RequestEvent, config: ReturnType) { const url = new URL(event.request.url); - const code = url.searchParams.get('code'); - const state = url.searchParams.get('state'); const error = url.searchParams.get('error'); - // Check for OAuth errors if (error) { - console.error('OAuth error:', error); return json({ error: `OAuth error: ${error}` }, { status: 400 }); } - - // Validate required parameters - if (!code || !state) { - console.error('Missing code or state parameter'); - return json({ error: 'Missing code or state parameter' }, { status: 400 }); - } - - // Verify state parameter - const storedState = await storage.getState(state); - if (!storedState) { - console.error('State not found:', state); - - // Store error for debugging with namespaced key to avoid collisions - await storage.setState(`error:${state}`, { - time: new Date().toISOString(), - error: 'State not found', - state - }); - - return json({ error: 'Invalid state parameter' }, { status: 401 }); - } - -const codeVerifier = typeof storedState === 'string' && storedState.length > 0 - ? storedState - : undefined; - - // Get post-login redirect URL - const redirectUrl = await storage.getState(`redirect:${state}`) || POST_LOGIN_REDIRECT_URL; - - // Clean up stored state - await storage.deleteState(state); - await storage.deleteState(`redirect:${state}`); - - try { - // Exchange code for tokens - const tokenResponse = await fetchTokens(code, codeVerifier); - - // Generate a unique session ID for this user - const sessionId = generateRandomString(32); - - // Store tokens in KV storage with user-specific session ID - await storage.setState(`session:${sessionId}:tokens`, { - access_token: tokenResponse.access_token, - refresh_token: tokenResponse.refresh_token || null, - id_token: tokenResponse.id_token || null, - expires_in: tokenResponse.expires_in || 3600, - timestamp: Date.now() - }); - - - // Extract user ID from tokens if available - let userId = null; - if (tokenResponse.id_token) { - try { - // Parse the ID token to get user information - const idTokenParts = tokenResponse.id_token.split('.'); - if (idTokenParts.length >= 2) { - const payload = JSON.parse(atob(idTokenParts[1])); - userId = payload.sub || null; - } - } catch (e) { - console.error('Failed to extract user ID from token:', e); - } - } - - // If we have a user ID, create a mapping for easier lookups - if (userId) { - await storage.setState(`user:${userId}:session`, sessionId); - } - - // Create a redirect response with proper headers and set session cookie - return new Response(null, { - status: 302, - headers: { - 'Location': redirectUrl, - 'Cache-Control': 'no-store', - 'Set-Cookie': `kinde_session=${sessionId}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=2592000` - } - }); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - console.error('Token exchange error:', errorMessage); - - // Store error for debugging - await storage.setState('token_error', { - time: new Date().toISOString(), - error: safeStringify(error) - }); - - return json({ error: 'Token exchange failed' }, { status: 500 }); - } -} -// Exchange authorization code for tokens -async function fetchTokens(code: string, codeVerifier?: string) { - const tokenUrl = new URL('/oauth2/token', ISSUER_URL); - const params = new URLSearchParams(); - - params.append('grant_type', 'authorization_code'); - params.append('code', code); - params.append('redirect_uri', REDIRECT_URL); - params.append('client_id', CLIENT_ID); - - const headers: Record = { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': 'application/json' - }; - - if (USE_PKCE && codeVerifier && codeVerifier !== 'true') { - params.append('code_verifier', codeVerifier); - params.append('client_secret', SECRET); - } else { - params.append('client_secret', SECRET); - } - - try { - const response = await fetch(tokenUrl.toString(), { - method: 'POST', - headers, - body: params + // Let js-utils handle the complete token exchange + await exchangeAuthCode({ + urlParams: url.searchParams, + domain: config.issuerURL, + clientId: config.clientID, + clientSecret: '', // Not needed for PKCE flow + redirectUri: config.redirectURL }); - - const responseText = await response.text(); - - // Try to parse the response as JSON - let responseData; - try { - responseData = JSON.parse(responseText); - } catch (parseError) { - console.error('Failed to parse token response as JSON:', responseText); - throw new Error(`Invalid JSON response: ${responseText}`); - } - - // Check for errors in the response - if (!response.ok) { - console.error('Token exchange error details:', { - status: response.status, - error: responseData.error, - description: responseData.error_description - }); - - throw new Error(`Token exchange failed: ${response.status} - ${responseData.error}: ${responseData.error_description}`); - } - - // Check if the response has the expected tokens - if (!responseData.access_token) { - console.error('Token response missing access_token:', responseData); - throw new Error('Token response missing required fields'); - } - - return responseData; - } catch (error) { - console.error('Token exchange error:', error instanceof Error ? error.message : 'Unknown error'); - throw error; - } -} -// Handle logout -async function handleLogout(event: RequestEvent, storage: any) { - // Get session ID from cookie - const cookies = event.request.headers.get('cookie') || ''; - const sessionMatch = cookies.match(/kinde_session=([^;]+)/); - const sessionId = sessionMatch ? sessionMatch[1] : null; - - if (sessionId) { - await storage.deleteState(`session:${sessionId}:tokens`); - } - - // Redirect to Kinde's logout endpoint - const logoutUrl = new URL('/logout', ISSUER_URL); - logoutUrl.searchParams.append('redirect', POST_LOGOUT_REDIRECT_URL); - - // Create a response with a Set-Cookie header to clear the session cookie - return new Response(null, { - status: 302, - headers: { - 'Location': logoutUrl.toString(), - 'Set-Cookie': 'kinde_session=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0' // Clear the cookie - } - }); -} + return redirect(302, config.postLoginRedirectURL); -// Helper function to safely stringify errors -function safeStringify(obj: any): string { - try { - if (obj instanceof Error) { - return obj.message + (obj.stack ? `\n${obj.stack}` : ''); + } catch (error) { + // Handle expected window error in server environment + if (error instanceof Error && error.message.includes('window')) { + // Wait for async token storage to complete + await new Promise(resolve => setTimeout(resolve, 2000)); + return redirect(302, config.postLoginRedirectURL); } - - return JSON.stringify(obj); - } catch (e) { - return `[Unstringifiable object: ${typeof obj}]`; - } -} -``` -## Step 8: Check authentication status - -Create `src/routes/+layout.server.ts`: - -```typescript -import { kindeAuthClient } from '@kinde-oss/kinde-auth-sveltekit'; -import type { LayoutServerLoad } from './$types'; -import { createKindeStorage } from '$lib/kindeCloudflareStorage'; -import { KINDE_ISSUER_URL, KINDE_CLIENT_ID, KINDE_CLIENT_SECRET } from '$env/static/private'; -export const load: LayoutServerLoad = async (event) => { - const storage = createKindeStorage(event); - - if (!storage) { - return { authenticated: false }; + console.error('Authentication error:', error); + return json({ error: 'Authentication failed' }, { status: 500 }); } - - try { - // Get session ID from cookie - const cookies = event.request.headers.get('cookie') || ''; - const sessionMatch = cookies.match(/kinde_session=([^;]+)/); - const sessionId = sessionMatch ? sessionMatch[1] : null; - - if (!sessionId) { - return { authenticated: false }; - } - - // Retrieve tokens for this specific session - const tokens = await storage.getState(`session:${sessionId}:tokens`); - - if (!tokens?.access_token) { - return { authenticated: false }; - } - - // Check token expiration - const now = Date.now(); - const tokenAge = now - (tokens.timestamp || 0); - const expiresIn = tokens.expires_in || 3600; // Default to 1 hour if not specified - const tokenExpiresInMs = expiresIn * 1000; - - // If token is still valid (with 60-second buffer) - if (tokenAge < tokenExpiresInMs - 60000) { - return { authenticated: true, sessionId }; - } - - // If token is expired but we have a refresh token, try to refresh - if (tokens.refresh_token) { - try { - const refreshedTokens = await refreshTokens(tokens.refresh_token); - - // Store refreshed tokens - await storage.setState(`session:${sessionId}:tokens`, { - access_token: refreshedTokens.access_token, - refresh_token: refreshedTokens.refresh_token || tokens.refresh_token, - id_token: refreshedTokens.id_token || tokens.id_token, - expires_in: refreshedTokens.expires_in || 3600, - timestamp: Date.now() - }); - - return { authenticated: true, sessionId }; - } catch (refreshError) { - console.error('Failed to refresh token:', refreshError); - // Token refresh failed, user needs to re-authenticate - return { authenticated: false }; - } - } - - return { authenticated: false }; - } catch (error) { - console.error('Error checking authentication:', error); - return { authenticated: false }; - } -}; +} -// Function to refresh tokens -async function refreshTokens(refreshToken: string) { - const tokenUrl = new URL('/oauth2/token', KINDE_ISSUER_URL); - const params = new URLSearchParams(); - - params.append('grant_type', 'refresh_token'); - params.append('refresh_token', refreshToken); - params.append('client_id', KINDE_CLIENT_ID); - params.append('client_secret', KINDE_CLIENT_SECRET); - - const response = await fetch(tokenUrl.toString(), { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': 'application/json' - }, - body: params - }); - - if (!response.ok) { - const error = await response.json().catch(() => ({})); - throw new Error(`Token refresh failed: ${error.error || response.status}`); - } +async function handleLogout(config: ReturnType) { + const logoutUrl = new URL('/logout', config.issuerURL); + logoutUrl.searchParams.append('redirect', config.postLogoutRedirectURL); - return await response.json(); -} + return redirect(302, logoutUrl.toString()); +} ``` -## Step 9: Protect routes +## Step 6: Check authentication in protected routes -For protected routes like a dashboard: +For protected routes, use js-utils authentication check: ```typescript // src/routes/dashboard/+page.server.ts import type { PageServerLoad } from './$types'; -import { createKindeStorage } from '$lib/kindeCloudflareStorage'; +import { isAuthenticated, getUserProfile } from '@kinde/js-utils'; +import { initializeKindeAuth } from '$lib/kindeAuth'; import { redirect } from '@sveltejs/kit'; export const load: PageServerLoad = async (event) => { - const storage = createKindeStorage(event); - - if (!storage) { - throw redirect(302, '/api/auth/login'); - } - - const cookies = event.request.headers.get('cookie') || ''; - const sessionMatch = cookies.match(/kinde_session=([^;]+)/); - const sessionId = sessionMatch ? sessionMatch[1] : null; - - if (!sessionId) { + if (!initializeKindeAuth(event)) { throw redirect(302, '/api/auth/login'); } + + const authenticated = await isAuthenticated(); - // Now we can safely access the tokens with the sessionId - const tokens = await storage.getState(`session:${sessionId}:tokens`); - - if (!tokens?.access_token) { + if (!authenticated) { throw redirect(302, '/api/auth/login'); } - + + const userProfile = await getUserProfile(); + return { authenticated: true, - userId: tokens.id_token ? getUserIdFromToken(tokens.id_token) : null + user: userProfile }; }; - -// Helper function to get user ID from ID token -function getUserIdFromToken(idToken) { - try { - const payload = JSON.parse(atob(idToken.split('.')[1])); - return payload.sub; - } catch (e) { - console.error('Failed to extract user ID from token'); - return null; - } -} ``` -Your Kinde authentication should now be working with SvelteKit on Cloudflare Pages. +## Usage + +Once configured, you can use standard links for authentication: + +- Login: `/api/auth/login` +- Register: `/api/auth/register` +- Logout: `/api/auth/logout` + +Use `isAuthenticated()` and `getUserProfile()` from js-utils in your server-side code to check authentication status and retrieve user data. + +## Key benefits + +- **Minimal code**: js-utils handles OAuth flow, token management, and user profile retrieval +- **Hybrid storage**: Cookies for immediate consistency (OAuth data), KV for persistence (tokens) +- **Type safety**: Full TypeScript support with js-utils types +- **Production ready**: Handles token refresh, state validation, and security best practices + +## Troubleshooting + +**"invalid_client" error**: Ensure your Kinde application is configured as a **Single Page Application** (SPA). + +**Window errors in server logs**: These are expected in server environments and are handled gracefully. +Your Kinde authentication should now be working seamlessly with SvelteKit on Cloudflare Pages! \ No newline at end of file From aa6a1f649a5d98732e872eff724fbc8339fd7d37 Mon Sep 17 00:00:00 2001 From: KeeganBeuthin Date: Thu, 26 Jun 2025 10:34:32 +0200 Subject: [PATCH 14/14] added rabbit changes --- .../kinde-sveltekit-with-cloudflare.mdx | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx index c2dface33..5e1c187db 100644 --- a/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx +++ b/src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx @@ -37,7 +37,7 @@ npm install -D @sveltejs/adapter-cloudflare ## Step 2: Configure Cloudflare KV storage 1. In Cloudflare dashboard, go to **Workers & Pages > KV** -2. Create a new namespace (e.g., `AUTH_TOKENS`) +2. Create a new namespace (e.g., `AUTH_STORAGE`) 3. Copy the namespace ID ## Step 3: Configure environment variables @@ -113,7 +113,7 @@ class CookieStorage implements SessionManager { } async removeSessionItem(key: string): Promise { - this.event.cookies.delete(`kinde_${key}`, { path: '/' }); + this.event.cookies.delete(`kinde_${key}`, { path: '/', secure: true }); } async removeItems(keys: string[]): Promise { @@ -125,7 +125,7 @@ class CookieStorage implements SessionManager { async clearSession(): Promise { const cookiesToClear = ['state', 'nonce', 'codeVerifier']; for (const key of cookiesToClear) { - this.event.cookies.delete(`kinde_${key}`, { path: '/' }); + this.event.cookies.delete(`kinde_${key}`, { path: '/', secure: true }); } } } @@ -202,8 +202,7 @@ export async function GET(event: RequestEvent) { return json({ error: 'KV storage not available' }, { status: 500 }); } - const url = new URL(event.request.url); - const path = url.pathname.split('/').pop() || ''; + const path = event.params.kindeAuth ?? ''; const config = getConfig(); switch (path) { @@ -325,13 +324,6 @@ Once configured, you can use standard links for authentication: Use `isAuthenticated()` and `getUserProfile()` from js-utils in your server-side code to check authentication status and retrieve user data. -## Key benefits - -- **Minimal code**: js-utils handles OAuth flow, token management, and user profile retrieval -- **Hybrid storage**: Cookies for immediate consistency (OAuth data), KV for persistence (tokens) -- **Type safety**: Full TypeScript support with js-utils types -- **Production ready**: Handles token refresh, state validation, and security best practices - ## Troubleshooting **"invalid_client" error**: Ensure your Kinde application is configured as a **Single Page Application** (SPA).