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.