Failing HTTPS proxy with Ngrok on Railway.app

I've been a fan of free code to cloud deployment services (PaaS) like Railway.app and Fly.io to launch my fly-by-night ideas. They both offer generous free tiers that will allow you to run your code (Node, Python, etc) on their platform and host it for whatever use. The downside of all these different PaaS companies is that they call come with their own CLI that you must learn, with different syntax. Regardless, deploying from Git is easy enough and in fact simpler. Anyway.

 

I've been working on a new project that I think I'll turn into a SaaS eventually. A part of the project uses Google OAuth to login to the user dashboard. Setting up a new application in Google Cloud Platform is easy enough, make your keys, set your permission scopes and voila; instant Google SSO for your custom application. A part of this is specifying your redirect and callback URLs. 

 

If you're developing locally, of course you'd use 127.0.0.1/localhost. Since we're dealing with authentication here, Google is gonna be picky and request HTTPS of course. What do you do? Generate a self signed SSL cert to get around Chrome's annoying popups. What if you want to share your cool, fun new app with your friends and get them to beta test your half-baked idea? ngrok is a reverse-proxy, allowing you to tunnel your local webserver to a free domain, allowing you to share your local application with anyone with the generated link. Gracefully, ngrok provides a SSL certificate to the service too, so you can focus on building your app.

 

I regularly use Flask for my Python projects, dabbling in FastAPI if I need something straightforward or Django is I feel like spending 5 days debugging why my routes don't work (I'm joking about the last part). I was encountering a weird issue where, upon loading my ngrok site in HTTPS, upon logining into Google SSO it would deny my request. Google displays the redirecting URI for you to debug, and I could see the redirect URI was http:// instead of https://. Why did that redirect happen when the HTTP domain was never accessed? Well, our webserver still serves content in HTTP but ngrok's reverse proxy does the magic and gives us that HTTPS part.

 

After some Googling, I was left with this solution:

from werkzeug.middleware.proxy_fix import ProxyFix

app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)

 

Why does this work and why is it needed?

 

When using ngrok, it acts as a reverse proxy, creating a secure HTTPS tunnel to our local HTTP server. This is where ProxyFix plays a pivotal role. It's akin to an interpreter that correctly translates the communication between the secure ngrok layer and our Flask app. By configuring the wsgi_app attribute of the Flask app with ProxyFix, it effectively aligns the external HTTPS requests with the internal HTTP environment of the Flask server. This alignment is critical because, without it, the Flask app might misinterpret the secure HTTPS requests as insecure HTTP, leading to issues like the one I faced with Google SSO. The middleware specifically trusts the headers from the proxy – X-Forwarded-Proto and X-Forwarded-Host, through the x_proto=1 and x_host=1 arguments. This ensures that even though the Flask server itself is running on HTTP, it recognizes and correctly handles requests forwarded through the HTTPS tunnel provided by ngrok.

 

Simple fix that allowed me to use HTTPS fully with ngrok and Flask. Now, when deploying to Railway.app, the situation is the same, except with a distinction:
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_port=1)
 

  1. x_for=1: This parameter ensures that your application uses the first 'X-Forwarded-For' header to determine the original client IP address. This is crucial in a cloud environment like Railway, where your app might be behind multiple layers of proxies.
  2. x_port=1: This indicates that the app should trust the first 'X-Forwarded-Port' header, which is important for accurately identifying the port number used in the client's original request. This can be essential for constructing URLs and for certain security checks.

Understanding x_host=1

  • What it does: Setting x_host=1 tells Flask to trust the X-Forwarded-Host header provided by a proxy server. This header indicates the original host requested by the client.
  • Why it's insecure: The primary security concern with trusting the X-Forwarded-Host header arises from the possibility of header spoofing. If the proxy isn't configured properly to overwrite or discard this header from incoming requests, a malicious user could inject a false host header. This might lead to incorrect URL generation, misleading redirects, or in worst cases, security vulnerabilities like open redirects or host header injection attacks.

While the change is small, the distinction should be made when deploying locally or remotely. I hope this post helps any other developer who landed in my place ;)


 

Tags