CORS Configuration Guide
Overview
Section titled “Overview”CORS (Cross-Origin Resource Sharing) configuration for multi-site authentication system.
Main Site Configuration
Section titled “Main Site Configuration”Frontend (gigmanager.local)
Section titled “Frontend (gigmanager.local)”// All API calls must include credentialsfetch('https://api.gigmanager.local/endpoint', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` }, credentials: 'include', // Required for cookies body: JSON.stringify(data)})Backend (api.gigmanager.local)
Section titled “Backend (api.gigmanager.local)”// Fastify CORS pluginfastify.register(cors, { origin: 'https://gigmanager.local:4202', // Exact origin (no wildcard with credentials) credentials: true, // Allow cookies methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], allowedHeaders: ['Content-Type', 'Authorization']})
// Cookie settingsres.cookie('sessionId', sessionId, { httpOnly: true, // JavaScript can't read secure: true, // HTTPS only sameSite: 'lax', // CSRF protection domain: '.gigmanager.local', // Works across subdomains maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days})Satellite Site Configuration
Section titled “Satellite Site Configuration”Frontend (any domain)
Section titled “Frontend (any domain)”// No credentials needed - headers onlyfetch('https://api.gigmanager.local/endpoint', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'X-Session-ID': sessionId // Only for refresh endpoint }, body: JSON.stringify(data)})Backend (api.gigmanager.local)
Section titled “Backend (api.gigmanager.local)”// CORS for satellites - allow multiple originsconst allowedOrigins = [ 'https://gigmanager.local:4202', // Main site 'https://client1domain.com', // Satellite 1 'https://client2domain.com', // Satellite 2 // Add more as needed]
fastify.register(cors, { origin: (origin, callback) => { if (!origin || allowedOrigins.includes(origin)) { callback(null, true) } else { callback(new Error('Not allowed by CORS')) } }, credentials: false, // No cookies for satellites methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], allowedHeaders: ['Content-Type', 'Authorization', 'X-Session-ID']})Environment-Specific Configuration
Section titled “Environment-Specific Configuration”Development
Section titled “Development”const isDevelopment = process.env.NODE_ENV === 'development'
const allowedOrigins = isDevelopment ? [ 'https://gigmanager.local:4202', 'https://api.gigmanager.local:4000', 'http://localhost:4202', // Fallback for non-HTTPS testing 'http://localhost:4000' ] : [ 'https://example.com', 'https://api.example.com', 'https://client1domain.com' ]Production
Section titled “Production”// Production uses environment variablesconst allowedOrigins = process.env.ALLOWED_ORIGINS.split(',')// ALLOWED_ORIGINS=https://example.com,https://client1domain.com,https://client2domain.comCookie SameSite Attribute
Section titled “Cookie SameSite Attribute”Values
Section titled “Values”- Strict: Cookie never sent cross-site (breaks satellite functionality)
- Lax: Cookie sent on top-level navigation, not on API calls from other sites (RECOMMENDED)
- None: Cookie sent everywhere (requires CSRF protection, increasingly blocked)
Our Usage
Section titled “Our Usage”// Main site - Lax is perfectsameSite: 'lax' // Prevents CSRF, allows same-site subdomains
// Would need 'none' only if:// - Main site AND satellites both needed cookies (we don't do this)// - Embedding in iframes (we don't do this)Headers Reference
Section titled “Headers Reference”Request Headers
Section titled “Request Headers”Main Site:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Content-Type: application/jsonCookie: sessionId=550e8400-e29b-41d4-a716-446655440000Origin: https://gigmanager.local:4202Satellite Site:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Content-Type: application/jsonX-Session-ID: 550e8400-e29b-41d4-a716-446655440000Origin: https://client1domain.comResponse Headers
Section titled “Response Headers”Main Site:
Access-Control-Allow-Origin: https://gigmanager.local:4202Access-Control-Allow-Credentials: trueAccess-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCHAccess-Control-Allow-Headers: Content-Type, AuthorizationSet-Cookie: sessionId=...; HttpOnly; Secure; SameSite=LaxSatellite Site:
Access-Control-Allow-Origin: https://client1domain.comAccess-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCHAccess-Control-Allow-Headers: Content-Type, Authorization, X-Session-IDPreflight Requests (OPTIONS)
Section titled “Preflight Requests (OPTIONS)”Browsers send OPTIONS preflight for:
- Custom headers (X-Session-ID)
- Methods other than GET/POST
- Content-Type other than simple values
Server must respond:
// Fastify CORS plugin handles this automatically// Manual implementation:fastify.options('*', async (request, reply) => { reply .header('Access-Control-Allow-Origin', request.headers.origin) .header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH') .header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Session-ID') .header('Access-Control-Max-Age', '86400') // Cache preflight for 24 hours .send()})Common Issues & Solutions
Section titled “Common Issues & Solutions”Issue: Cookies not being set
Section titled “Issue: Cookies not being set”Symptoms: Login succeeds but refresh fails, no cookie in browser Causes:
- Missing
credentials: 'include'in fetch - Missing
credentials: truein CORS config - Origin mismatch (exact match required)
- Not using HTTPS (Secure flag requires it)
- SameSite=None without Secure flag
Solution:
- Verify both client and server settings
- Check browser dev tools Network tab for Set-Cookie header
- Ensure HTTPS in development
Issue: CORS error on preflight
Section titled “Issue: CORS error on preflight”Symptoms: OPTIONS request fails, actual request never sent Causes:
- Missing custom header in allowedHeaders
- Origin not in allowed list
- Credentials true but origin is wildcard
Solution:
- Add all custom headers to allowedHeaders array
- Use exact origin, not ’*’
- Enable CORS logging to debug
Issue: Cookie sent but not read
Section titled “Issue: Cookie sent but not read”Symptoms: Cookie visible in Application tab but server doesn’t see it Causes:
- Cookie domain mismatch
- httpOnly cookie accessed from JavaScript
- SameSite too strict
Solution:
- Check cookie domain matches request domain
- Server should read from req.cookies, not client
- Use SameSite=Lax not Strict
Issue: Satellite can’t authenticate
Section titled “Issue: Satellite can’t authenticate”Symptoms: 401 errors from satellite sites Causes:
- Trying to use credentials: ‘include’ (don’t do this)
- sessionId not in localStorage
- X-Session-ID header not sent
Solution:
- Remove credentials: ‘include’ for satellites
- Verify localStorage has sessionId
- Add X-Session-ID to headers explicitly
Testing CORS Locally
Section titled “Testing CORS Locally”Verify Main Site
Section titled “Verify Main Site”# Login and check cookie is setcurl -i -X POST https://api.gigmanager.local:4000/api/login \ -H "Content-Type: application/json" \ -H "Origin: https://gigmanager.local:4202" \ -d '{"email":"test@example.com","password":"pass","source":"main"}' \ --cookie-jar cookies.txt
# Should see Set-Cookie header
# Test authenticated requestcurl -i https://api.gigmanager.local:4000/api/data \ -H "Authorization: Bearer {accessToken}" \ -H "Origin: https://gigmanager.local:4202" \ --cookie cookies.txt
# Should include cookie and succeedVerify Satellite
Section titled “Verify Satellite”# Login from satellitecurl -i -X POST https://api.gigmanager.local:4000/api/login \ -H "Content-Type: application/json" \ -H "Origin: https://client1domain.com" \ -d '{"email":"test@example.com","password":"pass","source":"satellite"}'
# Should return sessionId in body (not cookie)
# Test with headerscurl -i https://api.gigmanager.local:4000/api/data \ -H "Authorization: Bearer {accessToken}" \ -H "X-Session-ID: {sessionId}" \ -H "Origin: https://client1domain.com"
# Should succeedSecurity Checklist
Section titled “Security Checklist”- Main site uses
credentials: 'include' - Satellites do NOT use
credentials: 'include' - Cookie has
httpOnly: true - Cookie has
secure: true(HTTPS only) - Cookie has
sameSite: 'lax' - CORS origin is exact match (not wildcard when using credentials)
- Custom headers (X-Session-ID) in allowedHeaders
- Sensitive operations check for cookie presence
- Environment-specific allowed origins
- HTTPS in development matches production behavior