Cloudfront & CORS in Safari


3 mins


The new project was progressing rapidly until I hit a difficult-to-understand CORS issue in Safari. This post documents the specific issue and how I resolved it.

I like to say that when I encounter issues that I can’t resolve in a few hours, it’s a really obscure issue. This issue took me over 24 hours to fully understand & then resolve.

Scenario

The service takes incoming file assets from a member, posts them to a member-private location in S3, performs a transformation on the asset, then makes that transformation available to the member by delivering it back into a browser session upon request. The login context is protected with signed cookies and the assets are protected with signed URLs.

The user experience performed as expected in Chrome desktop & iOS as well as Firefox desktop.

Analysis

Attempts to load the signed URLs into a Safari session would at first succeed in one context of the UX but then fail when loading into a generated canvas, which as you may know, has a sort of sandboxed security model. If an attempt is made to paint a cross-origin asset into the canvas and no CORS header is present, the browser will (properly) refuse to load the asset.

Even though the assets had already been loaded into the window object - they were right there - no CORS header(s) were present in the Safari canvas load attempts. It was as though there were in fact multiple requests occuring.

That’s partially correct. They weren’t separate requests only because the initial load of the asset in the window object was cached, so the canvas function was attempting to reuse that cached asset. The cached asset doesn’t traverse the network, so no headers from Cloudfront are involved in satisifying that quasi-request.

Everything comes from the cache, which is desireable as long as the proper headers are cached with the asset. I could see that the Origin header - which instructs the browser whether to process the Access-Control-* headers - was missing. If the Origin header is either not present or is unreliably present, then the browser won’t process the headers.

What it will do, though, is refuse to load the different-origin asset because that’s a part of the browser’s spec. So what’s the deal here?

When the request traversed from S3 through Cloudfront, the Origin header was delivering on initial non-canvas loads. Once the asset was in the browser cache, a peculiar pattern of subsequent uses of that asset would not load any headers. This is either a bug or a very, very obscure implementation of the browser spec for handling cascaded asset requests & loading from cache. I can’t tell.

Resolution

What I needed to ensure was that for every single request of those signed URLs, a correct Origin header was delivered. This would ensure that the response would be handled & all the CORS headers would be read.

Accomplishing this required setting configuration in four places.

— My API

I had to overtly pass CORS headers in all requests that originated wholly in my service. This was easy enough to write the headers into the res(..) object for each interaction with my API.

— CORS configuration in S3

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
     <AllowedOrigin>https://[my service origin endpoint]</AllowedOrigin>
     <AllowedMethod>GET</AllowedMethod>
     <AllowedMethod>PUT</AllowedMethod>
     <AllowedMethod>POST</AllowedMethod>
     <AllowedMethod>DELETE</AllowedMethod>
     <AllowedMethod>HEAD</AllowedMethod>
     <AllowedHeader>x-requested-with</AllowedHeader>
     <AllowedHeader>Origin</AllowedHeader>
</CORSRule>
</CORSConfiguration>

Casing matters with headers.

— Cloudfront Origin Custom Header

In your Cloudfront origin’s setting page, add an Origin Custom Header like this:

Origin      https://[your.origin.name.com]  <== in my case this is my API endpoint

— Cloudfront Default Behavior Headers Whitelist

Add these to your header whitelist:

- Access-Control-Allow-Origin
- Access-Control-Request-Headers
- Access-Control-Request-Method
- x-requested-by

After these configuration changes, all requests in both desktop & iOS Safari function flawlessly as expected.

- jbminn