djangosveltecsrfsveltekitdjango-allauth

How do I override my SvelteKit CSRF token to match my Django backend's CSRF token?


Let me start by clarifying that I am using Django on the backend, but then using Svelte and SvelteKit on the "frontend". I am also using Allauth Headless for authentication.

When I remove django.middleware.csrf.CsrfViewMiddleware from my settings.py file, my signup works as expected. That leads to believe this issue is only caused by the CSRF mismatch and not CORS issues such as not having localhost:5173 in ALLOWED_HOSTS.

I would expect that the CSRF token that is being printed out by both Django and Svelte would match the token being passed in my request headers. However, even though I both of those printed out values match, the request header in the browser inspector has a different value for csrftoken. The error I get in the Django console is Forbidden (CSRF token from the 'X-Csrftoken' HTTP header incorrect.).

Here is how I have it set up:

  1. I am using get_token in the views.py to get the CSRF token:
from django.http import JsonResponse
from django.middleware.csrf import get_token

def get_csrf_token(request):
    token = get_token(request)
    print(f"CSRF Token: {token}")
    response = JsonResponse({'csrfToken': token})
    response.set_cookie('csrftoken', token)
    return response
  1. I am creating an endpoint to hit to retrieve that token:
...
from .views import get_csrf_token
...


urlpatterns = [
    ...
    path('csrf-token/', get_csrf_token, name='csrf_token'),
    ...
]
  1. I have my form in the +page.svelte file that has a signup action that will get carried out in the +page.server.ts file:
<script lang="ts">
  import type { ActionData } from './$types';

  export let form: ActionData;
