gitpod icon indicating copy to clipboard operation
gitpod copied to clipboard

CORS Preflight OPTIONS request with private port is blocked with 401 Unauthorized

Open omarkohl opened this issue 2 years ago • 11 comments

Bug description

My app is separated into an API and a GUI, started on different ports. If the API port is public, everything works fine. If the port is made private, then the OPTIONS request never reaches the API but seems to be blocked by Gitpod.

OPTIONS /query HTTP/2
Host: 8080-cleodoraforeca-cleodora-cnqioetp8sl.ws-eu74.gitpod.io
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:106.0) Gecko/20100101 Firefox/106.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
Referer: https://3000-cleodoraforeca-cleodora-cnqioetp8sl.ws-eu74.gitpod.io/
Origin: https://3000-cleodoraforeca-cleodora-cnqioetp8sl.ws-eu74.gitpod.io
Connection: keep-alive
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
TE: trailers

This request never reaches my API. Instead I see the following response in the browser console:

HTTP/2 401 Unauthorized
content-length: 0
date: Wed, 09 Nov 2022 20:05:01 GMT
X-Firefox-Spdy: h2

This suggests to me that Gitpod is blocking it. Possibly because Gitpod expects the request to already contain the authentication cookie. But note that this would be against the spec and the browser does NOT send authentication in OPTIONS requests.

Preflight requests and credentials

CORS-preflight requests must never include credentials. The response to a preflight request must specify Access-Control-Allow-Credentials: true to indicate that the actual request can be made with credentials.

https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#requests_with_credentials

If the Gitpod port is public then everything works fine and both the preflight OPTIONS request and the POST request reach my API!

I can tell that the OPTIONS request is not reaching my API thanks to logging output in my app.

More details: https://github.com/cleodora-forecasting/cleodora/issues/27

Steps to reproduce

You can check the workspace below for a concrete example, but this must be a general issue. Check the README of my example repository for step-by-step instructions: https://github.com/omarkohl/gitpod-cors

Workspace affected

cleodoraforeca-cleodora-1raaym6t355

Expected behavior

OPTIONS requests are let through by Gitpod and only other HTTP requests are checked for credentials.

Example repository

https://github.com/omarkohl/gitpod-cors

Anything else?

No response

omarkohl avatar Nov 09 '22 20:11 omarkohl

I created a small example repository that reproduces the issue. I updated the issue description. This is the repository. The README contains step by step instructions to reproduce the problem https://github.com/omarkohl/gitpod-cors

omarkohl avatar Nov 10 '22 11:11 omarkohl

For what it's worth, GitHub Codespaces has exactly the same problem (same code base?) and yesterday there was confirmation that the issue is real and that my hypothesis concerning the OPTIONS request and authentication is correct.

https://github.com/orgs/community/discussions/15351

So now it's a race, who will fix it first, Codespaces or Gitpod :wink:

omarkohl avatar Nov 11 '22 07:11 omarkohl

Hi @omarkohl, this issue is now forwarded to an engineering team.

Meanwhile you may take a look at the following resources:

  • https://www.gitpod.io/docs/configure/workspaces/ports#local-port-forwarding
  • https://www.gitpod.io/docs/configure/workspaces/ports#cross-origin-resource-sharing-cors
  • https://github.com/gitpod-io/gitpod/issues/459

axonasif avatar Nov 14 '22 14:11 axonasif

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Feb 19 '23 13:02 stale[bot]

@axonasif any update from the engineering team?

omarkohl avatar Feb 19 '23 13:02 omarkohl

I am also suffering this exact issue, are there any updates?

GCHQDeveloper926 avatar Apr 04 '23 13:04 GCHQDeveloper926

@omarkohl I've got round this for now with a small nginx proxy running in gitpod, happy to share the config and tasks with you?

GCHQDeveloper926 avatar Apr 05 '23 14:04 GCHQDeveloper926

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Sep 17 '23 11:09 stale[bot]

@GCHQDeveloper926 do you have more info on the workaround for this?

billy1kaplan avatar Feb 06 '24 23:02 billy1kaplan

Hi @billy1kaplan I've spun up NGINX inside my gitpod to forward requests to the app. I can send you a config if you would like?

