nexus-5-auth/ORY_SDK_MIGRATION_GUIDE.md
2026-01-26 11:15:52 -05:00

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.*