Complete Guide to Fixing Cross-Domain Cookie Issues in Django Microservices

Cross-Domain Cookie Authentication Fix - Complete Guide

🔐 Cross-Domain Cookie Authentication Fix

Complete Guide to Fixing Cross-Domain Cookie Issues in Django Microservices

DjangoDockerKong Gateway VercelJWTCORS Microservices

🧩Global Variables (edit once, reuse everywhere)

# Replace example values with yours
# These placeholders are shown in all snippets below.

# Frontend
FRONTEND_URL="https://gmb-front-end-wl.vercel.app"
VERCEL_PREVIEW_URL="https://fe-wl-gmb-fv4x.vercel.app"

# Backend & Gateway
BACKEND_URL="https://wlbegmb.mbgtesting.site"
KONG_PUBLIC_URL="https://wlbegmb.mbgtesting.site"
KONG_PORT="8008"

# Cookies
COOKIE_PARENT_DOMAIN=".mbgtesting.site"
ACCESS_TOKEN_COOKIE_NAME="user_access_token"
REFRESH_TOKEN_COOKIE_NAME="user_refresh_token"

# Tenancy / Headers
TENANT_DOMAIN="tenanttestfix.com"

# CORS
CORS_ALLOW_ALL_ORIGINS=True

# Misc (examples)
ALLOWED_HOSTS="69.62.79.122,localhost,127.0.0.1"
GOOGLE_OAUTH_CLIENT_ID="${GOOGLE_OAUTH_CLIENT_ID}"
GOOGLE_OAUTH_CLIENT_SECRET="${GOOGLE_OAUTH_CLIENT_SECRET}"
GOOGLE_OAUTH_REDIRECT_URI="${GOOGLE_OAUTH_REDIRECT_URI}"
FRONTEND_URL VERCEL_PREVIEW_URL BACKEND_URL KONG_PUBLIC_URL KONG_PORT COOKIE_PARENT_DOMAIN ACCESS_TOKEN_COOKIE_NAME REFRESH_TOKEN_COOKIE_NAME TENANT_DOMAIN CORS_ALLOW_ALL_ORIGINS ALLOWED_HOSTS

🚨Problem Statement

❌ The Challenge

When deploying a Django-based microservices application with:

  • Frontend: Hosted on Vercel (${FRONTEND_URL})
  • Backend: Hosted on different domain (${BACKEND_URL})
  • Authentication: JWT-based with HTTP-only cookies

Result: Authentication fails because browsers block cross-domain cookies by default.

🔍 Why This Happens

  1. Same-Origin Policy: Browsers restrict cookies to same domain
  2. CORS Restrictions: Cross-origin requests blocked without proper headers
  3. Cookie Security: Modern browsers require specific cookie attributes for cross-domain

🏗️Architecture Overview

System Architecture

┌─────────────────────────────────────────────────────────────────┐
│                        Production Environment                   │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Frontend (Vercel)                    Backend (Custom Domain)  │
│  ┌─────────────────┐                  ┌─────────────────────┐  │
│  │                 │                  │                     │  │
│  │ ${FRONTEND_URL} │ ◄─── HTTP ─────► │ ${BACKEND_URL}      │  │
│  │                 │                  │                     │  │
│  └─────────────────┘                  └─────────────────────┘  │
│           │                                    │                │
│           │                                    ▼                │
│           │                        ┌─────────────────────┐      │
│           │                        │   Kong Gateway      │      │
│           │                        │   (Port ${KONG_PORT}) │      │
│           │                        └─────────────────────┘      │
│           │                                    │                │
│           │                                    ▼                │
│           │                        ┌─────────────────────┐      │
│           │                        │  Django Services    │      │
│           │                        │  - Auth Service     │      │
│           │                        │  - Tenant Service   │      │
│           │                        │  - Core Service     │      │
│           │                        └─────────────────────┘      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Before: Broken Implementation

1. JWT Cookie Configuration (Broken)

# services/auth-service/core/settings.py
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(days=1),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
    'ROTATE_REFRESH_TOKENS': True,
    'BLACKLIST_AFTER_ROTATION': True,
    'ALGORITHM': 'HS256',
    'SIGNING_KEY': JWT_SECRET_KEY,
    'AUTH_HEADER_TYPES': ('Bearer',),
    'AUTH_COOKIE': '${ACCESS_TOKEN_COOKIE_NAME}',
    'AUTH_COOKIE_REFRESH': '${REFRESH_TOKEN_COOKIE_NAME}',
    'AUTH_COOKIE_DOMAIN': None,        # ❌ No cross-domain support
    'AUTH_COOKIE_SECURE': False,       # ❌ Not secure for HTTPS
    'AUTH_COOKIE_HTTP_ONLY': True,
    'AUTH_COOKIE_PATH': '/',
    'AUTH_COOKIE_SAMESITE': 'Lax',    # ❌ Blocks cross-domain
}

2. CORS Configuration (Incomplete)

