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

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:

  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

// 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

  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


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.