402 lines
12 KiB
Markdown
402 lines
12 KiB
Markdown
# 🚀 **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:
|
|
|
|
1. **Form submission uses `application/x-www-form-urlencoded`**
|
|
2. **Manual CSRF token handling** leads to cookie accumulation
|
|
3. **SDK expects `application/json`** with 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**
|
|
```typescript
|
|
// 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**
|
|
```typescript
|
|
// 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)**
|
|
```typescript
|
|
// 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)**
|
|
```typescript
|
|
// 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**
|
|
```typescript
|
|
// 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**
|
|
```typescript
|
|
// 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**
|
|
1. **No more CSRF errors** - SDK handles tokens automatically
|
|
2. **No cookie accumulation** - Proper JSON-based API usage
|
|
3. **Better error handling** - Structured error responses
|
|
4. **Type safety** - Full TypeScript support
|
|
5. **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**
|
|
1. **Clear all browser cookies** for your domain
|
|
2. **Test complete user journey**:
|
|
- Registration → Login → Settings update → Logout
|
|
3. **Verify no CSRF errors** in browser console
|
|
4. **Check network tab** - Should see JSON payloads, not form data
|
|
|
|
### **Production Validation**
|
|
1. **Monitor error logs** for any remaining CSRF issues
|
|
2. **Test across different browsers** (Chrome, Firefox, Safari)
|
|
3. **Verify admin interface** still works (should be unchanged)
|
|
|
|
---
|
|
|
|
## 📚 **Additional Resources**
|
|
|
|
- [Ory Kratos Self-Service Flows](https://www.ory.sh/docs/kratos/self-service)
|
|
- [TypeScript SDK Documentation](https://github.com/ory/sdk/tree/master/clients/client/typescript)
|
|
- [Flow UI Integration Guide](https://www.ory.sh/docs/kratos/self-service/flows/user-settings)
|
|
|
|
---
|
|
|
|
*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.* |