# services/auth-service/core/settings.py
CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000",
    "http://127.0.0.1:3000",
    "http://localhost:8000",
    "http://127.0.0.1:8000",
    "http://localhost:5173",
    "http://127.0.0.1:5173",
    "http://tenanta.com",
    "http://tenanta.com:8000",
    "http://tenanta.com:5173",
    "${VERCEL_PREVIEW_URL}",
    "${BACKEND_URL}",
    # ❌ Missing: "${FRONTEND_URL}"
]

3. Environment Variables (Missing)

# docker-compose.yml - Auth Service
environment:
  - GMB_DB_NAME=mbg_gmb_dev
  - GMB_DB_USER=mbg_gmb_dev
  - GMB_DB_PASSWORD=********
  - GMB_DB_HOST=182.70.246.120
  - GMB_DB_PORT=5432
  - TENANT_SERVICE_URL=http://gmb-tenant-service-prod:8000
  - REDIS_URL=redis://:********@redis:6379/0
  - SECRET_KEY=********
  - JWT_SECRET_KEY=********
  - DEBUG=True
  - ALLOWED_HOSTS=${ALLOWED_HOSTS}
  - GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID}
  - GOOGLE_OAUTH_CLIENT_SECRET=${GOOGLE_OAUTH_CLIENT_SECRET}
  - GOOGLE_OAUTH_REDIRECT_URI=${GOOGLE_OAUTH_REDIRECT_URI}
  # ❌ Missing: FRONTEND_URL and CORS_ALLOW_ALL_ORIGINS

After: Working Solution

1. JWT Cookie Configuration (Fixed)

# services/auth-service/core/settings.py
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(days=1),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
    'ROTATE_REFRESH_TOKENS': True,
    'BLACKLIST_AFTER_ROTATION': True,
    'ALGORITHM': 'HS256',
    'SIGNING_KEY': JWT_SECRET_KEY,
    'AUTH_HEADER_TYPES': ('Bearer',),
    'AUTH_COOKIE': '${ACCESS_TOKEN_COOKIE_NAME}',
    'AUTH_COOKIE_REFRESH': '${REFRESH_TOKEN_COOKIE_NAME}',
    'AUTH_COOKIE_DOMAIN': '${COOKIE_PARENT_DOMAIN}',  # ✅ Cross-domain
    'AUTH_COOKIE_SECURE': True,                            # ✅ HTTPS
    'AUTH_COOKIE_HTTP_ONLY': True,
    'AUTH_COOKIE_PATH': '/',
    'AUTH_COOKIE_SAMESITE': 'None',                       # ✅ Cross-domain allowed
}

2. CORS Configuration (Complete)

# services/auth-service/core/settings.py
CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000",
    "http://127.0.0.1:3000",
    "http://localhost:8000",
    "http://127.0.0.1:8000",
    "http://localhost:5173",
    "http://127.0.0.1:5173",
    "${FRONTEND_URL}",               # ✅ Main Vercel domain
    "${VERCEL_PREVIEW_URL}",         # ✅ Preview
    "${BACKEND_URL}",                # If needed
]

# Allow any tenant domain for CORS
CORS_ALLOW_ALL_ORIGINS = ${CORS_ALLOW_ALL_ORIGINS}
CORS_ALLOW_CREDENTIALS = True

3. Environment Variables (Complete)

# docker-compose.yml - Auth Service
environment:
  - GMB_DB_NAME=mbg_gmb_dev
  - GMB_DB_USER=mbg_gmb_dev
  - GMB_DB_PASSWORD=********
  - GMB_DB_HOST=182.70.246.120
  - GMB_DB_PORT=5432
  - TENANT_SERVICE_URL=http://gmb-tenant-service-prod:8000
  - REDIS_URL=redis://:********@redis:6379/0
  - SECRET_KEY=********
  - JWT_SECRET_KEY=********
  - DEBUG=True
  - ALLOWED_HOSTS=${ALLOWED_HOSTS}
  - GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID}
  - GOOGLE_OAUTH_CLIENT_SECRET=${GOOGLE_OAUTH_CLIENT_SECRET}
  - GOOGLE_OAUTH_REDIRECT_URI=${GOOGLE_OAUTH_REDIRECT_URI}
  - FRONTEND_URL=${FRONTEND_URL}                  # ✅ Added
  - CORS_ALLOW_ALL_ORIGINS=${CORS_ALLOW_ALL_ORIGINS}  # ✅ Added

🔧Technical Details

Cookie Domain Strategy

Key: Use a parent domain that encompasses all backend subdomains.

'AUTH_COOKIE_DOMAIN': '${COOKIE_PARENT_DOMAIN}'  # Leading dot

# Allows cookies to be shared across:
# - ${BACKEND_URL} (backend)
# - Any subdomain of ${COOKIE_PARENT_DOMAIN}
# - Cross-domain requests from ${FRONTEND_URL}

SameSite Cookie Attribute

# Before: 'Lax' - Blocks cross-domain cookies
'AUTH_COOKIE_SAMESITE': 'Lax'