</script>
<div class="container my-4">
    <div class="row justify-content-center">
        <div class="col-md-6">
            <h2 class="text-center logo-font mb-4 text-pink mt-5">Sign Up</h2>
            <hr class="w-25 mx-auto">

            <form action="?/signup" method="POST" class="signup-form">
                <div class="form-group">
                    <label for="username" class="--bs-pink1">Username</label>
                    <input type="username" name="username" id="username" class="form-control"
                        placeholder="Username" value={form?.username?? ''} required>
                    <small class="text-secondary">*Choose your username carefully, as it cannot be changed later on.</small>
                </div>
                <div class="form-group">
                  <label for="email" class="text-pink">Email</label>
                  <input type="email" name="email" id="email" class="form-control mb-3" placeholder="Email" value={form?.email ?? ''} required>
                </div>
                <div class="form-group">
                  <label for="password1" class="text-pink">Password</label>
                  <input type="password" name="password1" id="password1" class="form-control mb-3" placeholder="Password" required>
                </div>
                <div class="form-group">
                    <label for="password2" class="text-pink">Confirm password</label>
                    <input type="password" name="password2" id="password2" class="form-control mb-3"
                        placeholder="Confirm password" required>
                </div>
                {#if form?.error}
                <div class="alert alert-danger mt-3">
                  <ul class="list-unstyled m-0">
                    <li>{form.error}</li>
                  </ul>
                </div>
              {/if}
                <div class="form-actions text-center">
                    <button class="btn btn-lg btn-primary btn-block mt-4 -bs-purple" type="submit"
                        style="width: 80%">Confirm</button>
                </div>
          </form>
  1. I adapted the code from the Allauth Headless React SPA Example:
import { getCsrfToken } from './csrfCookie';

...

const BASE_URL = `http://localhost:8000/_allauth/${CLIENT}/v1`;
const ACCEPT_JSON = {
    accept: 'application/json'
};

...

export const URLs = Object.freeze({
        ...
    SIGNUP: BASE_URL + '/auth/signup',
        ...
});

async function request(method, path, data = {}, headers = {}, event = null) {
    const options = {
        method,
        headers: {
            ...ACCEPT_JSON,
            ...headers
        },
        credentials: 'include'
    };

    if (typeof data !== 'undefined') {
        options.body = JSON.stringify(data);
        options.headers['Content-Type'] = 'application/json';
    }

    const fetchFn = event ? event.fetch : fetch;
    const resp = await fetchFn(path, options);
    const msg = await resp.json();
    return msg;
}

...

export async function signUp(data, event = null) {
    const csrfToken = await getCsrfToken();
    const headers = csrfToken ? { 'X-CSRFToken': csrfToken } : {};
        // logs the same value as Django csrf token in console
    console.log(`headers in signUp func: ${JSON.stringify(headers)}`); 
    return await request('POST', URLs.SIGNUP, data, headers, event);
}
  1. In my csrfCookie.ts file, I have my function to retrieve the Django-generated token:
export async function getCsrfToken() {
    const server_url = 'http://localhost:8000';
    const response = await fetch(server_url + '/csrf-token/', {
        credentials: 'include'
    });

    const data = await response.json();
    const csrfToken = data.csrfToken;

    console.log('CSRF token response:', data);
    console.log('CSRF token for request:', csrfToken);

    return csrfToken;
}
  1. In my +page.server.ts file, I run the signup action, which in turn runs the signUp function:
// +page.server.ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
import { signUp } from '$lib/allauth';

export const actions: Actions = {
    signup: async ({ request, fetch }) => {
        const data = await request.formData();
        const username = data.get('username') as string;
        const email = data.get('email') as string;
        const password1 = data.get('password1') as string;
        const password2 = data.get('password2') as string;

        if (password1 !== password2) {
            return fail(400, { error: 'Passwords do not match' });
        }
        try {
            const response = await signUp({ username, email, password: password1 }, { fetch });
            console.log('Full response:', response);

            if (response.status === 200) {
                return redirect(303, '/account');
            } else if (response.status === 400) {
                return fail(400, { error: response.errors[0].message });
            } else {
                return fail(500, { error: 'An error occurred' });
            }
        } catch (error) {
            console.error('Signup error:', error);
            return fail(500, { error: 'An error occurred' });
        }
    }
};

If I disable SvelteKit's CSRF protection in the svelte.config.js file, I then start getting Forbidden (CSRF cookie not set.):

import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

/** @type {import('@sveltejs/kit').Config} */
const config = {
    kit: {
        csrf: {
            checkOrigin: false
        },
        adapter: adapter({
            out: 'build',
            preprocess: [vitePreprocess()]
        })
    }
};

export default config;

I have also tried loading the Django CSRF token into a hidden input in the form, but that also seems to get replaced if I try that, even if CSRF is disabled in SvelteKit.

Thanks in advance!


Solution

  • As it turns out, I was doing this kind of wrong. I should have been using a hooks.server.ts file first of all. Hyntabyte explains it well in this YouTube video: https://www.youtube.com/watch?v=K1Tya6ovVOI

    Essentially, we should be doing the auth check in the hooks.server.ts file, then passing whether or not it is authenticated using Locals. That way, you can access it through locals in each +page.server.ts file to determine if the user is authenticated or not.

    Another issue I was running into later was that I needed to forward to cookies to SvelteKit. I keep trying to set it using event.cookies, but it should have been event.request.headers.get like this:

    import type { Handle } from '@sveltejs/kit';
    import { API_URL } from '$lib/config';
    
    export const handle: Handle = async ({ event, resolve }) => {
        const cookies = event.request.headers.get('cookie') || '';
    
        const response = await fetch(`${API_URL}/api/auth/status`, { # my endpoint for checking auth status
            credentials: 'include',
            headers: {
                'X-Requested-With': 'XMLHttpRequest',
                Cookie: cookies
            }
        });
    
        const data = await response.json();
        event.locals.isAuthenticated = data.isAuthenticated;
        event.locals.user = data.user;
    
        return resolve(event);
    };
    
    

    Finally, I also had issues with CORS. I needed to run my Django server using python manage.py runserver localhost:8000 to prevent it from defaulting to 127.0.0.1:8000. I also needed to have these in my settings.py file:

    CSRF_COOKIE_SECURE = True
    SESSION_COOKIE_SECURE = True
    CSRF_COOKIE_SAMESITE = 'None'
    SESSION_COOKIE_SAMESITE = 'None'