enhancement
major
minor
defect
critical
major
minor
major
#30177
The development server ignores X-Forwarded-* headers, so a relative callback URL behind a proxy names the wrong origin
Problem
The development server of an application (com.top_logic.ide.jetty.Bootstrap) builds its HttpConfiguration without a ForwardedRequestCustomizer. Behind a reverse proxy, a request's scheme, server name and port are therefore those of the proxy's connection to the server (e.g. 127.0.0.1:<published port>), not the origin the browser used.
Everything that derives an absolute URL from the request breaks behind a proxy. One example is the pac4j login: DefaultOidcClientConfigurator completes the relative callback /servlet/callback from the request (DefaultUrlResolver). The OpenID provider then redirects the browser to an origin it never used. The application's session, which holds the pac4j state, is not sent there, and the login fails.
Configuring an absolute callback from tl_host is no way out when the same server is reached both through the proxy and directly (e.g. by a browser on localhost). It fixes one case and breaks the other.
Found by the control application tl-factory. Its preview route forwards to the development server of an application in a container with X-Forwarded-Host / X-Forwarded-Proto / X-Forwarded-For, while the agent's browser in that container reaches the application at localhost (Gitea issue TopLogic/tl-factory#47).
Design
- Bootstrap adds Jetty's ForwardedRequestCustomizer to its HttpConfiguration. Scheme, host, port and remote address of a request then follow X-Forwarded-* / Forwarded when present, and the request's own values otherwise.
- This is limited to the development server. A deployed application configures the trust of forwarded headers in its own container.
Acceptance
- A request with X-Forwarded-Host: factory.example:8443 and X-Forwarded-Proto: https sees https, factory.example and 8443 as scheme, server name and port.
- A request without these headers is unchanged.
- A pac4j login with the default relative callback behind such a proxy redirects to the forwarded origin, and a direct login on localhost to localhost.
Solution
- Development server: Bootstrap adds the ForwardedRequestCustomizer to its HttpConfiguration.
- TL-Script functions requestScheme(), requestServerName(), requestServerPort(), requestRemoteAddr() and requestUrl() (RequestFunctions, module tl-model-search) describe the HTTP request currently processed as the server sees it, i.e. after the forwarding headers have been applied. Outside an HTTP request (scheduler task, startup) they return null.
- OpenAPI server: OpenApiServlet now extends NoContextServlet. A REST call is handled in an interaction carrying the HTTP request (with a sub-session of its own, the locale from Accept-Language, no HTTP session), so the TL-Script implementation of an operation sees the request, e.g. through the functions above. Before, the script ran in a synthetic interaction without request. Authentication and the identity of the script (system or authenticated person) are unchanged.
- Demo: tl-demo-react contains the OpenAPI server with the public operation GET /api/request-info, which answers the scheme, server name, server port, client address and URL of the request as JSON. It shows whether the forwarding headers of a proxy take effect:
curl -H 'X-Forwarded-Host: factory.example:8443' -H 'X-Forwarded-Proto: https' -H 'X-Forwarded-For: 203.0.113.7' http://localhost:<port>/tl-demo-react/api/request-info
{"scheme":"https","serverName":"factory.example","serverPort":8443,"remoteAddr":"203.0.113.7","url":"https://factory.example:8443/tl-demo-react/api/request-info"}
Migration
- OpenApiServlet erweitert jetzt NoContextServlet statt AbstractTopLogicServlet; die Methode serviceWithLogMark(HttpServletRequest, HttpServletResponse) entfällt. Eine eigene Unterklasse von OpenApiServlet überschreibt stattdessen doService(HttpServletRequest, HttpServletResponse).