3

Using:

  • Django 3.x [ Django-Filters 2.2.0, graphene-django 2.8.0, graphql-relay 2.0.1 ]
  • Vue 2.x [ Vue-Apollo ]

I am testing single page vue app´s with Django, GraphQL & Vue-Apollo.

If i use csrf_exempt on my view everything works in the frontend.

urlpatterns = [
<...>
   path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True))),
<...>

Now i wanted to CSRF protect my request. Within the process of understanding the CSRF protection, i thought all Django GraphQLView needs is to receive the "value" of the X-Csrftoken in the Request Header. So i focused on sending the csrf Value in different ways...via a single view like this

path('csrf/', views.csrf),
path("graphql", GraphQLView.as_view(graphiql=True)),

or by ensure a cookie with ensure_csrf_cookie

Afterwards in my ApolloClient i fetch thes Value and send him back with the request Header .

This i what Django prints when i send a GraphQL request from a Django-Vue page.

Forbidden (CSRF token missing or incorrect.): /graphql

Parallel i always test with thegraphiql IDE and these requests still working. I also print everytime the info.context.headers value of my query resolver.

{'Content-Length': '400', 'Content-Type': 'application/json',
'Host': 'localhost:7000', 'Connection': 'keep-alive',
'Pragma': 'no-cache', 'Cache-Control': 'no-cache', 
'Accept': 'application/json', 'Sec-Fetch-Dest': 'empty', 'X-Csrftoken': 'dvMXuYfAXowxRGtwSVYQmpNcpGrLSR7RuUnc4IbIarjljxACtaozy3Jgp3YOkMGz',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36',
'Origin': 'http://localhost:7000',
'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-Mode': 'cors',
'Referer': 'http://localhost:7000/graphql', 'Accept-Encoding': 'gzip, deflate, br', 'Accept-Language': 'en-US,en;q=0.9,de;q=0.8',
'Cookie': 'sessionid=jqjvjfvg4sjmp7nkeunebqos8c7onhiz; csrftoken=dvMXuYfAXowxRGtwSVYQmpNcpGrLSR7RuUnc4IbIarjljxACtaozy3Jgp3YOkMGz'}

i recognized that the GraphQLView IDE alway puts the X-Csrftoken and the Cookie:..csrftoken. also in the request. if delete the csrftoken-cookie of a GraphQLView IDE before sending the request, i get this

Forbidden (CSRF cookie not set.): /graphql

The IDE shows a long, red report

.... CSRF verification failed. Request aborted.</p>\n\n\n  
<p>You are seeing this message because this site requires a CSRF cookie when submitting forms.
This cookie is required for security reasons, to ensure that your browser is not being hijacked by third parties.</p>\n

The Information of the IDE say´s the request needs a CSRF cookie. But all read until now in Forums, Doc´s, was more related to the value itself. Meaning all you need is to send the csrf value within the Header as X-Csrftoken or so and the View would do the magic.


Question

Therefore my Question is:

Do i have to set the X-Csrftoken and the Cookie:..csrftoken at the same time in my ApolloClient to make a request on my django GraphQLView ?

Or is it also possible to simple send only the X-Csrftoken without a csrf-cookie and vice versa?

black_hole_sun
  • 908
  • 11
  • 41

2 Answers2

4

After long time and a pause to follow the issue, i tried once more and found a solution.

Setup

  • django 3.1
  • vue 2.6
  • vue-apollo 3.0.4 (supports the new Apollo-Client 3)
  • @apollo/client 3.1.3

Presumption

  • I am using Vue as a multi app and not single app.
  • the Webpack DevServer will Hot-Reload while writing my *vue.js files in the Django STATICFILES_DIRS. Django will take the Files from there. works fine

Problem Recap

After revisiting my problem i noticed i have 2 Issue. One was the Browser denied graphQL request because of CORS. And the Second was the CSRF Token.


Solution

To Fix the CORS Issue i noticed that my uri of the Apollo Client was not the same as my Django Dev Server. Instead of http://127.0.0.1:7000/graphql it was set to http://localhost:7000/graphql. I also set the credentials (see vue-apollo.js)