# After: 'None' - Allows cross-domain cookies
'AUTH_COOKIE_SAMESITE': 'None'

# Note: SameSite=None requires Secure=True for HTTPS
'AUTH_COOKIE_SECURE': True

CORS Credentials Flow

# 1. Frontend sends request with credentials
fetch(`${BACKEND_URL}/api/v1/auth/login/`, {
  method: 'POST',
  credentials: 'include',    # ✅ Critical for cookies
  headers: {
    'Content-Type': 'application/json',
    'X-Tenant-Domain': `${TENANT_DOMAIN}`
  },
  body: JSON.stringify(loginData)
})

# 2. Backend responds with proper CORS headers
response['Access-Control-Allow-Origin'] = origin   # ✅ Specific origin
response['Access-Control-Allow-Credentials'] = 'true'  # ✅ Required for cookies

# 3. Browser accepts cookies from different domain

🧪Testing & Verification

1. Test Cross-Domain Cookie Setting

curl -X POST ${KONG_PUBLIC_URL}:${KONG_PORT}/api/v1/auth/login/ \ -H "Content-Type: application/json" \ -H "X-Tenant-Domain: ${TENANT_DOMAIN}" \ -H "Origin: ${FRONTEND_URL}" \ -d '{"email": "admintestfix@gmail.com", "password": "testpass123"}' \ -v

2. Expected Response Headers

HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: ${FRONTEND_URL}
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With, X-Tenant-Domain, X-Google-Access, X-Gmb-Access
Set-Cookie: ${ACCESS_TOKEN_COOKIE_NAME}=...; Domain=${COOKIE_PARENT_DOMAIN}; SameSite=None; Secure; HttpOnly

3. Verify Cookie Properties

// In browser console on ${FRONTEND_URL}
document.cookie
// Should show: ${ACCESS_TOKEN_COOKIE_NAME}=... (if cookies are working)

// Check cookie attributes
// Domain: ${COOKIE_PARENT_DOMAIN}
// SameSite: None
// Secure: true
// HttpOnly: true

🚀Deployment Guide

Step-by-Step Deployment

  1. Update Django Settings
    vim services/auth-service/core/settings.py
  2. Update JWT Configuration
    SIMPLE_JWT = {
      'AUTH_COOKIE_DOMAIN': '${COOKIE_PARENT_DOMAIN}',
      'AUTH_COOKIE_SECURE': True,
      'AUTH_COOKIE_SAMESITE': 'None',
    }
  3. Add Vercel Domains to CORS
    CORS_ALLOWED_ORIGINS = [
      "${FRONTEND_URL}",
      "${VERCEL_PREVIEW_URL}",
    ]
  4. Rebuild and Deploy
    docker compose down
    docker compose up -d --build

📚Git Workflow & Version Control

Commit Structure

# Commit message format
git commit -m "Cross-Domain Cookies issue fix (variable-ized guide)"

# Files changed in this commit:
# - docker-compose.yml (environment variables)
# - services/auth-service/core/cors_middleware.py (CORS handling)
# - services/auth-service/core/settings.py (JWT & CORS config)
# - services/tenant-service/core/settings.py (CORS origins)
# - docs/this-guide.html (variables added)

Branch Strategy

# Current branch: dev
git branch
# * dev

# Push to remote
git push origin dev

Rollback Strategy

# If you need to rollback to previous version
git log --oneline
git reset --hard <previous_commit_sha>

🎯Production Deployment Checklist

Pre-Deployment Verification

  • Local testing completed with Vercel domain simulation
  • Cookie attributes verified (Domain, SameSite, Secure)
  • CORS headers confirmed for production domains
  • Environment variables properly set
  • Git changes committed and pushed

Production Environment Setup

  • Backend domain: ${BACKEND_URL}
  • Frontend domain: ${FRONTEND_URL}
  • HTTPS certificates properly configured
  • Environment variables updated in production
  • Database connections verified

Deployment Steps

# 1. Pull latest changes
git pull origin dev

# 2. Stop production services
docker compose down

# 3. Rebuild with new configuration
docker compose up -d --build

# 4. Verify all services are running
docker compose ps

# 5. Test production endpoints
curl -X POST ${BACKEND_URL}/api/v1/auth/login/ \ -H "Origin: ${FRONTEND_URL}" \ -H "X-Tenant-Domain: ${TENANT_DOMAIN}" \ -d '{"email": "test@example.com", "password": "password"}' \ -v

📚Key Learnings

🔑 Critical Insights

  1. Cookie Domain Strategy: Use parent domain (${COOKIE_PARENT_DOMAIN})
  2. SameSite Cookie: None + Secure=True for cross-domain
  3. CORS Credentials: Access-Control-Allow-Credentials: true is mandatory
  4. Middleware Order: CORS middleware must be first in Django MIDDLEWARE
  5. Env Separation: Distinguish dev vs prod CORS and cookie settings

Post a Comment

Previous Post Next Post