CORS Configuration

Understanding and Implementing Cross-Origin Resource Sharing

What is CORS?

CORS (Cross-Origin Resource Sharing) is a security feature implemented by web browsers that restricts web pages from making requests to a different domain than the one that served the web page. This is known as the Same-Origin Policy.

To understand CORS, let's use a real-world analogy: Think of the Same-Origin Policy as a building security system. If you work in Company A, you can freely access Company A's offices, use their meeting rooms, and access their documents. However, you cannot enter Company B's offices in the same building without explicit permission, even though both companies share the building.

CORS is like having a visitor pass system that allows certain visitors from other companies to access specific areas of your office, based on pre-defined rules.

flowchart LR A[Browser] --> B{Same Origin?} B -->|Yes| C[Request Proceeds Normally] B -->|No| D{CORS Headers Present?} D -->|Yes| E[Request Allowed] D -->|No| F[Request Blocked]

Why does CORS exist?

CORS exists for security reasons. Without these restrictions, malicious websites could make requests to other domains on behalf of users, potentially accessing or manipulating sensitive data. For example, a malicious site could make requests to your banking website if you're logged in, performing unauthorized transactions.

However, legitimate web applications often need to make cross-origin requests. For instance, a frontend React application running on app.example.com might need to fetch data from an API at api.example.com. CORS provides a controlled mechanism to enable these legitimate cross-origin requests while maintaining security.

How CORS Works

When a browser makes a cross-origin request, it automatically adds an Origin header to the request, indicating the origin of the requesting page. The server then checks this header and decides whether to allow the request by sending appropriate CORS headers in the response.

Browser Frontend App app.example.com API Server api.example.com 1. Request with Origin: app.example.com 2. Response with CORS headers

The Main CORS Headers

Simple vs. Preflight Requests

CORS requests fall into two categories:

Simple Requests

Simple requests are sent directly to the server without a "preflight" check. A request is considered simple when it meets all these conditions:

Simple CORS Request & Response Example


// Browser automatically adds:
GET /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com

// Server responds with:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json

{"data": "This is accessible cross-origin"}
                

Preflight Requests

For more complex requests, browsers send a preflight request using the OPTIONS method to check if the actual request is permitted. This happens for requests that:

Preflight CORS Request & Response Example


// Preflight request (automatic by browser before actual request)
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization

// Preflight response from server
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

// Only after successful preflight, the actual request is sent
PUT /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/json
Authorization: Bearer token123

{"name": "Updated data"}
                

Think of a preflight request like calling ahead before visiting a restricted office area. You ask, "I'd like to bring my team and equipment to use your conference room. Is that allowed?" The office manager checks your request against their policies before granting or denying permission.

Implementing CORS in Express.js

In Express.js applications, implementing CORS is straightforward using the cors middleware. Let's explore different configuration approaches from basic to advanced.

Basic CORS Implementation


const express = require('express');
const cors = require('cors');
const app = express();

// Enable CORS for all routes with default settings
app.use(cors());

app.get('/api/data', (req, res) => {
  res.json({ message: 'This API is accessible from any origin' });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});
                

This basic implementation allows requests from any origin. It's equivalent to setting Access-Control-Allow-Origin: *. While simple, this is generally not recommended for production environments as it's too permissive.

Configuring Specific Origins


const express = require('express');
const cors = require('cors');
const app = express();

// Allow specific origin
const corsOptions = {
  origin: 'https://app.example.com',
  optionsSuccessStatus: 200 // For legacy browser support
};

app.use(cors(corsOptions));

app.get('/api/data', (req, res) => {
  res.json({ message: 'This API is accessible only from app.example.com' });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});
                

This configuration only allows requests from the specific origin https://app.example.com. Any requests from other origins will be blocked by the browser.

Allowing Multiple Origins


const express = require('express');
const cors = require('cors');
const app = express();

// Allow multiple specific origins
const corsOptions = {
  origin: ['https://app.example.com', 'https://admin.example.com'],
  optionsSuccessStatus: 200
};

// Or with a regex pattern
const corsOptionsWithRegex = {
  origin: /\.example\.com$/, // Allows all subdomains of example.com
  optionsSuccessStatus: 200
};

app.use(cors(corsOptions));

app.get('/api/data', (req, res) => {
  res.json({ message: 'This API is accessible from allowed origins' });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});
                

This approach allows you to specify multiple trusted origins, either as an array or using a regular expression pattern.

Dynamic Origin Validation


const express = require('express');
const cors = require('cors');
const app = express();

