CORS Complete Guide | Cross-Origin Resource Sharing in Node.js

CORS Complete Guide | Cross-Origin Resource Sharing in Node.js

이 글의 핵심

CORS (Cross-Origin Resource Sharing) is a security mechanism that controls which origins can access your API. Understanding CORS is essential for building secure web applications.

Introduction

CORS (Cross-Origin Resource Sharing) is a security feature implemented by browsers to control how web pages can request resources from different origins.

What is an Origin?

An origin consists of:

  • Protocol: http or https
  • Domain: example.com
  • Port: :3000
https://example.com:3000
└─┬──┘ └────┬─────┘└┬─┘
  │         │       └─ Port
  │         └───────── Domain
  └─────────────────── Protocol

Same origin:

https://example.com/api/users
https://example.com/api/posts

Different origins (CORS applies):

http://localhost:3000  → http://localhost:8000
https://example.com    → https://api.example.com
https://example.com    → https://example.com:3000

1. The Problem

Without CORS, browsers block cross-origin requests:

// Frontend at http://localhost:3000
fetch('http://localhost:8000/api/users')
  .then(res => res.json())
  .catch(err => console.error(err));

// Error:
// Access to fetch at 'http://localhost:8000/api/users' 
// from origin 'http://localhost:3000' has been blocked by CORS policy

2. Basic Setup

npm install cors
const express = require('express');
const cors = require('cors');

const app = express();

// Enable CORS for all origins (not recommended for production)
app.use(cors());

app.get('/api/users', (req, res) => {
  res.json({ users: ['Alice', 'Bob'] });
});

app.listen(8000);

3. Specific Origins

// Allow single origin
app.use(cors({
  origin: 'http://localhost:3000'
}));

// Allow multiple origins
const allowedOrigins = [
  'http://localhost:3000',
  'https://example.com',
  'https://www.example.com'
];