To Fix the CSRF i did 3 things

  • make sure to send an {% csrf_token %} with the HTML where your Vue/ GraphQL Client app is hooked. So that we can fetch it later.
  • Install js-cookie for getting the Cookie
  • set a header in the Apollo Client Constructor with X-CSRFToken in vue-apollo.js

vue-apollo.js


import Vue from 'vue'
// import path for the new Apollo Client 3 and Vue-Apollo
import { ApolloClient, InMemoryCache } from '@apollo/client/core';
import VueApollo from 'vue-apollo'
import Cookies from 'js-cookie'

  
// Create the apollo client
const apolloClient = new ApolloClient({
  // -------------------
  // # Required Fields #
  // -------------------
  // URI - GraphQL Endpoint
  uri: 'http://127.0.0.1:7000/graphql',
  // Cache
  cache: new InMemoryCache(),

  // -------------------
  // # Optional Fields #
  // -------------------
  // DevBrowserConsole
  connectToDevTools: true,
  // Else
  credentials: 'same-origin',
  headers: {
    'X-CSRFToken': Cookies.get('csrftoken')
  }
});
  
// create Vue-Apollo Instance
const apolloProvider = new VueApollo({
  defaultClient: apolloClient,
})
  
// Install the vue plugin
Vue.use(VueApollo)
  
export default apolloProvider

Vue.config.js


const BundleTracker = require("webpack-bundle-tracker");

// hook your apps
const pages = {
    'page_1': {
        entry: './src/page_1.js',
        chunks: ['chunk-vendors']
    },
    'page_2': {
        entry: './src/page_2.js',
        chunks: ['chunk-vendors']
    },
}

module.exports = {
    pages: pages,
    filenameHashing: false,
    productionSourceMap: false,

    // puplicPath: 
    // Tells Django where do find the bundle.
    publicPath: '/static/',

    // outputDir:
    // The directory where the production build files will be generated - STATICFILES_DIRS
    outputDir: '../dev_static/vue_bundle',
 
    
    chainWebpack: config => {

        config.optimization
        .splitChunks({
            cacheGroups: {
                vendor: {
                    test: /[\\/]node_modules[\\/]/,
                    name: "chunk-vendors",
                    chunks: "all",
                    priority: 1
                },
            },
        });


        // Don´t create Templates because we using Django Templates
        Object.keys(pages).forEach(page => {
            config.plugins.delete(`html-${page}`);
            config.plugins.delete(`preload-${page}`);
            config.plugins.delete(`prefetch-${page}`);
        })

        // create webpack-stats.json. 
        // This file will describe the bundles produced by this build process.
        // used eventually by django-webpack-loader
        config
            .plugin('BundleTracker')
            .use(BundleTracker, [{filename: '/webpack-stats.json'}]);


        // added to use ApolloQuery Tag (Apollo Components) see vue-apollo documentation
        config.module
        .rule('vue')
        .use('vue-loader')
            .loader('vue-loader')
            .tap(options => {
            options.transpileOptions = {
                transforms: {
                dangerousTaggedTemplateString: true,
                },
            }
            return options
            })
        
        // This will allows us to reference paths to static 
        // files within our Vue component as <img src="~__STATIC__/logo.png">
        config.resolve.alias
            .set('__STATIC__', 'static')

        // configure a development server for use in non-production modes,
        config.devServer
            .public('http://localhost:8080')
            .host('localhost')
            .port(8080)
            .hotOnly(true)
            .watchOptions({poll: 1000})
            .https(false)
            .headers({"Access-Control-Allow-Origin": ["*"]})
        
        // DO have Webpack hash chunk filename
        config.output
            .chunkFilename("[id].js")
            },

    devServer: {
        writeToDisk: true
      }
};
black_hole_sun
  • 908
  • 11
  • 41
1