// Dynamic origin validation
const corsOptions = {
  origin: function (origin, callback) {
    // Allow requests with no origin (like mobile apps or curl requests)
    if (!origin) return callback(null, true);
    
    const allowedOrigins = [
      'https://app.example.com',
      'https://admin.example.com',
      /\.example\.net$/
    ];
    
    // Check if origin is allowed
    let allowed = false;
    for (let allowedOrigin of allowedOrigins) {
      if (typeof allowedOrigin === 'string' && origin === allowedOrigin) {
        allowed = true;
        break;
      } else if (allowedOrigin instanceof RegExp && allowedOrigin.test(origin)) {
        allowed = true;
        break;
      }
    }
    
    if (allowed) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  optionsSuccessStatus: 200
};

app.use(cors(corsOptions));

app.get('/api/data', (req, res) => {
  res.json({ message: 'This API is accessible from dynamically validated origins' });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});
                

This advanced approach gives you complete control over which origins are allowed. The function receives the requesting origin and must call the callback with either an error (to deny) or true (to allow). This is particularly useful for dynamic environments where allowed origins might change.

Route-Specific CORS Configuration


const express = require('express');
const cors = require('cors');
const app = express();

// Default CORS configuration (stricter)
const defaultCorsOptions = {
  origin: 'https://app.example.com'
};

// Apply default CORS to all routes
app.use(cors(defaultCorsOptions));

// Public API with more permissive CORS
const publicApiCors = cors({
  origin: '*' // Allow any origin
});

app.get('/api/public', publicApiCors, (req, res) => {
  res.json({ message: 'This is public data accessible from anywhere' });
});

// Admin API with stricter CORS
const adminApiCors = cors({
  origin: 'https://admin.example.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true // Allow cookies
});

app.get('/api/admin', adminApiCors, (req, res) => {
  res.json({ message: 'This is admin data with restricted access' });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});
                

This configuration allows different CORS policies for different routes. This is useful when your API has both public endpoints that should be widely accessible and restricted endpoints that should only be accessible from specific origins.

Advanced CORS Options

Handling Credentials (Cookies)

By default, browsers don't send cookies in cross-origin requests. To allow credentials (cookies, HTTP authentication, client certificates), you need to enable the credentials option:


const express = require('express');
const cors = require('cors');
const app = express();

const corsOptions = {
  origin: 'https://app.example.com',
  credentials: true // Allow credentials
};

app.use(cors(corsOptions));

app.get('/api/user', (req, res) => {
  // Can now access session cookies
  res.json({ user: req.user });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});
                

On the frontend side, you also need to include credentials in your fetch/XHR requests:


// Using fetch with credentials
fetch('https://api.example.com/api/user', {
  credentials: 'include'
})
.then(response => response.json())
.then(data => console.log(data));

// Using axios with credentials
axios.get('https://api.example.com/api/user', { 
  withCredentials: true 
})
.then(response => console.log(response.data));
                

Important Note: When setting credentials: true, the value of Access-Control-Allow-Origin cannot be '*'. It must be a specific origin.

Configuring Preflight Cache Duration

You can control how long browsers cache preflight results using the maxAge option:


const corsOptions = {
  origin: 'https://app.example.com',
  maxAge: 86400, // 24 hours in seconds
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization']
};

app.use(cors(corsOptions));
                

This reduces the number of preflight requests for improved performance, as browsers will cache the preflight response for the specified duration.

Exposing Custom Headers

If your API sends custom headers that the frontend needs to access, you must explicitly expose them:


const corsOptions = {
  origin: 'https://app.example.com',
  exposedHeaders: ['X-Total-Count', 'X-Rate-Limit']
};

app.use(cors(corsOptions));

app.get('/api/items', (req, res) => {
  // Frontend can now access these custom headers
  res.set('X-Total-Count', '42');
  res.set('X-Rate-Limit', '100');
  res.json({ items: [] });
});
                

This allows the frontend JavaScript to read these headers with response.headers.get('X-Total-Count').

Environment-Specific CORS Configuration

In real-world applications, you often need different CORS settings for different environments (development, staging, production):


const express = require('express');
const cors = require('cors');
const app = express();

// Configuration based on environment
let corsOptions;

if (process.env.NODE_ENV === 'production') {
  corsOptions = {
    origin: ['https://app.example.com', 'https://admin.example.com'],
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    credentials: true,
    maxAge: 86400
  };
} else if (process.env.NODE_ENV === 'staging') {
  corsOptions = {
    origin: [/\.example-staging\.com$/, /\.example-dev\.com$/],
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
    allowedHeaders: ['Content-Type', 'Authorization', 'X-Debug'],
    credentials: true,
    maxAge: 3600
  };
} else {
  // Development: allow local development servers
  corsOptions = {
    origin: ['http://localhost:3000', 'http://localhost:8080'],
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
    allowedHeaders: ['Content-Type', 'Authorization', 'X-Debug'],
    credentials: true,
    maxAge: 60 // Short cache for rapid development
  };
}

app.use(cors(corsOptions));

app.get('/api/data', (req, res) => {
  res.json({ message: 'Data accessible with environment-specific CORS' });
});

app.listen(3000, () => {
  console.log(`Server running in ${process.env.NODE_ENV || 'development'} mode on port 3000`);
});
                

This approach ensures appropriate security in production while allowing flexibility during development.

Common CORS Issues and Troubleshooting

Preflight Request Failures

A common issue is when preflight OPTIONS requests receive a 404 or 401 status because the server isn't configured to handle them:


// Explicitly handle OPTIONS requests for all routes
app.options('*', cors(corsOptions));

// Or for a specific route
app.options('/api/sensitive-data', cors(corsOptions));
                

Credentials with Wildcard Origin

If you're using credentials, you cannot use origin: '*':


// This will not work
const corsOptions = {
  origin: '*',
  credentials: true // Incompatible with wildcard origin
};

// Instead, use a specific origin or a function
const correctCorsOptions = {
  origin: 'https://app.example.com',
  credentials: true
};

// Or if you need multiple origins with credentials
const dynamicCorsOptions = {
  origin: function(origin, callback) {
    const allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
    if (allowedOrigins.includes(origin) || !origin) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true
};
                

Missing Required Headers

If your frontend is sending custom headers, make sure they're included in the allowedHeaders option:


const corsOptions = {
  origin: 'https://app.example.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: [
    'Content-Type', 
    'Authorization', 
    'X-Requested-With', 
    'X-Custom-Header'
  ]
};
                

Debugging CORS Issues

When troubleshooting CORS issues, the browser's developer console is your best friend. It will explicitly show CORS errors with details about what's missing or incorrect.

Common Browser Error Messages and Solutions

  • Error: "The 'Access-Control-Allow-Origin' header has a value that is not equal to the supplied origin."
    Solution: Make sure your CORS configuration includes the specific origin making the request.
  • Error: "Request header field X-Custom-Header is not allowed by Access-Control-Allow-Headers."
    Solution: Add the header to your allowedHeaders array.
  • Error: "The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'."
    Solution: Use a specific origin instead of '*' when credentials: true.
  • Error: "Method PUT is not allowed by Access-Control-Allow-Methods."
    Solution: Add the HTTP method to your methods array.

A helpful approach for debugging is to temporarily enable a more permissive CORS policy and then incrementally add restrictions until you identify the specific issue.

CORS Best Practices

Security Considerations

Performance Considerations

Real-World Example: Microservices Architecture

In a microservices architecture, different services might run on different domains:

  • Frontend: app.example.com
  • User Service: users-api.example.com
  • Products Service: products-api.example.com
  • Orders Service: orders-api.example.com

Each service needs to configure CORS to allow requests from the frontend domain. Additionally, if services communicate with each other client-side (e.g., the frontend directly calls multiple services), each service must allow the frontend origin.

This is a perfect use case for environment-specific configurations, as you might have different domain structures in development, staging, and production environments.

Alternative Approaches to Cross-Origin Communication

Proxy Server

Instead of configuring CORS, you can use a proxy server. This is particularly useful during development:

Using Create React App's Proxy Feature


// In package.json
{
  "name": "my-react-app",
  "version": "0.1.0",
  "proxy": "http://localhost:3001"
}
                

This forwards any unknown requests to your API server, avoiding CORS issues during development.

Using a Development Proxy in Vite


// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3001',
        changeOrigin: true
      }
    }
  }
}
                

