From f17a4ed6da013cd336fd671092e0beff896712ca Mon Sep 17 00:00:00 2001 From: nickviljoen Date: Mon, 27 Apr 2026 15:55:14 +0200 Subject: [PATCH] Box redirect URI: infer from hostname when X-Forwarded-Host is absent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous fix relied on Apache forwarding X-Forwarded-Host, but on optical-dev that header isn't set. Apache uses ProxyPreserveHost (so request.host correctly resolves to optical-dev.oliver.solutions) but the backend connection is plain http and Flask sees no path prefix, so the fallback emitted "http://optical-dev.oliver.solutions/auth/box/callback" — which Box rejected as "insecure_redirect_uri" (no HTTPS) and which is also missing the required /ai_qc/ prefix. Resolution order is now: 1. BOX_REDIRECT_URI env var (escape hatch / unusual deploys). 2. X-Forwarded-Host header if Apache happens to send it. 3. Otherwise: infer from request.host. Any host that isn't localhost or 127.0.0.1 is treated as the optical-dev / optical-prod proxy and gets HTTPS + the /ai_qc/ prefix. localhost stays http and rootless. Verified all five paths (dev with and without XF-Host, laptop on localhost and 127.0.0.1, explicit override) produce the right URL. Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/api_server.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/backend/api_server.py b/backend/api_server.py index 08735a9..459fd3e 100755 --- a/backend/api_server.py +++ b/backend/api_server.py @@ -5385,22 +5385,31 @@ def _box_redirect_uri(): """ Compute the public OAuth callback URL. - - BOX_REDIRECT_URI env var wins if set (override / testing). - - Otherwise, when behind the Apache reverse proxy on optical-dev/optical-prod, - X-Forwarded-Host is set; the app is mounted at /ai_qc/ so we prepend it. - - Direct access (Nick's laptop, port 7183) goes through neither — use the - Flask host as-is. + Resolution order: + 1. BOX_REDIRECT_URI env var if set (escape hatch / unusual deploys). + 2. X-Forwarded-Host header if Apache sets it (some setups do). + 3. Otherwise, infer from request.host: anything that isn't localhost + is treated as being behind the Apache proxy at /ai_qc/ over HTTPS + (this matches optical-dev / optical-prod where Apache uses + ProxyPreserveHost so request.host is already the public hostname, + but the backend connection is plain http and Flask sees no prefix). """ explicit = (os.environ.get('BOX_REDIRECT_URI') or '').strip() if explicit: return explicit + forwarded_host = request.headers.get('X-Forwarded-Host') if forwarded_host: - # First entry if a chain of proxies was somehow involved. host = forwarded_host.split(',')[0].strip() proto = request.headers.get('X-Forwarded-Proto', 'https') return f'{proto}://{host}/ai_qc/auth/box/callback' - return f'{request.scheme}://{request.host}/auth/box/callback' + + host = (request.host or '').strip() + is_local = (not host) or 'localhost' in host or host.startswith('127.0.0.1') + if not is_local: + # Behind the optical-dev / optical-prod Apache proxy, mounted at /ai_qc/. + return f'https://{host}/ai_qc/auth/box/callback' + return f'{request.scheme}://{host}/auth/box/callback' @app.route('/auth/box/login', methods=['GET'])