diff --git a/app/__init__.py b/app/__init__.py index 1203f8f..01893e4 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,21 +1,38 @@ from flask import Flask -from werkzeug.middleware.proxy_fix import ProxyFix from config import Config from app.extensions import db, migrate, oauth from app.storage import public_url +class _CloudFrontHTTPSFix: + """Flip wsgi.url_scheme to https when the request came in via CloudFront. + + The stack is CloudFront(HTTPS) → ALB(HTTP) → ECS, and ALB *replaces* + `X-Forwarded-Proto` with its listener's value (http) rather than + appending — so the standard ProxyFix pattern doesn't help here. + CloudFront is configured to inject `X-Forwarded-Scheme: https` as an + immutable origin custom header (see terraform/main/modules/cloudfront/main.tf); ALB + passes that name through verbatim because it has no special handling + for it. Presence = the request entered through prod's CloudFront, + so url_for(_external=True) should generate https URLs (e.g. the OIDC + redirect_uri that Keycloak validates). + """ + + def __init__(self, wsgi_app): + self.wsgi_app = wsgi_app + + def __call__(self, environ, start_response): + if environ.get("HTTP_X_FORWARDED_SCHEME") == "https": + environ["wsgi.url_scheme"] = "https" + return self.wsgi_app(environ, start_response) + + def create_app(config_class=Config): app = Flask(__name__) app.config.from_object(config_class) - # CloudFront → ALB (HTTP) → ECS. CloudFront stamps `X-Forwarded-Proto: - # https`; ALB appends its own `http`. x_proto=2 picks the CloudFront - # value so `url_for(..., _external=True)` produces https URLs (e.g. - # the OIDC redirect_uri Keycloak validates). x_for=2 mirrors the same - # for client IPs in access logs. - app.wsgi_app = ProxyFix(app.wsgi_app, x_for=2, x_proto=2, x_host=1) + app.wsgi_app = _CloudFrontHTTPSFix(app.wsgi_app) db.init_app(app) migrate.init_app(app, db) diff --git a/terraform/main/modules/cloudfront/main.tf b/terraform/main/modules/cloudfront/main.tf index 0bc67e7..9c82112 100644 --- a/terraform/main/modules/cloudfront/main.tf +++ b/terraform/main/modules/cloudfront/main.tf @@ -76,6 +76,19 @@ resource "aws_cloudfront_distribution" "this" { origin_protocol_policy = "http-only" origin_ssl_protocols = ["TLSv1.2"] } + + # `viewer_protocol_policy = "redirect-to-https"` below means every + # request reaching origin came from an HTTPS viewer, so this static + # value is always correct. The ALB's HTTP listener replaces + # `X-Forwarded-Proto` with `http` before forwarding to ECS, which + # breaks Flask's url_for(_external=True) for OIDC redirect_uris. A + # custom header name (vs reusing `X-Forwarded-Proto`) sidesteps the + # ALB rewrite entirely; the app reads this header to flip + # wsgi.url_scheme back to https. + custom_header { + name = "X-Forwarded-Scheme" + value = "https" + } } # Default behavior: forward everything to the ALB with no caching. Auth