CORS Configuration

What is CORS?

CORS (Cross-Origin Resource Sharing) is a security mechanism that controls whether your web app running in the WebView can make requests to external servers. When your app tries to fetch data or assets from a different domain, the browser (WebView) enforces the Same-Origin Policy and requires the external server to explicitly allow the request.

When Do You Need CORS?

You need CORS if:

  • Your app makes requests to external servers (REST API, GraphQL, CDN assets, etc.)
  • Your app fetches data from a backend you control
  • Your JavaScript code uses fetch() or XMLHttpRequest to call external services

You don't need CORS if:

  • Your app is completely self-contained (no external requests)
  • You only load static resources from CDNs (handled by CSP, not CORS)
  • You use native plugins to make HTTP requests (native code bypasses CORS)

Why CORS Exists

CORS prevents malicious websites from stealing your data. Without CORS, any website could make API calls to your bank, email, or social media accounts using your stored credentials.

Example Attack Prevented by CORS

Evil Website (evil.com)
  ↓ tries to call
Your Bank API (bank.com/api/transfer)

❌ BLOCKED by CORS
Your bank didn't allow requests from evil.com

Your App Scenario

Your Mobile App (localhost:random-port)
  ↓ tries to call
Your External Server (api.yourapp.com, cdn.example.com, etc.)

✅ ALLOWED if you configure CORS
Your server explicitly allows localhost origins

How CORS Works in Your App

When your JavaScript code makes an API call, here's what happens:

┌─────────────────────────────────────────┐
│  Your App (localhost:49152)             │
│  fetch('https://api.yourapp.com/data')  │
└──────────────────┬──────────────────────┘

                   │ Request with:
                   │ Origin: http://localhost:49152

         ┌──────────────────────┐
         │  Your API Server     │
         │  api.yourapp.com     │
         │                      │
         │  Checks Origin       │
         │  Adds CORS headers   │
         └──────────┬───────────┘

                    │ Response with:
                    │ Access-Control-Allow-Origin: http://localhost:49152

         ┌──────────────────────┐
         │  WebView             │
         │  Checks headers      │
         │  ✅ Allows response  │
         └──────────────────────┘

The Random Port Challenge

Your mobile app's internal server uses a random port that changes every time the app starts:

  • First launch: http://localhost:49152
  • Second launch: http://localhost:51234
  • Third launch: http://localhost:52891

This means you cannot hardcode a specific port in your API server's CORS configuration.

Solution: Allow Any Localhost Port

Configure your API server to dynamically allow any localhost origin. This is secure because:

  • ✅ Only your mobile app runs on localhost
  • ✅ External websites can't pretend to be localhost
  • ✅ Users must have your app installed on their device
  • ✅ No network access to localhost from outside the device

Configuring CORS on Your External Server

You must configure CORS on your external server, not in the SaaS dashboard. Below we show a Node.js/Express example, but this works very similarly with other servers and languages like Python (Flask, Django), Go, PHP, Ruby (Rails), Java (Spring), C# (ASP.NET), and more. The key concept is the same: dynamically allow any localhost or 127.0.0.1 origin with any port.

Node.js / Express Example

Install the cors package:

npm install cors

Configure dynamic localhost validation:

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

const app = express()

// Custom CORS configuration
const corsOptions = {
  origin: function (origin, callback) {
    // Allow requests from localhost with any port
    if (!origin || origin.startsWith('http://localhost:') || origin.startsWith('http://127.0.0.1:')) {
      callback(null, true)
    } else {
      callback(new Error('Not allowed by CORS'))
    }
  },
  credentials: true, // Allow cookies and authentication headers
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization']
}

app.use(cors(corsOptions))

// Your API routes
app.get('/api/data', (req, res) => {
  res.json({ message: 'Hello from API' })
})

app.listen(3000)

Option 2: Manual middleware

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