app.use(cors({
  origin: (origin, callback) => {
    // Allow requests with no origin (like mobile apps, Postman)
    if (!origin) return callback(null, true);
    
    if (allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  }
}));

4. Full Configuration

app.use(cors({
  origin: 'http://localhost:3000',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  exposedHeaders: ['X-Total-Count'],
  credentials: true, // Allow cookies
  maxAge: 3600, // Cache preflight response for 1 hour
}));

5. Credentials (Cookies)

// Server
app.use(cors({
  origin: 'http://localhost:3000',
  credentials: true, // Allow cookies
}));

app.get('/api/profile', (req, res) => {
  // Can now read cookies
  res.json({ user: req.user });
});
// Client
fetch('http://localhost:8000/api/profile', {
  credentials: 'include', // Send cookies
})
  .then(res => res.json());

6. Preflight Requests

For complex requests (PUT, DELETE, custom headers), browsers send a preflight OPTIONS request:

// Automatic preflight handling
app.use(cors());

// Manual preflight handling
app.options('/api/users', cors());

app.put('/api/users/:id', cors(), (req, res) => {
  // Update user
});

7. Dynamic Origins

app.use(cors({
  origin: (origin, callback) => {
    // Allow all subdomains
    if (!origin || origin.match(/\.example\.com$/)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  }
}));

// Or check database
app.use(cors({
  origin: async (origin, callback) => {
    const allowed = await db.allowedOrigins.findOne({ origin });
    callback(null, !!allowed);
  }
}));

8. Route-Specific CORS

// Enable CORS for specific routes
app.get('/api/public', cors(), (req, res) => {
  res.json({ data: 'public' });
});

// No CORS for this route
app.get('/api/private', (req, res) => {
  res.json({ data: 'private' });
});

// Different CORS config per route
app.get('/api/admin', cors({
  origin: 'https://admin.example.com'
}), (req, res) => {
  res.json({ data: 'admin' });
});

9. Custom Headers

app.use(cors({
  origin: 'http://localhost:3000',
  allowedHeaders: [
    'Content-Type',
    'Authorization',
    'X-Requested-With',
    'X-Custom-Header'
  ],
  exposedHeaders: [
    'X-Total-Count',
    'X-Page-Count'
  ]
}));

// Client can now read exposed headers
fetch('http://localhost:8000/api/users')
  .then(res => {
    const totalCount = res.headers.get('X-Total-Count');
    return res.json();
  });

10. Environment-Based Configuration

const corsOptions = {
  origin: process.env.NODE_ENV === 'production'
    ? 'https://example.com'
    : 'http://localhost:3000',
  credentials: true,
};

app.use(cors(corsOptions));

11. Common Errors and Solutions

Error 1: No ‘Access-Control-Allow-Origin’ header

// Problem: CORS not enabled
app.get('/api/users', (req, res) => {
  res.json({ users: [] });
});

// Solution: Add CORS
app.use(cors());

Error 2: Credentials not supported with ’*‘

// Problem: Can't use credentials with wildcard origin
app.use(cors({
  origin: '*',
  credentials: true, // ❌ Error!
}));

// Solution: Specify exact origins
app.use(cors({
  origin: 'http://localhost:3000',
  credentials: true, // ✅ Works
}));

Error 3: Method not allowed

// Problem: Method not in allowed methods
app.use(cors({
  methods: ['GET', 'POST'], // PUT not allowed
}));

app.put('/api/users/:id', (req, res) => {
  // CORS error!
});

// Solution: Add method
app.use(cors({
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
}));

Error 4: Header not allowed

// Problem: Custom header not allowed
fetch('http://localhost:8000/api/users', {
  headers: {
    'X-Custom-Header': 'value' // Not allowed!
  }
});

// Solution: Add to allowedHeaders
app.use(cors({
  allowedHeaders: ['Content-Type', 'X-Custom-Header'],
}));

12. Production Configuration

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

const app = express();

// Production-ready CORS
const allowedOrigins = [
  'https://example.com',
  'https://www.example.com',
  'https://app.example.com',
];

app.use(cors({
  origin: (origin, callback) => {
    // Allow requests with no origin (mobile apps, curl, Postman)
    if (!origin) return callback(null, true);
    
    if (allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      console.warn(`CORS blocked: ${origin}`);
      callback(new Error('Not allowed by CORS'));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  exposedHeaders: ['X-Total-Count'],
  credentials: true,
  maxAge: 86400, // 24 hours
  optionsSuccessStatus: 200,
}));

app.listen(process.env.PORT || 8000);

13. With Authentication

const express = require('express');
const cors = require('cors');
const jwt = require('jsonwebtoken');

const app = express();

app.use(cors({
  origin: 'http://localhost:3000',
  credentials: true,
  allowedHeaders: ['Content-Type', 'Authorization'],
}));

// Protected route
app.get('/api/profile', (req, res) => {
  const token = req.headers.authorization?.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'No token' });
  }
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    res.json({ user: decoded });
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
});

// Client
fetch('http://localhost:8000/api/profile', {
  headers: {
    'Authorization': `Bearer ${token}`,
  },
  credentials: 'include',
});

14. CORS Proxy (Development)

For development, you can proxy requests:

// Next.js (next.config.js)
module.exports = {
  async rewrites() {
    return [
      {
        source: '/api/:path*',
        destination: 'http://localhost:8000/api/:path*',
      },
    ];
  },
};

// Vite (vite.config.js)
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8000',
        changeOrigin: true,
      },
    },
  },
};

15. Testing CORS

// Test CORS with curl
curl -H "Origin: http://localhost:3000" \
     -H "Access-Control-Request-Method: POST" \
     -H "Access-Control-Request-Headers: Content-Type" \
     -X OPTIONS \
     http://localhost:8000/api/users

// Check response headers
// Access-Control-Allow-Origin: http://localhost:3000
// Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE

Summary

CORS controls cross-origin access:

  • Security mechanism implemented by browsers
  • Origin checking prevents unauthorized access
  • Credentials support for cookies/auth
  • Preflight requests for complex operations
  • Production config must be restrictive

Key Takeaways:

  1. Never use origin: '*' in production
  2. Enable credentials only for trusted origins
  3. Specify allowed methods and headers
  4. Test CORS in production-like environment
  5. Use proxies for development

Next Steps:

  • Secure with Helmet
  • Auth with Passport
  • Build Express API

Resources: