From 77704a91b6067a41bdbf2a5b74fda1260adcedb2 Mon Sep 17 00:00:00 2001 From: Steven Hawkins Date: Mon, 26 Jan 2026 12:14:54 -0500 Subject: [PATCH] fix: adding support for xforwarded prefix (#45699) closes: #35298 Signed-off-by: Steve Hawkins --- .../topics/changes/changes-26_6_0.adoc | 4 ++++ docs/guides/server/hostname.adoc | 2 +- docs/guides/server/reverseproxy.adoc | 18 +++++++++--------- .../java/org/keycloak/config/ProxyOptions.java | 5 +++++ .../mappers/ProxyPropertyMappers.java | 4 ++++ .../resources/ConstantsDebugHostname.java | 3 ++- .../it/cli/dist/ProxyHostnameV2DistTest.java | 9 +++++++++ 7 files changed, 34 insertions(+), 11 deletions(-) diff --git a/docs/documentation/upgrading/topics/changes/changes-26_6_0.adoc b/docs/documentation/upgrading/topics/changes/changes-26_6_0.adoc index 07252772ca2..ee100044476 100644 --- a/docs/documentation/upgrading/topics/changes/changes-26_6_0.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-26_6_0.adoc @@ -12,6 +12,10 @@ In minor or patch releases, {project_name} will only introduce breaking changes Notable changes may include internal behavior changes that prevent common misconfigurations, bugs that are fixed, or changes to simplify running {project_name}. It also lists significant changes to internal APIs. +=== `X-Forwarded-Prefix` Header is now supported + +With `proxy-headers` set to `xforwarded`, the server can determine the proxy context path from the `X-Forwarded-Prefix` header. + === New database indexes on the `BROKER_LINK` table The `BROKER_LINK` table now contains two additional indexes `IDX_BROKER_LINK_USER_ID` and `IDX_BROKER_LINK_IDENTITY_PROVIDER` to improve performance. diff --git a/docs/guides/server/hostname.adoc b/docs/guides/server/hostname.adoc index 92be53e5717..ebd100c469b 100644 --- a/docs/guides/server/hostname.adoc +++ b/docs/guides/server/hostname.adoc @@ -66,7 +66,7 @@ The `proxy-headers` option can be also used to resolve the URL partially dynamic <@kc.start parameters="--hostname my.keycloak.org --proxy-headers xforwarded"/> -In this case, scheme, and port are resolved dynamically from X-Forwarded-* headers, while hostname is statically defined as `my.keycloak.org`. +In this case, scheme, port, and context-path are resolved dynamically from X-Forwarded-* headers, while hostname is statically defined as `my.keycloak.org`. === Fixed URLs diff --git a/docs/guides/server/reverseproxy.adoc b/docs/guides/server/reverseproxy.adoc index 2847c3bc779..24cc0fa87a5 100644 --- a/docs/guides/server/reverseproxy.adoc +++ b/docs/guides/server/reverseproxy.adoc @@ -30,7 +30,7 @@ You only need to proxy port `8443` (or `8080`) even when you use different host * By default if the option is not specified, no reverse proxy headers are parsed. This should be used when no proxy is in use or with https passthrough. * `forwarded` enables parsing of the `Forwarded` header as per https://www.rfc-editor.org/rfc/rfc7239.html[RFC 7239]. -* `xforwarded` enables parsing of non-standard `X-Forwarded-*` headers, such as `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`, and `X-Forwarded-Port`. +* `xforwarded` enables parsing of non-standard `X-Forwarded-*` headers, such as `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`, `X-Forwarded-Port`, and `X-Forwarded-Prefix`. NOTE: If you are using a reverse proxy for anything other than https passthrough and do not set the `proxy-headers` option, then by default you will see 403 Forbidden responses to requests via the proxy that perform origin checking. @@ -47,14 +47,14 @@ NOTE: When using the `xforwarded` setting, the `X-Forwarded-Port` takes preceden NOTE: If the TLS connection is terminated at the reverse proxy (edge termination), enabling HTTP through the `http-enabled` setting is required. -== Different context-path on reverse proxy +== Different context path on reverse proxy -{project_name} assumes it is exposed through the reverse proxy under the same context path as {project_name} is configured for. By default {project_name} is exposed through the root (`/`), which means it expects to be exposed through the reverse proxy on `/` as well. -You can use a full URL for the `hostname` option in these cases, for example using `--hostname=https://my.keycloak.org/auth` if {project_name} is exposed through the reverse proxy on `/auth`. - -For more details on exposing {project_name} on different hostname or context-path incl. Administration REST API and Console, see <@links.server id="hostname"/>. - -Alternatively you can also change the context path of {project_name} itself to match the context path for the reverse proxy using the `http-relative-path` option, which will change the context-path of {project_name} itself to match the context path used by the reverse proxy. +By default {project_name} is exposed through the root context path (`/`). If the proxy is using a different context path than {project_name}, one of the following must be done: +- Use a simple hostname for the `hostname` option, `xforwarded` for the `proxy-headers` option, and have the proxy set the `X-Forwarded-Prefix` header. +- Use a full URL for the `hostname` option including the proxy context path, for example using `--hostname=https://my.keycloak.org/auth` if {project_name} is exposed through the reverse proxy on `/auth`. +- Change the context path of {project_name} itself to match the context path for the reverse proxy using the `http-relative-path` option. + +For more details on exposing {project_name} on different hostname or context path incl. Administration REST API and Console, see <@links.server id="hostname"/>. == Enable sticky sessions @@ -141,7 +141,7 @@ The following table shows the recommended paths to expose. We assume you run {project_name} on the root path `/` on your reverse proxy/gateway's public API. If not, prefix the path with your desired one. -NOTE: If you configured a `http-relative-path` on the server, proceed as follows to use discovery wih RFC 8414: Configure a reverse proxy to map the `/.well-known/` path without the prefix to the path with the prefix on the server. +NOTE: If you configured a `http-relative-path` on the server, proceed as follows to use discovery with RFC 8414: Configure a reverse proxy to map the `/.well-known/` path without the prefix to the path with the prefix on the server. == Trusted Proxies diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/ProxyOptions.java b/quarkus/config-api/src/main/java/org/keycloak/config/ProxyOptions.java index c4b49cc49ba..88f278a59de 100644 --- a/quarkus/config-api/src/main/java/org/keycloak/config/ProxyOptions.java +++ b/quarkus/config-api/src/main/java/org/keycloak/config/ProxyOptions.java @@ -35,6 +35,11 @@ public class ProxyOptions { .defaultValue(Boolean.FALSE) .build(); + public static final Option PROXY_X_FORWARDED_PREFIX_HEADER_ENABLED = new OptionBuilder<>("proxy-allow-x-forwarded-prefix-header", Boolean.class) + .category(OptionCategory.PROXY) + .defaultValue(Boolean.FALSE) + .build(); + public static final Option PROXY_TRUSTED_HEADER_ENABLED = new OptionBuilder<>("proxy-trusted-header-enabled", Boolean.class) .category(OptionCategory.PROXY) .defaultValue(Boolean.FALSE) diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ProxyPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ProxyPropertyMappers.java index 369207fbe87..3eb64b0e797 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ProxyPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ProxyPropertyMappers.java @@ -41,6 +41,10 @@ final class ProxyPropertyMappers implements PropertyMapperGrouping{ .to("quarkus.http.proxy.allow-x-forwarded") .mapFrom(ProxyOptions.PROXY_HEADERS, (v, c) -> proxyEnabled(ProxyOptions.Headers.xforwarded, v, c)) .build(), + fromOption(ProxyOptions.PROXY_X_FORWARDED_PREFIX_HEADER_ENABLED) + .to("quarkus.http.proxy.enable-forwarded-prefix") + .mapFrom(ProxyOptions.PROXY_HEADERS, (v, c) -> proxyEnabled(ProxyOptions.Headers.xforwarded, v, c)) + .build(), fromOption(ProxyOptions.PROXY_TRUSTED_HEADER_ENABLED) .to("quarkus.http.proxy.enable-trusted-proxy-header") .mapFrom(ProxyOptions.PROXY_HEADERS, (v, c) -> proxyEnabled(null, v, c)) diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/services/resources/ConstantsDebugHostname.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/services/resources/ConstantsDebugHostname.java index a8233d3b211..48549b6671f 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/services/resources/ConstantsDebugHostname.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/services/resources/ConstantsDebugHostname.java @@ -27,7 +27,8 @@ public class ConstantsDebugHostname { "X-Forwarded-Host", "X-Forwarded-Proto", "X-Forwarded-Port", - "X-Forwarded-For" + "X-Forwarded-For", + "X-Forwarded-Prefix" }; public static final String FORWARDED_PROXY_HEADER = "Forwarded"; diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ProxyHostnameV2DistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ProxyHostnameV2DistTest.java index 3ba0be2df5d..892d1c6df00 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ProxyHostnameV2DistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ProxyHostnameV2DistTest.java @@ -121,6 +121,14 @@ public class ProxyHostnameV2DistTest { assertXForwardedHeaders(); } + @Test + @Launch({ "start-dev", "--hostname=fixed", "--proxy-headers=xforwarded" }) + public void testXForwardedProxyHeadersWithHostname() { + given().header("X-Forwarded-Prefix", "/prefix").when().get("http://localhost:8080").then().header(HttpHeaders.LOCATION, containsString("http://fixed:8080/prefix/admin")); + given().header("X-Forwarded-Host", "test:123").when().get("https://localhost:8443").then().header(HttpHeaders.LOCATION, containsString("https://fixed:123/admin")); + given().header("X-Forwarded-Proto", "https").when().get("http://localhost:8080").then().header(HttpHeaders.LOCATION, containsString("https://fixed/admin")); + } + private void assertForwardedHeader() { // trigger a login error assertForwardedHeader("http://mykeycloak.org:8080/realms/master/protocol/openid-connect/auth?client_id=security-admin-console", "https://test:1234/admin", ADDRESS); @@ -139,6 +147,7 @@ public class ProxyHostnameV2DistTest { } private void assertXForwardedHeaders() { + given().header("X-Forwarded-Prefix", "/prefix").when().get("http://localhost:8080").then().header(HttpHeaders.LOCATION, containsString("http://localhost:8080/prefix/admin")); given().header("X-Forwarded-Host", "test:123").when().get("http://mykeycloak.org:8080").then().header(HttpHeaders.LOCATION, containsString("http://test:123/admin")); given().header("X-Forwarded-Host", "test:123").when().get("http://localhost:8080").then().header(HttpHeaders.LOCATION, containsString("http://test:123/admin")); given().header("X-Forwarded-Host", "test:123").when().get("https://localhost:8443").then().header(HttpHeaders.LOCATION, containsString("https://test:123/admin"));