12 KiB
12 KiB
🚀 Ory SDK Migration Guide
Fixing CSRF Issues by Using the SDK Properly
Created: October 17, 2025
📋 Problem Analysis
Current Issue
The frontend has been manually handling forms with fetch() and FormData, which causes CSRF token mismatches because:
- Form submission uses
application/x-www-form-urlencoded - Manual CSRF token handling leads to cookie accumulation
- SDK expects
application/jsonwith automatic CSRF management
Why Admin Works vs Settings Fails
- Admin Interface: Uses
kratosAdminClient.methodName()→ JSON payloads → ✅ Works - Settings Forms: Uses manual
fetch()→ Form-encoded data → ❌ CSRF errors
🔧 The Solution: Proper SDK Usage
Core Principle
Stop using manual fetch() calls. Use the Ory SDK methods which handle CSRF automatically.
📚 SDK Method Reference
Available Methods
// Flow Creation (usually browser redirects)
kratosClient.createBrowserLoginFlow()
kratosClient.createBrowserRegistrationFlow()
kratosClient.createBrowserSettingsFlow()
kratosClient.createBrowserRecoveryFlow()
// Flow Data Fetching
kratosClient.getLoginFlow({ id: flowId })
kratosClient.getRegistrationFlow({ id: flowId })
kratosClient.getSettingsFlow({ id: flowId })
kratosClient.getRecoveryFlow({ id: flowId })
// Flow Submission (the key ones!)
kratosClient.updateLoginFlow({ flow: flowId, updateLoginFlowBody: data })
kratosClient.updateRegistrationFlow({ flow: flowId, updateRegistrationFlowBody: data })
kratosClient.updateSettingsFlow({ flow: flowId, updateSettingsFlowBody: data })
kratosClient.updateRecoveryFlow({ flow: flowId, updateRecoveryFlowBody: data })
🏗️ Migration Pattern
1. Form Data Conversion
// Helper function to convert FormData to JSON
const formDataToJson = (formData: FormData): Record<string, any> => {
const json: Record<string, any> = {};
for (const [key, value] of formData.entries()) {
// Skip CSRF token - SDK handles this automatically
if (key === 'csrf_token') continue;
// Handle method field (used by Kratos to determine which action to take)
if (key === 'method') {
json.method = value;
continue;
}
// Handle nested object notation (e.g., traits.email, traits.name.first)
if (key.includes('.')) {
setNestedProperty(json, key, value);
} else {
json[key] = value;
}
}
return json;
};
// Helper for nested properties
const setNestedProperty = (obj: any, path: string, value: any) => {
const keys = path.split('.');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!(key in current)) {
current[key] = {};
}
current = current[key];
}
current[keys[keys.length - 1]] = value;
};
2. Settings Form Handler (Before → After)
❌ BEFORE (Manual fetch - causes CSRF issues)
// OLD WAY - DON'T USE
async function handleSubmit(event: Event) {
event.preventDefault();
const form = event.target as HTMLFormElement;
const formData = new FormData(form);
const response = await fetch(flow.ui.action, {
method: 'POST',
body: formData, // ← Form-encoded data
credentials: 'include',
headers: {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
}
});
// ... handle response
}
✅ AFTER (SDK method - handles CSRF automatically)
// NEW WAY - USE THIS
import { kratosClient } from '$lib/kratos';
async function handleSubmit(event: Event) {
event.preventDefault();
const form = event.target as HTMLFormElement;
const formData = new FormData(form);
// Convert form data to JSON
const updateBody = formDataToJson(formData);
try {
// Use SDK method - handles CSRF automatically via JSON + cookies
const { data } = await kratosClient.updateSettingsFlow({
flow: flowId,
updateSettingsFlowBody: updateBody
});
// Handle success
if (data.redirect_browser_to) {
window.location.href = data.redirect_browser_to;
} else {
// Show success message or reload
window.location.reload();
}
} catch (error: any) {
console.error('Settings update failed:', error);
// Handle specific errors
if (error.response?.status === 410) {
// Flow expired, restart
window.location.href = `${PUBLIC_KRATOS_URL}/self-service/settings/browser`;
} else if (error.response?.data?.ui) {
// Update flow with validation errors
flow = error.response.data;
}
}
}
📝 Implementation Steps
Step 1: Update FlowForm Component
// src/lib/components/FlowForm.svelte
<script lang="ts">
import { kratosClient } from '$lib/kratos';
import type { LoginFlow, RegistrationFlow, SettingsFlow, RecoveryFlow, VerificationFlow } from '@ory/client';
type Flow = LoginFlow | RegistrationFlow | SettingsFlow | RecoveryFlow | VerificationFlow;
let { flow, groups = ['default', 'password', 'profile', 'totp', 'webauthn', 'code', 'link'] }: {
flow: Flow;
groups?: string[]
} = $props();
const nodes = $derived(filterNodesByGroups(flow.ui.nodes, ...groups));
const messages = $derived(flow.ui.messages || []);
const formMethod = $derived((flow.ui.method?.toLowerCase() || 'post') as 'get' | 'post');
// Helper function to convert FormData to JSON
const formDataToJson = (formData: FormData): Record<string, any> => {
const json: Record<string, any> = {};
for (const [key, value] of formData.entries()) {
if (key === 'csrf_token') continue; // SDK handles this
if (key.includes('.')) {
setNestedProperty(json, key, value);
} else {
json[key] = value;
}
}
return json;
};
const setNestedProperty = (obj: any, path: string, value: any) => {
const keys = path.split('.');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!(key in current)) current[key] = {};
current = current[key];
}
current[keys[keys.length - 1]] = value;
};
// SDK-based form submission
async function handleSubmit(event: Event) {
event.preventDefault();
const form = event.target as HTMLFormElement;
const formData = new FormData(form);
const updateBody = formDataToJson(formData);
try {
let response;
// Use appropriate SDK method based on flow type
switch (flow.type) {
case 'login':
response = await kratosClient.updateLoginFlow({
flow: flow.id,
updateLoginFlowBody: updateBody
});
break;
case 'registration':
response = await kratosClient.updateRegistrationFlow({
flow: flow.id,
updateRegistrationFlowBody: updateBody
});
break;
case 'settings':
response = await kratosClient.updateSettingsFlow({
flow: flow.id,
updateSettingsFlowBody: updateBody
});
break;
case 'recovery':
response = await kratosClient.updateRecoveryFlow({
flow: flow.id,
updateRecoveryFlowBody: updateBody
});
break;
case 'verification':
response = await kratosClient.updateVerificationFlow({
flow: flow.id,
updateVerificationFlowBody: updateBody
});
break;
default:
throw new Error(`Unsupported flow type: ${flow.type}`);
}
// Handle success response
if (response.data.redirect_browser_to) {
window.location.href = response.data.redirect_browser_to;
} else {
// For settings, might want to show success message
if (flow.type === 'settings') {
window.location.href = window.location.pathname + '?updated=true';
} else {
window.location.reload();
}
}
} catch (error: any) {
console.error('Flow submission failed:', error);
if (error.response?.status === 410) {
// Flow expired, restart appropriate flow
const flowType = flow.type;
window.location.href = `${PUBLIC_KRATOS_URL}/self-service/${flowType}/browser`;
} else if (error.response?.data?.ui) {
// Update with validation errors
flow = error.response.data;
}
}
}
</script>
<!-- Remove manual onsubmit handlers, use SDK-based submission -->
<form action={flow.ui.action} method={formMethod} onsubmit={handleSubmit}>
{#each nodes as node (node.attributes)}
<FormField {node} />
{/each}
</form>
Step 2: Update Individual Page Components
// src/routes/settings/+page.svelte
<script lang="ts">
import { kratosClient } from '$lib/kratos';
import FlowForm from '$lib/components/FlowForm.svelte';
import { page } from '$app/state';
import { onMount } from 'svelte';
import { PUBLIC_KRATOS_URL } from '$env/static/public';
import type { SettingsFlow } from '@ory/client';
let flow = $state<SettingsFlow | null>(null);
const isUpdated = $derived(page.url.searchParams.has('updated'));
onMount(async () => {
const flowId = page.url.searchParams.get('flow');
if (!flowId) {
// Redirect to create flow
window.location.href = `${PUBLIC_KRATOS_URL}/self-service/settings/browser`;
return;
}
try {
// Use SDK method to fetch flow
const { data } = await kratosClient.getSettingsFlow({ id: flowId });
flow = data;
} catch (error) {
console.error('Failed to fetch settings flow:', error);
// Restart flow on error
window.location.href = `${PUBLIC_KRATOS_URL}/self-service/settings/browser`;
}
});
</script>
<!-- Rest of component remains the same -->
{#if flow}
<FlowForm {flow} groups={['profile']} />
<FlowForm {flow} groups={['password']} />
<FlowForm {flow} groups={['totp', 'webauthn']} />
{/if}
🎯 Key Benefits
✅ What This Fixes
- No more CSRF errors - SDK handles tokens automatically
- No cookie accumulation - Proper JSON-based API usage
- Better error handling - Structured error responses
- Type safety - Full TypeScript support
- Consistency - Same pattern as admin interface
📊 Comparison
| Aspect | Manual Fetch (Old) | SDK Methods (New) |
|---|---|---|
| Content-Type | application/x-www-form-urlencoded |
application/json |
| CSRF Handling | Manual form fields | Automatic via cookies |
| Error Handling | Manual response parsing | Structured exceptions |
| Type Safety | None | Full TypeScript |
| Cookie Management | Manual/broken | Automatic |
🚨 Migration Checklist
- Remove
clearCsrfCookies()calls - No longer needed - Update FlowForm component - Use SDK methods instead of fetch
- Update all page components - Use SDK for flow fetching
- Remove manual CSRF handling - SDK does this automatically
- Test all flows - Login, Registration, Settings, Recovery
- Remove unused utilities - Clean up old cookie management code
🔄 Testing Strategy
Before Deployment
- Clear all browser cookies for your domain
- Test complete user journey:
- Registration → Login → Settings update → Logout
- Verify no CSRF errors in browser console
- Check network tab - Should see JSON payloads, not form data
Production Validation
- Monitor error logs for any remaining CSRF issues
- Test across different browsers (Chrome, Firefox, Safari)
- Verify admin interface still works (should be unchanged)
📚 Additional Resources
This migration will eliminate your CSRF issues completely by using the Ory SDK as intended. The SDK's JSON-based approach with automatic CSRF handling is the official, supported pattern.