app.use((req, res, next) => {
  const origin = req.headers.origin

  // Allow any localhost or 127.0.0.1 origin
  if (origin && (origin.startsWith('http://localhost:') || origin.startsWith('http://127.0.0.1:'))) {
    res.header('Access-Control-Allow-Origin', origin)
    res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
    res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
    res.header('Access-Control-Allow-Credentials', 'true')
  }

  // Handle preflight requests
  if (req.method === 'OPTIONS') {
    return res.sendStatus(200)
  }

  next()
})

// Your API routes
app.get('/api/data', (req, res) => {
  res.json({ message: 'Hello from API' })
})

app.listen(3000)

Required CORS Headers Explained

Access-Control-Allow-Origin

What it does: Specifies which origins can access the resource.

Values:

  • http://localhost:49152 - Specific origin (not useful with random ports)
  • * - Any origin (cannot use with credentials)
  • Dynamic origin from request (recommended for your app)

Example:

Access-Control-Allow-Origin: http://localhost:49152

Access-Control-Allow-Methods

What it does: Lists HTTP methods allowed for cross-origin requests.

Example:

Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS

Access-Control-Allow-Headers

What it does: Lists headers that can be used in the actual request.

Example:

Access-Control-Allow-Headers: Content-Type, Authorization

Access-Control-Allow-Credentials

What it does: Indicates whether credentials (cookies, auth headers) can be sent.

Example:

Access-Control-Allow-Credentials: true

Important: When using Access-Control-Allow-Credentials: true, you cannot use Access-Control-Allow-Origin: *. You must specify the exact origin.

Preflight Requests

For certain requests (POST, PUT, DELETE, or with custom headers), browsers send a preflight OPTIONS request first to check if the actual request is allowed.

How Preflight Works

1. Browser sends OPTIONS request:
   OPTIONS /api/data
   Origin: http://localhost:49152
   Access-Control-Request-Method: POST
   Access-Control-Request-Headers: Content-Type

2. Server responds with CORS headers:
   Access-Control-Allow-Origin: http://localhost:49152
   Access-Control-Allow-Methods: GET, POST, PUT, DELETE
   Access-Control-Allow-Headers: Content-Type
   Status: 200 OK

3. Browser sends actual request:
   POST /api/data
   Origin: http://localhost:49152
   Content-Type: application/json
   Body: {"data": "..."}

Handling Preflight in Your Server

All the code examples above already handle preflight requests by:

  1. Checking if the request method is OPTIONS
  2. Returning CORS headers
  3. Sending a 200 OK status code

If you don't handle OPTIONS requests, preflight will fail and your API calls will be blocked.

Testing Your CORS Configuration

Step 1: Test from Your Mobile App

Make an API call from your app's JavaScript:

fetch('https://api.yourapp.com/data', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json'
  },
  credentials: 'include' // Include cookies/auth
})
  .then(response => response.json())
  .then(data => console.log('Success:', data))
  .catch(error => console.error('Error:', error))

Step 2: Check Browser Console

Open the WebView console and look for CORS errors:

Success - No errors:

Success: {message: "Hello from API"}

CORS error:

Access to fetch at 'https://api.yourapp.com/data' from origin
'http://localhost:49152' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the
requested resource.

Step 3: Check Network Tab

In the browser developer tools, check the Network tab:

Preflight request (OPTIONS):

  • Request Headers: Origin: http://localhost:49152
  • Response Headers should include:
    • Access-Control-Allow-Origin: http://localhost:49152
    • Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS

Actual request (GET/POST/etc):

  • Request Headers: Origin: http://localhost:49152
  • Response Headers should include:
    • Access-Control-Allow-Origin: http://localhost:49152

Step 4: Test with curl

Test your CORS configuration from the command line:

# Test preflight request
curl -X OPTIONS https://api.yourapp.com/data \
  -H "Origin: http://localhost:12345" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type" \
  -v

# Test actual request
curl -X GET https://api.yourapp.com/data \
  -H "Origin: http://localhost:12345" \
  -v

Check for Access-Control-Allow-Origin in the response headers.

Common CORS Issues and Solutions

Issue 1: "No 'Access-Control-Allow-Origin' header"

Error:

Access to fetch at 'https://api.yourapp.com' from origin 'http://localhost:49152'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is
present on the requested resource.

