diff --git a/.changeset/cors-proxy-credentials.md b/.changeset/cors-proxy-credentials.md new file mode 100644 index 00000000000..7a69c2c506d --- /dev/null +++ b/.changeset/cors-proxy-credentials.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +Include Access-Control-Allow-Credentials on app-dev reverse-proxy CORS preflights so credentialed cross-origin fetches succeed. diff --git a/packages/app/src/cli/utilities/app/http-reverse-proxy.test.ts b/packages/app/src/cli/utilities/app/http-reverse-proxy.test.ts index 7d26a2bf0c8..b6565fdc2c8 100644 --- a/packages/app/src/cli/utilities/app/http-reverse-proxy.test.ts +++ b/packages/app/src/cli/utilities/app/http-reverse-proxy.test.ts @@ -70,6 +70,7 @@ describe.sequential.each(each)('http-reverse-proxy for %s', (protocol) => { expect(response.headers.get('access-control-allow-methods')).toBe('GET') expect(response.headers.get('access-control-allow-headers')).toBe('Authorization') expect(response.headers.get('access-control-max-age')).toBe('86400') + expect(response.headers.get('access-control-allow-credentials')).toBe('true') }) test('responds to CORS preflight OPTIONS with defaults when no request headers', {retry: 2}, async ({setup}) => { @@ -81,6 +82,7 @@ describe.sequential.each(each)('http-reverse-proxy for %s', (protocol) => { expect(response.headers.get('access-control-allow-origin')).toBe('*') expect(response.headers.get('access-control-allow-methods')).toBe('GET, POST, PUT, DELETE, PATCH, OPTIONS') expect(response.headers.get('access-control-allow-headers')).toBe('Content-Type, Authorization') + expect(response.headers.get('access-control-allow-credentials')).toBeNull() }) test('closes the server when aborted', async ({setup}) => { diff --git a/packages/app/src/cli/utilities/app/http-reverse-proxy.ts b/packages/app/src/cli/utilities/app/http-reverse-proxy.ts index a0380b05995..bb9ec41c85a 100644 --- a/packages/app/src/cli/utilities/app/http-reverse-proxy.ts +++ b/packages/app/src/cli/utilities/app/http-reverse-proxy.ts @@ -74,13 +74,15 @@ function getProxyServerRequestListener( // The proxy does not forward OPTIONS reliably, so we respond here // using the headers requested by the client. if (req.method === 'OPTIONS') { + const origin = req.headers.origin res.writeHead(204, { - 'Access-Control-Allow-Origin': req.headers.origin ?? '*', + 'Access-Control-Allow-Origin': origin ?? '*', 'Access-Control-Allow-Methods': req.headers['access-control-request-method'] ?? 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 'Access-Control-Allow-Headers': req.headers['access-control-request-headers'] ?? 'Content-Type, Authorization', 'Access-Control-Max-Age': '86400', + ...(origin ? {'Access-Control-Allow-Credentials': 'true'} : {}), }) return res.end() }