1

I am using Django rest framework as my API backend and React as my frontend. What I'd like to do is secure my backend so only the frontend can make "unsafe" requests to it such as post, put, delete, etc. My app is related to the blockchain so I use the user's MetaMask wallet as a means of authentication. As such, each user doesn't have a username or password so I am not equipped to use JWT. Instead, my hope was to pass a CSRF token to the frontend to enable the frontend to make those "unsafe" http requests.

I set up an endpoint on my backend to return a CSRF token to the frontend when it is requested. That token is passed fine. When I include that token in the "unsafe" request's headers (i.e. "X-CSRFToken"), however, my backend returns 403 forbidden error. The Django documentation indicates that a 403 error could be the result of not passing the correct CSRF token or that permissions are wrong. In response, I set permissions to "AllowAny" as shown in my settings.py file below. Is this an issue with passing the CSRF token incorrectly, or setting up permission incorrectly (or neither)?

My understanding is that when you use Django's CSRF middleware, it will return a cookie to the frontend. Something I thought could be a potential issue is that normally when a cookie is passed to the browser, it sets that cookie in cookie storage. My browser, however, does not store the cookie with the CSRF token in the browser's cookie storage. What could be preventing my browser from storing the cookie?

To clarify, anytime I make a PUT, POST, or DELETE HTTP request from the frontend to the backend, I get a "403 forbidden" error. Additionally, a cookie containing a CSRF Token never gets set in my browser as a result of making a GET request to the CSRF Token endpoint.

React Component: Here, it is the response to executing the "postLink" function that is often the "403 forbidden" error.

import React, { useState, useEffect, useRef } from "react";
import { useLocation } from 'react-router';
import axios from "axios";
import { useParams, Link } from 'react-router-dom';
import LightSpinner from "../Components/LightSpinner";

axios.defaults.xsrfCookieName = 'csrftoken';
axios.defaults.xsrfHeaderName = 'X-CSRFToken';
axios.defaults.withCredentials = true;

function AddLinkScreen() {
    const location = useLocation();
    const [link, setLink] = useState("");
    const [tag, setTag] = useState("")
    const [project] = useState(location.state.project)
    const { name } = useParams();
    const [isLoading, setIsLoading] = useState(false);
    const [csrfToken, setCsrfToken] = useState("");
    const isFirstRender = useRef(true);

    useEffect(() => {
        if (isFirstRender.current === true) {
            isFirstRender.current = false
            return
        } else {
            postLink({ link: link, tag: tag, project: project.id }, csrfToken)
                .then(() => { setIsLoading(false) })
                .catch((err) => { console.log(err); setIsLoading(false)})
        }
    }, [csrfToken])

    const getCsrfToken = async() => {
        await axios.get("http://127.0.0.1:8000/api/csrf/", { withCredentials: true, })
            .then((res) => { setCsrfToken(res.data.csrfToken) })
            .catch((err) => console.log(err))
    }

    const postLink = async(link, csrftkn) => {
        setIsLoading(true)
        await axios.post('http://127.0.0.1:8000/api/link/', link, {headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            'X-CSRFToken': csrftkn,
          }, withCredentials: true })
            .catch((err) => { console.log(err); setIsLoading(false)})
    }

    const onSubmitHandler = async(e) => {
        e.preventDefault()
        getCsrfToken()
    }

    return (
        <div>
                <div>
                {isLoading ? (
                    <div className="spinner-position"><LightSpinner/></div>
                ) : (
                    <div>
                    <div>
                    <button className="back-button" type="button"><Link className="link-button-remove-link" to={{ pathname: `/${name}/links/` }}>Back</Link></button>
                        <form onSubmit={onSubmitHandler}>
                            <div className="edit-link-container">
                                <label htmlFor="link">Link: </label><br></br>
                                <input className="edit-link-container-input" type="text" name="link" id="link" value={link} onChange={(event) => setLink(event.target.value)} autoComplete="off"/><br></br>
                                <label htmlFor="tag1">Tag: </label><br></br>
                                <select className="edit-link-container-input" name="tag" id="tag" onChange={(e) => setTag(e.target.value)}>
                                    <option value="" defaultValue>Tag #1</option>
                                    <option value="Website">Website</option>
                                    <option value="Discord">Discord</option>
                                    <option value="Twitter">Twitter</option>
                                    <option value="OpenSea">OpenSea</option>
                                    <option value="Youtube">Youtube</option>
                                    <option value="Facebook">Facebook</option>
                                    <option value="Instagram">Instagram</option>
                                    <option value="Other">Other</option>
                                </select><br></br>
                            </div>
                            <button className="edit-link-button" type="submit">Submit</button>
                        </form>
                    </div>
                    </div>
                )}
                </div>
        </div>
    )
}

export default AddLinkScreen;

Django get CSRF Token endpoint:

from rest_framework.decorators import api_view
from django.views.decorators.csrf import csrf_protect, ensure_csrf_cookie

@api_view(['GET'])
@ensure_csrf_cookie
def csrf(request):
        tkn = get_token(request)
        response = Response({'csrfToken': tkn})
        response.set_cookie("X-CSRFToken", tkn, samesite="lax")
        return response

Django post payload endpoint:

@api_view(['GET','POST'])
@csrf_protect
def LinkView(request):
    if request.method == 'POST':
        serializer = LinkSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=201)
        return Response(serializer.errors, status=400)

Django settings.py:

from pathlib import Path
import os

BASE_DIR = Path(__file__).resolve().parent.parent
DEBUG = True
ALLOWED_HOSTS = ['*']

STATIC_URL = '/static/'
STATIC_ROOT =  os.path.join(BASE_DIR, 'static')

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'myapp',
    'corsheaders',
    'rest_framework',
]

MIDDLEWARE = [
    'django.middleware.csrf.CsrfViewMiddleware',
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'backend.urls'
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'backend.wsgi.application'

CORS_ALLOW_CREDENTIALS = True
ACCESS_CONTROL_ALLOW_HEADERS = True
CORS_ORIGIN_ALLOW_ALL = True

CSRF_COOKIE_DOMAIN = ['*']
CSRF_TRUSTED_ORIGINS = ['localhost:3000', '127.0.0.1:8000', 'localhost', '127.0.0.1', 'http://localhost:3000', '127.0.0.1:8000', 'http://localhost', 'http://127.0.0.1']

CORS_EXPOSE_HEADERS = (
    'Access-Control-Allow-Origin',
    'set-cookie',
    "Access-Control-Expose-Headers",
    'X-CSRFToken'
)

CORS_ALLOW_METHODS = (
    'DELETE',
    'GET',
    'POST',
    'PUT',
)

CORS_ALLOW_HEADERS = (
    'accept',
    'accept-encoding',
    'authorization',
    'content-type',
    'dnt',
    'origin',
    'user-agent',
    'x-csrftoken',
    'X-CSRFToken',
    'x-requested-with',
    'Access-Control-Allow-Origin',
    'withcredentials',
    'csrfmiddlewaretoken'
)

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.SessionAuthentication',
    ),
    'DEFAULT_PERMISSION_CLASSES':(
        'rest_framework.permissions.AllowAny',
    ),
}

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]

LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

If there is a better way to restrict access to my Django API, I'd love to hear other ways of doing it.

1 Answer 1

0

you are not describing your error scenario well. is it sometimes get 403 or always get 403? if it is sometimes, when did you get 403 and when you didn't get 403?

if it is just incidental, maybe it is because you try login in different tab. so the csrf get rotated. explained here

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for the feedback. I updated the question, adding "To clarify..."
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.