CORS - the definitive final actual guide to solving this nightmare
CORS or Cross-Origin Resource Sharing is the protocol designed to make the internet more secure whilst destroying the mental health of developers the world over on a semi-regular basis.
If you're still reading, I know who you are and I've got bad news - all of those days you’ve spent trying to fix CORS problems? You’re never getting those back. Hopefully this is it, for today I am declaring war on CORS with a definitive guide to what you need to do to fix it up proper. The examples are javascript but the pain and cures are the same.
CORS is awful
CORS is setup like an N-dimensional Rubik’s cube. You change one thing and it makes another thing invalid. Only by balancing the jewels at the exact right positions in an off-world apache sunset can you get all the apps AND the website to work.
To be fair to the creators of CORS, when they designed it smartphones were yet to become ubiquitous and ... how could they foresee that almost every backend would need multiple consumers?
Settings that work for same/different origins, secure and non-secure, and apps
FIRST you've probably already had a stab - so here are the three places to check:
- client side it's just making sure the
fetch
calls are including credentials - server-side the cookie setting code needs to reflect secure / not secure & sameSite policy
- responses need to have CORS headers set (unnecessarily complicated - see below) Let's do it..!
1. Client CORS settings
- fetch needs
credentials: 'include'
to pass the cookie from the different server, BUT you CANNOT have an origin of'*'
server-side - make sure the credentials:include is OUTSIDE of the
headers
section
res = await fetch(url, {
method: 'GET',
headers: {"Content-Type": "application/json; charset=utf-8"},
credentials: 'include'
});
2. CORS Cookies
- Cookies need to be set with
SameSite='Lax'
for non-secure andSamesite='None'
for secure - Cookies also need secure:true or false to match
// Create JWT with user details
const token = jwt.sign(user, env.JWT_SESSION_KEY, { expiresIn: 8035200 });
// Set CORS depending on local or production
cookies.set('session_jwt', token, {
httpOnly: true,
sameSite: (env.NODE_ENV=="production") ? 'None' : 'Lax',
secure: (env.NODE_ENV=="production") ? true : false,
path: '/',
maxAge: 3 * (31 * 24 * 60 * 60) // 3 months in secs
});
3. CORS at server-side (responding to requests)
- Ironically, you can only use Allow-Origin:'*' if you're using the same origin(!) or don't want to set cookies. As soon as you add credentials:'include' to your fetch calls this will break!
- Annoyingly, you also cannot list acceptable origins! (the original design for this was so bad...) so you have to mirror the calling origin as if it is the only acceptable one
- You MUST handle pre-flight with the verb
OPTIONS
- You need to set Allow-Credentials if you want working cookies
// CORS HEADERS
// Change Allow-Origin depending on the origin of the caller
var origin = event.request.headers.get('Origin');
origin = (allowedOrigins.includes(origin)) ? origin : 'null';
response.headers.set('Access-Control-Allow-Origin', origin);
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, Credentials, Origin, Methods');
response.headers.set('Access-Control-Allow-Credentials', 'true');
// Handle CORS preflight requests
if (event.request.method === 'OPTIONS') {
return new Response(null, {
headers: response.headers,
status: 200
});
}
return response;
Extra: CORS from inside a webview
If you're using a webview (e.g. Capacitor) to build apps from your React / Svelte / Angular app, the built-in fetch used by iOS / Android does NOT handle OPTIONS requests automatically like a proper browser would, so you need to either use a third-party fetch-a-like or with Capacitor you can seamless replace fetch with the Capacitor version by enabling CapacitorHttp in capacitor.config.ts
:
plugins: {
CapacitorHttp: {
enabled: true,
},
}
That's it! After which you can use "fetch" as normal.