Search code examples
djangodjango-rest-frameworkdjango-rest-framework-simplejwtdjango-cors-headers

DRF set_cookie does not work when frontend is on localhost and backend is on a remote server


I've created a DRF app which the backend is using jwt authentication with httpolnly cookies for authentication and it also uses a enforce_csrf for perventing csrf attacks.

from rest_framework_simplejwt.authentication import JWTAuthentication
from django.conf import settings

from rest_framework.authentication import CSRFCheck
from rest_framework import exceptions

def enforce_csrf(request):
    check = CSRFCheck()
    check.process_request(request)
    reason = check.process_view(request, None, (), {})
    print(reason)
    if reason:
         raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)

class CookieBasedJWTAuthentication(JWTAuthentication):
    def authenticate(self, request):
        header = self.get_header(request)
    
        if header is None:
            raw_token = request.COOKIES.get(settings.SIMPLE_JWT['AUTH_COOKIE_ACCESS']) or None
        else:
            raw_token = self.get_raw_token(header)
        if raw_token is None:
            return None
        
        validated_token = self.get_validated_token(raw_token)
        enforce_csrf(request)
        return self.get_user(validated_token), validated_token

to pervent csrf attacks django set a cookie and also a 'csrftokenmiddleware' and compares them.

here is the sample code for setting csrf cookie and token:

class SetCSRFToken(APIView):
    permission_classes = [AllowAny]
    def get(self, request):
        response = Response() 
        csrf.get_token(request)
        response.status_code = status.HTTP_200_OK
        csrf_secret = csrf.get_token(request)
        response.set_cookie(
            key = 'csrftoken', 
            value = request.META["CSRF_COOKIE"],
            expires = settings.SIMPLE_JWT['REFRESH_TOKEN_LIFETIME'],
            path= settings.SIMPLE_JWT['AUTH_COOKIE_PATH'],
            secure = settings.SIMPLE_JWT['AUTH_COOKIE_SECURE'],
            httponly = False,
            samesite = 'Lax'
          )
        response.data = {"status": "CSRF cookie set", 'csrfmiddlewaretoken':request.META["CSRF_COOKIE"]}

        return response

i also set :

CORS_ALLOW_ALL_ORIGINS=  True
CORS_ALLOW_CREDENTIALS = True

the code works perfect when both frontend and backend are on the localhost, but when i run the backend on a remote server, the cookies are not set but the browser receives the Response to request.

when frontend is on the same host

when backend on remote server and frontend on localhsot

this is the console result for both cases

this is the settings, I've tried using CSRF options, but still not setting the cookie, what is weired is when i call out to my apiView from browser(rather than javascript) the cookie is being set.

settings.py:

    """
Django settings for core project.

Generated by 'django-admin startproject' using Django 4.0.2.

For more information on this file, see
https://docs.djangoproject.com/en/4.0/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.0/ref/settings/
"""

from pathlib import Path
import os
from .info import *
from datetime import timedelta

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent

EMAIL_USE_TLS = EMAIL_USE_TLS
EMAIL_HOST = EMAIL_HOST
EMAIL_HOST_USER =  EMAIL_HOST_USER
EMAIL_HOST_PASSWORD =  EMAIL_HOST_PASSWORD
EMAIL_PORT =  EMAIL_PORT 
FRONTEND_URL = FRONTEND_URL

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-yzvg4k4r0%*3001&oo&up*@-yvcq(k@tpsdi^g=*ql#zvtogav'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ['myip', 'localhost', '127.0.0.1']
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = False
CSRF_COOKIE_SAMESITE = None
CSRF_TRUESTED_ORIGINS =['myip', 'localhost', '127.0.0.1'] 

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'api',
    'rest_framework',
    'corsheaders',
    'users',
    'rest_framework_simplejwt.token_blacklist',
]

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

ROOT_URLCONF = 'core.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR,'templates')],
        '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 = 'core.wsgi.application'


# Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}


# Password validation
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators

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',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/

STATIC_URL = 'static/'

# Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

CORS_ALLOW_ALL_ORIGINS=  True
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOWED_ORIGINS = [
    "http://myip:3000",
    "http://127.0.0.1:3000",
    "http://localhost:3000",
]


'''
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ),
    'DEFAULT_RENDERER_CLASSES': (
        'rest_framework.renderers.JSONRenderer',
    )
}
'''
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ]  ,
    'DEFAULT_AUTHENTICATION_CLASSES': (
        #'rest_framework_simplejwt.authentication.JWTAuthentication',
        'users.authentication.CookieBasedJWTAuthentication',
    )
}
AUTH_USER_MODEL = 'users.DearUser'

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
    'ROTATE_REFRESH_TOKENS': True,
    'BLACKLIST_AFTER_ROTATION': True,
    'UPDATE_LAST_LOGIN': False,
    'AUTH_HEADER_TYPES': ('Bearer','JWT'),
    'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
    'ALGORITHM': 'HS256',
    'SIGNING_KEY': SECRET_KEY,
    'USER_ID_FIELD': 'email',
    'USER_ID_CLAIM': 'email',


    # cookie based jwt settings
    'AUTH_COOKIE_ACCESS':'ACCESS',
    'AUTH_COOKIE_REFRESH':'REFRESH',
    'AUTH_COOKIE_SECURE': False, 
    'AUTH_COOKIE_HTTP_ONLY' : True,
    'AUTH_COOKIE_PATH': '/',
    'AUTH_COOKIE_SAMESITE': 'None', #Strict

}

Solution

  • Found it! First make sure you got everything right in this answer: https://stackoverflow.com/a/46412839/18327111

    because django corsheaders middleware is checking the below if, make sure to have the following settings:

    if conf.CORS_ALLOW_ALL_ORIGINS and not conf.CORS_ALLOW_CREDENTIALS:
        response[ACCESS_CONTROL_ALLOW_ORIGIN] = "*"
    else:
        response[ACCESS_CONTROL_ALLOW_ORIGIN] = origin
    

    new settings:

    from corsheaders.defaults import default_headers
    CORS_ALLOW_ALL_ORIGINS=  False
    CORS_ALLOW_CREDENTIALS = True
    CORS_ALLOWED_ORIGINS = [
        "http://yourip_or_domain:3000",
        "http://127.0.0.1:3000",
        "http://localhost:3000",
    ]
    #making sure CORS_ALLOW_HEADERS  is not "*"
    CORS_ALLOW_HEADERS = list(default_headers) + ['Set-Cookie']
    

    in case you are not using django session authentication(as i do) and want to bypass it add the following settings

    CSRF_USE_SESSIONS = False
    SESSION_COOKIE_SECURE = False
    CSRF_COOKIE_SECURE = False
    CSRF_COOKIE_SAMESITE = None
    SESSION_COOKIE_SAMESITE = None
    

    and the main reason it failed: this is copied from chrome developer tools and mozila also have this warning:

    Because a cookie’s SameSite attribute was not set or is invalid, it defaults to SameSite=Lax, which prevents the cookie from being sent in a cross-site request. This behavior protects user data from accidentally leaking to third parties and cross-site request forgery. Resolve this issue by updating the attributes of the cookie:

    Specify SameSite=None and Secure if the cookie should be sent in cross-site requests. This enables third-party use.

    Specify SameSite=Strict or SameSite=Lax if the cookie should not be sent in cross-site requests.

    so if you want to access cross origin, the only way is using an https and SameSite=None, otherwise you have to deploy your api and backend on the same domain

    Note that http and https are considered as different domain and are cross-origin https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#samesitenone_requires_secure

    Cookies from the same domain are no longer considered to be from the same site if sent using a different scheme (http: or https:).