GCHQDeveloper926 avatar Feb 19 '24 09:02 GCHQDeveloper926

The workaround that is possible here is to prevent the OPTIONS pre-flights all together by hosting all endpoints on the same domain name (eg. same port for Gitpod).

It means we don't get the added benefit of always checking that our pre-flights requests are fine, but that might neeed to be validated at test/staging anyway to have production-env parity. This issue seems like a real blocker for private ports working out of the box, and I don't suppose it can get fixed at anytime by Gitpod/Github due to the resulting security questions, so I'm taking the extra liberty here of documenting a full copy-pastable solution - hopefully it will save someone a few hours, without needing to use exposed ports.

Below is a sample nginx setup (2 files), usable with pre-installed Gitpod nginx, current version nginx/1.25.4 that uses mostly default settings but runs nginx as a standalone program. To run nginx, you need to make sure there is a tmp/nginx directory preexisting and use the command line: nginx -p $(pwd) -c .dev-api-proxy/nginx.conf (where '-p' is the way to set current working dir).

I am using the folder ".dev-api-proxy" to hold the nginx config. The config in question targets a backend on port 4000, a frontend-serve at port 3001 and a resulting reverse-proxy that accepts requests to port 3000. The API is surfaced with the /api endpoint prefix, and (you can remove this if not needed) socket.io requests to /socket.io. Below the nginx config, I've also included some sample code for a nuxt-vue3 frontend that redirects to the reverse-proxy port when opened.

File: .dev-api-proxy/nginx.conf

daemon off;
worker_processes auto;
pid tmp/nginx/nginx.pid;
error_log tmp/nginx/error.log;

events {
	worker_connections 768;
}

http {
	sendfile on;
	tcp_nopush on;
	types_hash_max_size 2048;

	include ./proxy.conf;
}

File: .dev-api-proxy/proxy.conf

map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}
upstream http3001 {
    server 127.0.0.1:3001;
}
upstream http4000 {
    server 127.0.0.1:4000;
}
server {
    listen 3000;
    proxy_request_buffering off;
    proxy_buffers 16 128k;
    proxy_buffer_size 256k;
    proxy_busy_buffers_size 256k;
    location /api {
        proxy_pass http://http4000;
        proxy_pass_request_headers on;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Real-IP $remote_addr;
    }
    location /socket.io {
        proxy_pass http://http4000;
        proxy_pass_request_headers on;
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
    # Forward all other requests to the frontend build running on 3001.
    location / {
        proxy_pass http://http3001;
        proxy_pass_request_headers on;
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}

Additional sample code for nuxt-vue3 to ensure the right port is always used.

File: utils/gitpod.ts

export const useGitpodPortRedirect = () => {
    // Ports in use are currently hardcoded here and should match the dev-api-proxy config.
    // The necessity of running dev-api-proxy is only for gitpod, where private ports will
    // block OPTIONS pre-flight requests due to not having any credentials (which is defined
    // as correct in HTTP spec). Gitpod probably cannot fix this situation as it would need
    // to allow inbound traffic on private ports, potentially opening security issues.
    // The workaround is to use dev-api-proxy that exposes both the nuxt build and API
    // endpoints, thus avoiding all pre-flights, and here we just redirect the browser.
    let isRedirected = false
    const nuxtPort = "3001"
    const proxyPort = "3000"
    if (
        window.location.host.endsWith(".gitpod.io") &&
        window.location.host.startsWith(`${nuxtPort}-`)
    ) {
        window.location.host = window.location.host.replace(nuxtPort, proxyPort)
        isRedirected = true
    }
    return { isRedirected }
}

File: app.vue

<script setup>
import { useGitpodPortRedirect } from "~/utils/gitpod"

const { isRedirected } = useGitpodPortRedirect()
</script>

<template>
    <div v-if="isRedirected">&nbsp;Redirecting...</div>
    <NuxtLayout v-else>
        <NuxtPage />
    </NuxtLayout>
</template>

dsschneidermann avatar Mar 01 '24 14:03 dsschneidermann

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

github-actions[bot] avatar May 30 '24 15:05 github-actions[bot]