Backend for Frontend (BFF) Pattern

The BFF pattern involves creating a dedicated backend service that sits between your frontend and other services. This service, which shares the same origin as your frontend, acts as a proxy:

flowchart LR A[Frontend\napp.example.com] --> B[BFF\napp.example.com/api] B --> C[Service A\napi-a.example.com] B --> D[Service B\napi-b.example.com] B --> E[Service C\napi-c.example.com]

Since the BFF shares the same origin as the frontend, there are no CORS issues. The BFF handles all cross-origin communication with other services server-side.

JSONP (Historical Approach)

JSONP is an older technique that exploits the fact that <script> tags aren't subject to the Same-Origin Policy. It's mostly obsolete but worth understanding for legacy systems:


// Client-side JSONP request
function jsonpCallback(data) {
  console.log(data); // Handle the data here
}

const script = document.createElement('script');
script.src = 'https://api.example.com/data?callback=jsonpCallback';
document.body.appendChild(script);

// Server-side JSONP response (Express)
app.get('/data', (req, res) => {
  const data = { name: 'Example', value: 42 };
  const callback = req.query.callback || 'callback';
  res.set('Content-Type', 'application/javascript');
  res.send(`${callback}(${JSON.stringify(data)})`);
});
                

JSONP has significant limitations (only supports GET requests, potential security issues) and should generally be avoided in favor of CORS in modern applications.