Cause: Your API server is not sending CORS headers.

Solution: Add CORS middleware to your server (see code examples above).

Issue 2: "Origin is not allowed by Access-Control-Allow-Origin"

Error:

Access to fetch at 'https://api.yourapp.com' from origin 'http://localhost:49152'
has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header has
a value 'http://localhost:3000' that is not equal to the supplied origin.

Cause: Your API is allowing a specific port, but your app is using a different port.

Solution: Update your CORS configuration to allow any localhost port (see code examples above).

Issue 3: Credentials Not Working

Error:

Access to fetch at 'https://api.yourapp.com' from origin 'http://localhost:49152'
has been blocked by CORS policy: 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'.

Cause: You're using Access-Control-Allow-Origin: * with credentials.

Solution: Use dynamic origin (echo back the request's Origin header):

res.header('Access-Control-Allow-Origin', req.headers.origin)
res.header('Access-Control-Allow-Credentials', 'true')

Issue 4: Preflight Failing

Error:

Access to fetch at 'https://api.yourapp.com' from origin 'http://localhost:49152'
has been blocked by CORS policy: Response to preflight request doesn't pass
access control check.

Cause: Your server is not handling OPTIONS requests properly.

Solution: Add OPTIONS handler that returns CORS headers and 200 status (see code examples above).

Issue 5: Custom Headers Blocked

Error:

Request header field authorization is not allowed by Access-Control-Allow-Headers
in preflight response.

Cause: Your custom headers aren't listed in Access-Control-Allow-Headers.

Solution: Add your headers to the allowed list:

Access-Control-Allow-Headers: Content-Type, Authorization, X-Custom-Header

Security Best Practices

Do: Validate Localhost Origins

Always check that the origin starts with http://localhost: or http://127.0.0.1::

if (origin.startsWith('http://localhost:') || origin.startsWith('http://127.0.0.1:')) {
  // Allow
}

Don't: Use Wildcards with Credentials

Never use Access-Control-Allow-Origin: * when allowing credentials:

Wrong:

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

Correct:

Access-Control-Allow-Origin: http://localhost:49152
Access-Control-Allow-Credentials: true

Do: Use HTTPS in Production

For production APIs, always use HTTPS to encrypt data in transit:

https://api.yourapp.com
http://api.yourapp.com

Don't: Trust the Origin Header for Authorization

The CORS origin check is done by the browser, not your server. For authorization, always verify tokens or API keys on the server side.

Development vs Production

Development (Permissive)

For development and testing, you can be more permissive:

// Allow any origin during development
res.header('Access-Control-Allow-Origin', req.headers.origin)

Production (Strict)

For production, restrict to specific origins:

const allowedOrigins = [
  /^http:\/\/localhost:\d+$/, // Mobile app (localhost)
  /^http:\/\/127\.0\.0\.1:\d+$/, // Mobile app (127.0.0.1)
  'https://yourwebsite.com' // Your website (if applicable)
]

const origin = req.headers.origin
const isAllowed = allowedOrigins.some(allowed => {
  if (allowed instanceof RegExp) {
    return allowed.test(origin)
  }
  return allowed === origin
})

if (isAllowed) {
  res.header('Access-Control-Allow-Origin', origin)
}

Quick Reference

Minimum CORS Headers Required

Access-Control-Allow-Origin: http://localhost:49152
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization

With Credentials

Access-Control-Allow-Origin: http://localhost:49152
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true

Preflight Response

Status: 200 OK
Access-Control-Allow-Origin: http://localhost:49152
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization

Summary

Key Points:

  • CORS controls whether your app can call external servers (APIs, CDNs, etc.)
  • Configure CORS on your API server, not in the SaaS dashboard
  • Allow any localhost or 127.0.0.1 origin with any port
  • Handle OPTIONS requests for preflight
  • Use HTTPS in production
  • Validate origins dynamically

Remember:

  • Your app uses a random port - you can't hardcode it
  • Allowing any localhost port is secure (only your app uses localhost)
  • CORS is enforced by the browser (WebView), not your server
  • Use the code examples above for your specific server technology

Next Steps