Box redirect URI: infer from hostname when X-Forwarded-Host is absent

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) <noreply@anthropic.com>
This commit is contained in:
nickviljoen 2026-04-27 15:55:14 +02:00
parent 7c3945417a
commit f17a4ed6da

View file

@ -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'])