CORS in Production Environments

Using a Reverse Proxy

In production, you might use Nginx or Apache as a reverse proxy in front of your Node.js application. You can configure CORS headers at the proxy level:

Nginx CORS Configuration


# Inside server or location block
location /api/ {
    proxy_pass http://backend_server;
    
    # CORS headers
    add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
    add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
    add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
    add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
    
    # Preflight requests
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
        add_header 'Access-Control-Max-Age' 1728000;
        add_header 'Content-Type' 'text/plain; charset=utf-8';
        add_header 'Content-Length' 0;
        return 204;
    }
}
                

Cloud Provider Configurations

If you're using cloud services, most providers offer built-in CORS configuration:

AWS API Gateway CORS Configuration


// AWS CloudFormation template snippet
Resources:
  ApiGateway:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: MyApi
  
  ApiGatewayCors:
    Type: AWS::ApiGateway::GatewayResponse
    Properties:
      RestApiId: !Ref ApiGateway
      ResponseType: DEFAULT_4XX
      ResponseParameters:
        gatewayresponse.header.Access-Control-Allow-Origin: "'https://app.example.com'"
        gatewayresponse.header.Access-Control-Allow-Headers: "'Content-Type,Authorization'"
        gatewayresponse.header.Access-Control-Allow-Methods: "'GET,POST,PUT,DELETE,OPTIONS'"
      ResponseTemplates:
        application/json: '{"message":$context.error.messageString}'
                

Azure Functions CORS Configuration


// In host.json
{
  "version": "2.0",
  "extensions": {
    "http": {
      "routePrefix": "api",
      "cors": {
        "allowedOrigins": [
          "https://app.example.com"
        ],
        "allowedMethods": [
          "GET",
          "POST",
          "PUT",
          "DELETE",
          "OPTIONS"
        ],
        "allowedHeaders": [
          "Content-Type",
          "Authorization"
        ],
        "maxAge": 86400
      }
    }
  }
}
                

CDN and Edge Configurations

Content Delivery Networks (CDNs) can also handle CORS headers at the edge, often with simpler configuration:

Cloudflare Workers CORS Example


addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
  // Get the response from the origin
  let response = await fetch(request)
  
  // Clone the response to modify headers
  response = new Response(response.body, response)
  
  // Add CORS headers
  response.headers.set('Access-Control-Allow-Origin', 'https://app.example.com')
  response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
  response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
  response.headers.set('Access-Control-Max-Age', '86400')
  
  return response
}
                

Practical Exercises

Exercise 1: Basic CORS Configuration

Create a simple Express.js API with CORS enabled for a specific origin.

  1. Create a new Express project
  2. Install the cors middleware
  3. Configure CORS to allow requests only from 'http://localhost:3000'
  4. Create a simple GET endpoint at '/api/data' that returns JSON data
  5. Test the endpoint using fetch from both allowed and disallowed origins

Exercise 2: Dynamic CORS Configuration

Extend the previous example to use dynamic origin validation based on environment variables.

  1. Create a .env file with an ALLOWED_ORIGINS variable (comma-separated list)
  2. Parse the environment variable to create an array of allowed origins
  3. Implement dynamic origin validation in the CORS configuration
  4. Test with different origin values

Exercise 3: Route-Specific CORS

Create an API with different CORS configurations for different routes.

  1. Create a public endpoint with permissive CORS
  2. Create a protected endpoint with strict CORS and credentials enabled
  3. Test both endpoints from different origins
  4. Add a custom header to the protected endpoint and configure CORS to allow it

Exercise 4: Troubleshooting CORS

Deliberately create and then fix common CORS issues.

  1. Create an endpoint that requires credentials but with a wildcard origin
  2. Create an endpoint that doesn't handle preflight requests properly
  3. Create an endpoint that doesn't expose a custom header needed by the frontend
  4. Use browser developer tools to identify and fix each issue

Additional Resources

Summary

CORS is a crucial security feature that allows controlled cross-origin resource sharing while protecting users from malicious websites. Key takeaways:

By properly configuring CORS, you can create secure APIs that can be safely accessed from different origins, enabling modern web architectures like microservices and SPAs.