Was running into the same issue. My application backend is Django with graphene. My frontend is React. I also had 2 issues:

  1. i didn't use the correct graphql url in my frontend apollo createHttpLink. In my django urls.py, my graphql url had "/" but i didn't put "/" in frontend. Just make sure the urls match exactly.

  2. In addition to setting csrf token to header, you also have to set the csrf in Cookie object otherwise you will get a forbidden error. See [this][https://github.com/graphql-python/graphene-django/issues/786]. When the CSRF_USE_SESSIONS settings variable is set to True post requests can not be made as the request will be rejected. This is because the CSRFTOKEN is not provided, because it will not be stored in a cookie.Django will end up giving this warning: Forbidden (CSRF token missing or incorrect.): This issue can be prevented by passing the view to csrf_exempt in django's urls.py file.

    path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True)))

However a better solution is to store csrf in global cookie in the frontend in addition to setting it in the header like this:

const csrftoken = await getCsrfToken();
const cookies = new Cookies();
cookies.set('csrftoken', csrftoken);

(See my index.js code below for full code).

My index.js file:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './custom.scss'
import allReducers from './reducer';
import {Provider} from 'react-redux';
import {BrowserRouter, Route, Routes} from "react-router-dom";
import {
    ApolloClient,
    InMemoryCache,
    ApolloProvider, from, createHttpLink
} from "@apollo/client";
import {createStore} from "redux";
import {AuthProvider} from "./utils/auth";
import {setContext} from "@apollo/client/link/context";
import {ACCESS_TOKEN_KEY} from "./constants/Constants";
import {onError} from "@apollo/client/link/error";
import Cookies from "universal-cookie/es6";

const store = createStore(allReducers,
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

let csrftoken;

async function getCsrfToken() {
    if (csrftoken) return csrftoken;
    csrftoken = await fetch('http://localhost:8000/csrf/')
        .then(response => response.json())
        .then(data => data.csrfToken)
    return await csrftoken
}


const authMiddleware = setContext(async (req, { headers }) => {
    const token = localStorage.getItem(ACCESS_TOKEN_KEY);
    const csrftoken = await getCsrfToken();
    const cookies = new Cookies();
    cookies.set('csrftoken', csrftoken);
    return {
        headers: {
            ...headers,
            'X-CSRFToken': csrftoken,
            Authorization: token ? `Bearer ${token}` : ''
        },
    };
});

const httpLink = createHttpLink({
    uri: 'http://localhost:8000/graphql/',
    credentials: 'include'
});

const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors)
    graphQLErrors.map(({ message, locations, path }) =>
      console.log(
        `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
      )
    )
  if (networkError) console.log(`[Network error]: ${networkError}`)
})


const client2 = new ApolloClient({
    uri: 'http://localhost:8000/graphql/',
    cache: new InMemoryCache(),
    credentials: 'include',
    link: from([authMiddleware, errorLink, httpLink])
});

ReactDOM.render(
    <Provider store={store}>
        <React.StrictMode>
            <ApolloProvider client={client2}>
                <BrowserRouter>
                    <AuthProvider>
                        <Routes>
                            <Route path="/*" element={<App/>} />
                        </Routes>
                    </AuthProvider>
                </BrowserRouter>
            </ApolloProvider>,
        </React.StrictMode></Provider>,
    document.getElementById('root')
);

On the backend, here are some of the related files:

urls.py

urlpatterns = [
    path('admin/', admin.site.urls),
    path('graphql/', GraphQLView.as_view(graphiql=True)),
    path('csrf/', csrf),
]

settings.py:

CORS_ALLOW_CREDENTIALS = True

CORS_ORIGIN_WHITELIST = ["http://localhost:3000", ]
CSRF_TRUSTED_ORIGINS = ["http://localhost:3000", ]

CORS_ALLOW_METHODS = [
    'DELETE',
    'GET',
    'OPTIONS',
    'PATCH',
    'POST',
    'PUT',
]

CORS_ALLOW_HEADERS = [
    "accept",
    "accept-encoding",
    "authorization",
    "content-type",
    "dnt",
    "origin",
    "user-agent",
    "x-csrftoken",
    "x-requested-with",
]

views.py (returns generated csrf token to frontend)

from django.http import JsonResponse
from django.middleware.csrf import get_token
from django.shortcuts import render


# Create your views here.
def csrf(request):
    return JsonResponse({'csrfToken': get_token(request)})
Sher Sanginov
  • 431
  • 5
  • 9