Skip to main content
Ory Kratos implements CSRF (Cross-Site Request Forgery) protection for all self-service flows to prevent attackers from tricking users into performing unwanted actions.

How CSRF protection works

Kratos uses the double submit cookie pattern for CSRF protection:
  1. When a flow is initialized, Kratos generates a CSRF token
  2. The token is:
    • Stored in a cookie (ory_csrf_[flow_type])
    • Included in the flow UI (csrf_token field)
  3. When submitting the flow, both the cookie and form field must match
  4. Kratos validates the token server-side before processing the request
CSRF tokens are flow-specific and time-bound. Each flow type (login, registration, etc.) has its own CSRF token.

Browser flows

For browser-based flows, CSRF protection is automatic:

Flow initialization

When you initialize a flow:
curl -X GET 'https://kratos-public/self-service/login/browser' \
  -H 'Cookie: ory_csrf_login=csrf-token-value'
Kratos returns:
{
  "id": "9f425a8d-7efc-4768-8f23-7647a74c1a50",
  "ui": {
    "action": "/self-service/login?flow=9f425a8d...",
    "method": "POST",
    "nodes": [
      {
        "type": "input",
        "group": "default",
        "attributes": {
          "name": "csrf_token",
          "type": "hidden",
          "value": "csrf-token-value",
          "required": true
        }
      }
    ]
  }
}

Flow submission

Include the CSRF token when submitting:
curl -X POST 'https://kratos-public/self-service/login?flow=9f425a8d...' \
  -H 'Cookie: ory_csrf_login=csrf-token-value' \
  -H 'Content-Type: application/json' \
  -d '{
    "method": "password",
    "csrf_token": "csrf-token-value",
    "identifier": "user@example.com",
    "password": "secret"
  }'
Both the cookie and the form field must contain the same CSRF token. Mismatches result in a 400 error.

API flows

API flows (native mobile apps, SPAs without cookies) do not use CSRF protection because:
  • They use token-based authentication
  • They cannot be targeted by browser-based CSRF attacks
  • Same-origin policy doesn’t apply
# API flow - no CSRF token required
curl -X POST 'https://kratos-public/self-service/login?flow=api-flow-id' \
  -H 'Content-Type: application/json' \
  -d '{
    "method": "password",
    "identifier": "user@example.com",
    "password": "secret"
  }'
Configure CSRF cookies in your Kratos configuration:
cookies:
  domain: example.com
  path: /
  same_site: Lax  # or Strict
  secure: true
domain
string
Cookie domain. Use your root domain (e.g., example.com) to share cookies across subdomains.
same_site
string
SameSite attribute:
  • Strict: Most secure, blocks all cross-site requests
  • Lax: Balances security and usability (recommended)
  • None: Allows cross-site requests (requires secure: true)
secure
boolean
Only send cookies over HTTPS. Must be true in production.

Frontend implementation

React example

import { useEffect, useState } from 'react';

function LoginForm() {
  const [flow, setFlow] = useState(null);
  
  useEffect(() => {
    // Initialize flow
    fetch('https://kratos/self-service/login/browser', {
      credentials: 'include'  // Include cookies
    })
      .then(res => res.json())
      .then(setFlow);
  }, []);
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    // Find CSRF token from flow UI
    const csrfToken = flow.ui.nodes.find(
      n => n.attributes.name === 'csrf_token'
    ).attributes.value;
    
    const response = await fetch(flow.ui.action, {
      method: 'POST',
      credentials: 'include',  // Send cookies
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        method: 'password',
        csrf_token: csrfToken,  // Include token
        identifier: email,
        password: password
      })
    });
  };
  
  return (
    <form onSubmit={handleSubmit}>
      {/* Form fields */}
    </form>
  );
}

Vue.js example

<template>
  <form @submit.prevent="handleSubmit">
    <input type="hidden" :value="csrfToken" />
    <!-- Form fields -->
  </form>
</template>

<script>
export default {
  data() {
    return {
      flow: null,
      csrfToken: ''
    };
  },
  async mounted() {
    const response = await fetch('/self-service/login/browser', {
      credentials: 'include'
    });
    this.flow = await response.json();
    
    // Extract CSRF token
    const csrfNode = this.flow.ui.nodes.find(
      n => n.attributes.name === 'csrf_token'
    );
    this.csrfToken = csrfNode.attributes.value;
  },
  methods: {
    async handleSubmit() {
      await fetch(this.flow.ui.action, {
        method: 'POST',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          method: 'password',
          csrf_token: this.csrfToken,
          // ... other fields
        })
      });
    }
  }
};
</script>

CSRF token errors

Common CSRF-related errors:
Cause: Cookie token doesn’t match form tokenSolutions:
  • Ensure cookies are enabled
  • Check that credentials: 'include' is set in fetch requests
  • Verify cookie domain matches your application domain
  • Ensure HTTPS is used if secure: true
Cause: CSRF token not included in requestSolutions:
  • Extract csrf_token from flow UI nodes
  • Include token in request body
  • Ensure you’re using browser flow (not API flow)
Cause: Cross-origin or domain configuration issuesSolutions:
  • Configure cookies.domain correctly
  • Use same domain for frontend and Kratos
  • Check CORS configuration
  • Verify browser allows third-party cookies (if needed)

Development mode

The --dev flag disables CSRF protection:
kratos serve --dev
Never use --dev in production. It disables critical security features including CSRF protection.

Security considerations

1

Always use HTTPS

CSRF protection relies on secure cookies. Always use HTTPS in production.
2

Configure SameSite correctly

Use Lax or Strict for maximum protection. Only use None if you need cross-site authentication.
3

Validate on the server

Never rely solely on client-side CSRF checks. Kratos validates all tokens server-side.
4

Use short token lifetimes

CSRF tokens are tied to flow lifetimes. Keep flow lifespans short (e.g., 10-15 minutes).

Troubleshooting

Debug CSRF issues

Enable trace logging to see CSRF validation:
log:
  level: trace
Check cookies in browser DevTools:
  1. Open DevTools → Application → Cookies
  2. Look for ory_csrf_* cookies
  3. Verify cookie attributes (Secure, SameSite, Domain)

Test CSRF protection

Verify CSRF is working:
# This should fail (no CSRF token)
curl -X POST 'https://kratos/self-service/login?flow=...' \
  -H 'Content-Type: application/json' \
  -d '{"method": "password", "identifier": "test@example.com", "password": "test"}'

# Expected: 400 Bad Request

Next steps

Best practices

Security hardening guide

Rate limiting

Protect against brute force

Build docs developers (and LLMs) love