task: remove hostname v1 (#32352)

closes: #27731

Signed-off-by: Steve Hawkins <shawkins@redhat.com>
This commit is contained in:
Steven Hawkins 2024-08-28 11:48:06 -04:00 committed by GitHub
parent d9ed0a333b
commit 29eb0171de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 23 additions and 1269 deletions

View file

@ -108,7 +108,6 @@ public class Profile {
CLIENT_TYPES("Client Types", Type.EXPERIMENTAL),
HOSTNAME_V1("Hostname Options V1", Type.DEPRECATED, 1),
HOSTNAME_V2("Hostname Options V2", Type.DEFAULT, 2),
PERSISTENT_USER_SESSIONS("Persistent online user sessions across restarts and upgrades", Type.DEFAULT),

View file

@ -31,7 +31,7 @@ public class ProfileTest {
private static final Profile.Feature DISABLED_BY_DEFAULT_FEATURE = Profile.Feature.DOCKER;
private static final Profile.Feature PREVIEW_FEATURE = Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ;
private static final Profile.Feature EXPERIMENTAL_FEATURE = Profile.Feature.DYNAMIC_SCOPES;
private static Profile.Feature DEPRECATED_FEATURE = Profile.Feature.HOSTNAME_V1;
private static Profile.Feature DEPRECATED_FEATURE = Profile.Feature.LOGIN1;
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();

View file

@ -130,6 +130,14 @@ not recommended at all in production deployments of Keycloak, it is fairly frequ
of `localhost`. As an alternative to the `_LEGACY` cookies Keycloak now doesn't set the `secure` flag and sets `SameSite=Lax`
instead of `SameSite=None` when it detects an insecure context is used.
= Hostname v1 feature removed
The deprecated hostname v1 feature was removed. This feature was deprecated in {project_name} 25 and replaced by hostname v2. If you are still using this feature, you must migrate to hostname v2. For more details, see the https://www.keycloak.org/server/hostname[Configuring the hostname (v2)] and https://www.keycloak.org/docs/latest/upgrading/#new-hostname-options[the initial migration guide].
= Proxy option removed
The deprecated `proxy` option was removed. This option was deprecated in {project_name} 24 and replaced by the `proxy-headers` option in combination with hostname options as needed. For more details, see https://www.keycloak.org/server/reverseproxy[using a reverse proxy] and https://www.keycloak.org/docs/latest/upgrading/index.html#deprecated-proxy-option[the initial migration guide].
= Property `origin` in the `UserRepresentation` is deprecated
The `origin` property in the `UserRepresentation` is deprecated and planned to be removed in future releases.

View file

@ -183,3 +183,11 @@ Additionally, the following resources have been removed from the `common` theme:
- `node_modules/jquery`
If you previously used any of the removed resources in your theme, make sure to add them to your own theme resources instead.
= Hostname v1 feature removed
The deprecated hostname v1 feature was removed. This feature was deprecated in {project_name} 25 and replaced by hostname v2. If you are still using this feature, you must migrate to hostname v2. For more details, see the https://www.keycloak.org/server/hostname[Configuring the hostname (v2)] and https://www.keycloak.org/docs/latest/upgrading/#new-hostname-options[the initial migration guide].
= Proxy option removed
The deprecated `proxy` option was removed. This option was deprecated in {project_name} 24 and replaced by the `proxy-headers` option in combination with hostname options as needed. For more details, see https://www.keycloak.org/server/reverseproxy[using a reverse proxy] and https://www.keycloak.org/docs/latest/upgrading/index.html#deprecated-proxy-option[the initial upgrading guide].

View file

@ -1,158 +0,0 @@
<#import "/templates/guide.adoc" as tmpl>
<#import "/templates/kc.adoc" as kc>
<#import "/templates/links.adoc" as links>
<@tmpl.guide
title="Configuring the hostname (v1)"
summary="Learn how to configure the frontend and backchannel endpoints exposed by {project_name}."
includedOptions="hostname hostname-* proxy"
deniedCategories="hostname_v2">
WARNING: The hostname configuration options used in this guide are deprecated and will be removed in a future release. Therefore, it is recommended to use the new hostname configuration options. For more details, see <@links.server id="hostname"/> guide.
== Server Endpoints
{project_name} exposes different endpoints to talk with applications and allow access to the administration console. These endpoints
can be categorized into three main groups:
* Frontend
* Backend
* Administration Console
The base URL for each group has an important impact on how tokens are issued and validated, on how links are created for actions that require the user
to be redirected to {project_name} (for example, when resetting password through email links), and, most importantly, how applications will
discover these endpoints when fetching the OpenID Connect Discovery Document from `realms/++{realm-name}++/.well-known/openid-configuration`.
=== Frontend
The frontend endpoints are those accessible through a public domain and usually related to authentication/authorization flows that happen
through the front-channel. For instance, when an SPA wants to authenticate their users it redirects them to the `authorization_endpoint` so that users
can authenticate using their browsers through the front-channel.
By default, when the hostname settings are not set, the base URL for these endpoints is based on the incoming request so that the HTTP scheme,
host, port, and path, are the same from the request. The default behavior also has a direct impact on how the server is going to issue tokens given that the issuer is also based on
the URL set to the frontend endpoints. If the hostname settings are not set, the token issuer will also be based on the incoming request and also lack consistency if the client is requesting tokens using different URLs.
When deploying to production you usually want a consistent URL for the frontend endpoints and the token issuer regardless of how the request is constructed.
In order to achieve this consistency, you can set either the `hostname` or the `hostname-url` options.
Most of the time, it should be enough to set the `hostname` option in order to change only the *host* of the frontend URLs:
<@kc.start parameters="--hostname=<host>"/>
When using the `hostname` option the server is going to resolve the HTTP scheme, port, and path, automatically so that:
* `https` scheme is used unless you set `hostname-strict-https=false`
* if the `proxy-headers` option is set, the proxy will use the default ports (i.e.: 80 and 443). If the proxy uses a different port, it needs to be specified via the `hostname-url` configuration option
However, if you want to set not only the host but also a scheme, port, and path, you can set the `hostname-url` option:
<@kc.start parameters="--hostname-url=<scheme>://<host>:<port>/<path>"/>
This option gives you more flexibility as you can set the different parts of the URL from a single option. Note that
the `hostname` and `hostname-url` are mutually exclusive.
[NOTE]
====
By `hostname` and `proxy-headers` configuration options you affect only the static resources URLs, redirect URIs, OIDC well-known endpoints, etc. In order to change, where/on which port the server actually listens on, you need to use the `http/tls` configuration options (e.g. `http-host`, `https-port`, etc.). For more details, see <@links.server id="enabletls"/> and <@links.server id="all-config"/>.
====
=== Backend
The backend endpoints are those accessible through a public domain or through a private network. They are used for a direct communication
between the server and clients without any intermediary but plain HTTP requests. For instance, after the user is authenticated an SPA
wants to exchange the `code` sent by the server with a set of tokens by sending a token request to `token_endpoint`.
By default, the URLs for backend endpoints are also based on the incoming request. To override this behavior, set the `hostname-strict-backchannel` configuration option by entering this command:
<@kc.start parameters="--hostname=<value> --hostname-strict-backchannel=true"/>
By setting the `hostname-strict-backchannel` option, the URLs for the backend endpoints are going to be exactly the same as the frontend endpoints.
When all applications connected to {project_name} communicate through the public URL, set `hostname-strict-backchannel` to `true`.
Otherwise, leave this parameter as `false` to allow client-server communication through a private network.
=== Administration Console
The server exposes the administration console and static resources using a specific URL.
By default, the URLs for the administration console are also based on the incoming request. However, you can set a specific host or base URL if you want
to restrict access to the administration console using a specific URL. Similarly to how you set the frontend URLs, you can use the `hostname-admin` and `hostname-admin-url` options to achieve that.
Note that if HTTPS is enabled (`http-enabled` configuration option is set to false, which is the default setting for the production mode), the {project_name} server automatically assumes you want to use HTTPS URLs. The admin console then tries to contact {project_name} over HTTPS and HTTPS URLs are also used for its configured redirect/web origin URLs. It is not recommended for production, but you can use HTTP URL as `hostname-admin-url` to override this behaviour.
Most of the time, it should be enough to set the `hostname-admin` option in order to change only the *host* of the administration console URLs:
<@kc.start parameters="--hostname-admin=<host>"/>
However, if you want to set not only the host but also a scheme, port, and path, you can set the `hostname-admin-url` option:
<@kc.start parameters="--hostname-admin-url=<scheme>://<host>:<port>/<path>"/>
Note that the `hostname-admin` and `hostname-admin-url` are mutually exclusive.
To reduce attack surface, the administration endpoints for {project_name} and the Admin Console should not be publicly accessible.
Therefore, you can secure them by using a reverse proxy.
For more information about which paths to expose using a reverse proxy, see <@links.server id="reverseproxy"/>.
== Example Scenarios
The following are more example scenarios and the corresponding commands for setting up a hostname.
Note that the `start` command requires setting up TLS. The corresponding options are not shown for example purposes. For more details, see <@links.server id="enabletls"/>.
=== Exposing the server behind a TLS termination proxy
In this example, the server is running behind a TLS termination proxy and publicly available from `https://mykeycloak`.
.Configuration:
<@kc.start parameters="--hostname=mykeycloak --http-enabled=true --proxy-headers=forwarded|xforwarded"/>
=== Exposing the server without a proxy
In this example, the server is running without a proxy and exposed using a URL using HTTPS.
.{project_name} configuration:
<@kc.start parameters="--hostname-url=https://mykeycloak"/>
It is highly recommended using a TLS termination proxy in front of the server for security and availability reasons. For more details,
see <@links.server id="reverseproxy"/>.
=== Forcing backend endpoints to use the same URL the server is exposed
In this example, backend endpoints are exposed using the same URL used by the server so that clients always fetch the same URL
regardless of the origin of the request.
.{project_name} configuration:
<@kc.start parameters="--hostname=mykeycloak --hostname-strict-backchannel=true"/>
=== Exposing the server using a port other than the default ports
In this example, the server is accessible using a port other than the default ports.
.{project_name} configuration:
<@kc.start parameters="--hostname-url=https://mykeycloak:8989"/>
=== Exposing {project_name} behind a TLS reencrypt proxy using different ports
In this example, the server is running behind a proxy and both the server and the proxy are using their own certificates, so the communication between {project_name} and the proxy is encrypted. The reverse proxy uses the `Forwarded` header and does not set the `X-Forwarded-*` headers. We need to keep in mind that the proxy configuration options (as well as hostname configuration options) are not changing the ports on which the server actually is listening on (it changes only the ports of static resources like JavaScript and CSS links, OIDC well-known endpoints, redirect URIs, etc.). Therefore, we need to use HTTP configuration options to change the {project_name} server to internally listen on a different port, e.g. 8543. The proxy will be listening on the port 8443 (the port visible while accessing the console via a browser). The example hostname `my-keycloak.org` will be used for the server and similarly the admin console will be accessible via the `admin.my-keycloak.org` subdomain.
.{project_name} configuration:
<@kc.start parameters="--proxy-headers=forwarded --https-port=8543 --hostname-url=https://my-keycloak.org:8443 --hostname-admin-url=https://admin.my-keycloak.org:8443"/>
WARNING: Usage of the `proxy-headers` option rely on `Forwarded` and `X-Forwarded-*` headers, respectively, that have to be set and overwritten by the reverse proxy.
Misconfiguration may leave {project_name} exposed to security issues. For more details, see <@links.server id="reverseproxy"/>.
== Troubleshooting
To troubleshoot the hostname configuration, you can use a dedicated debug tool which can be enabled as:
.{project_name} configuration:
<@kc.start parameters="--hostname=mykeycloak --hostname-debug=true"/>
Then after {project_name} started properly, open your browser and go to:
`http://mykeycloak:8080/realms/<your-realm>/hostname-debug`
.By default, this endpoint is disabled (`--hostname-debug=false`)
</@tmpl.guide>

View file

@ -31,31 +31,6 @@ 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.
== Proxy modes (deprecated)
NOTE: The support for setting proxy modes is deprecated and will be removed in a future {project_name} release. Consider configuring accepted reverse proxy headers instead as described in the chapter above. For migration instructions consult the https://www.keycloak.org/docs/latest/upgrading/index.html#deprecated-proxy-option[Upgrading Guide].
For {project_name}, your choice of proxy modes depends on the TLS termination in your environment. The following proxy modes are available:
edge:: Enables communication through HTTP between the proxy and {project_name}.
This mode is suitable for deployments with a highly secure internal network where the reverse proxy keeps a secure connection (HTTP over TLS) with clients while communicating with {project_name} using HTTP.
reencrypt:: Requires communication through HTTPS between the proxy and {project_name}.
This mode is suitable for deployments where internal communication between the reverse proxy and {project_name} should also be protected.
Different keys and certificates are used on the reverse proxy as well as on {project_name}.
passthrough:: The proxy forwards the HTTPS connection to {project_name} without terminating TLS.
The secure connections between the server and clients are based on the keys and certificates used by the {project_name} server.
When in **edge** or **reencrypt** proxy mode, {project_name} will parse the following headers and expects the reverse proxy to set them:
* `Forwarded` as per https://www.rfc-editor.org/rfc/rfc7239.html[RFC7239]
* Non-standard `X-Forwarded-*`, such as `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`, and `X-Forwarded-Port`
=== Configure the proxy mode in {project_name}
To select the proxy mode, enter this command:
<@kc.start parameters="--proxy <mode>"/>
== 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.

View file

@ -1,60 +0,0 @@
package org.keycloak.config;
public class HostnameV1Options {
public static final Option<String> HOSTNAME = new OptionBuilder<>("hostname", String.class)
.category(OptionCategory.HOSTNAME_V1)
.description("Hostname for the Keycloak server.")
.build();
public static final Option<String> HOSTNAME_URL = new OptionBuilder<>("hostname-url", String.class)
.category(OptionCategory.HOSTNAME_V1)
.description("Set the base URL for frontend URLs, including scheme, host, port and path.")
.build();
public static final Option<String> HOSTNAME_ADMIN = new OptionBuilder<>("hostname-admin", String.class)
.category(OptionCategory.HOSTNAME_V1)
.description("The hostname for accessing the administration console. Use this option if you are exposing the administration console using a hostname other than the value set to the 'hostname' option.")
.build();
public static final Option<String> HOSTNAME_ADMIN_URL = new OptionBuilder<>("hostname-admin-url", String.class)
.category(OptionCategory.HOSTNAME_V1)
.description("Set the base URL for accessing the administration console, including scheme, host, port and path")
.build();
public static final Option<Boolean> HOSTNAME_STRICT = new OptionBuilder<>("hostname-strict", Boolean.class)
.category(OptionCategory.HOSTNAME_V1)
.description("Disables dynamically resolving the hostname from request headers. Should always be set to true in production, unless proxy verifies the Host header.")
.defaultValue(Boolean.TRUE)
.build();
public static final Option<Boolean> HOSTNAME_STRICT_HTTPS = new OptionBuilder<>("hostname-strict-https", Boolean.class)
.category(OptionCategory.HOSTNAME_V1)
.description("Forces frontend URLs to use the 'https' scheme. If set to false, the HTTP scheme is inferred from requests.")
.hidden()
.defaultValue(Boolean.TRUE)
.build();
public static final Option<Boolean> HOSTNAME_STRICT_BACKCHANNEL = new OptionBuilder<>("hostname-strict-backchannel", Boolean.class)
.category(OptionCategory.HOSTNAME_V1)
.description("By default backchannel URLs are dynamically resolved from request headers to allow internal and external applications. If all applications use the public URL this option should be enabled.")
.build();
public static final Option<String> HOSTNAME_PATH = new OptionBuilder<>("hostname-path", String.class)
.category(OptionCategory.HOSTNAME_V1)
.description("This should be set if proxy uses a different context-path for Keycloak.")
.build();
public static final Option<Integer> HOSTNAME_PORT = new OptionBuilder<>("hostname-port", Integer.class)
.category(OptionCategory.HOSTNAME_V1)
.description("The port used by the proxy when exposing the hostname. Set this option if the proxy uses a port other than the default HTTP and HTTPS ports.")
.defaultValue(-1)
.build();
public static final Option<Boolean> HOSTNAME_DEBUG = new OptionBuilder<>("hostname-debug", Boolean.class)
.category(OptionCategory.HOSTNAME_V1)
.description("Toggle the hostname debug page that is accessible at /realms/master/hostname-debug")
.defaultValue(Boolean.FALSE)
.build();
}

View file

@ -1,8 +1,5 @@
package org.keycloak.config;
import java.util.List;
import java.util.TreeSet;
public class ProxyOptions {
public enum Headers {
@ -10,39 +7,11 @@ public class ProxyOptions {
xforwarded
}
public enum Mode {
none(false),
edge,
reencrypt,
passthrough(false);
private final boolean proxyHeadersEnabled;
Mode(boolean proxyHeadersEnabled) {
this.proxyHeadersEnabled = proxyHeadersEnabled;
}
Mode() {
this(true);
}
public boolean isProxyHeadersEnabled() {
return proxyHeadersEnabled;
}
}
public static final Option<Headers> PROXY_HEADERS = new OptionBuilder<>("proxy-headers", Headers.class)
.category(OptionCategory.PROXY)
.description("The proxy headers that should be accepted by the server. Misconfiguration might leave the server exposed to security vulnerabilities. Takes precedence over the deprecated proxy option.")
.build();
public static final Option<Mode> PROXY = new OptionBuilder<>("proxy", Mode.class)
.category(OptionCategory.PROXY)
.description("The proxy address forwarding mode if the server is behind a reverse proxy.")
.defaultValue(Mode.none)
.deprecated(new TreeSet<>(List.of(PROXY_HEADERS.getKey(), HttpOptions.HTTP_ENABLED.getKey())))
.build();
public static final Option<Boolean> PROXY_FORWARDED_HOST = new OptionBuilder<>("proxy-forwarded-host", Boolean.class)
.category(OptionCategory.PROXY)
.defaultValue(Boolean.FALSE)

View file

@ -1,48 +0,0 @@
package org.keycloak.quarkus.runtime.configuration.mappers;
import org.keycloak.common.Profile;
import org.keycloak.config.HostnameV1Options;
import java.util.List;
import java.util.stream.Stream;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
final class HostnameV1PropertyMappers {
private HostnameV1PropertyMappers(){}
public static PropertyMapper<?>[] getHostnamePropertyMappers() {
return Stream.of(
fromOption(HostnameV1Options.HOSTNAME)
.to("kc.spi-hostname-default-hostname")
.paramLabel("hostname"),
fromOption(HostnameV1Options.HOSTNAME_URL)
.to("kc.spi-hostname-default-hostname-url")
.paramLabel("url"),
fromOption(HostnameV1Options.HOSTNAME_ADMIN)
.to("kc.spi-hostname-default-admin")
.paramLabel("hostname"),
fromOption(HostnameV1Options.HOSTNAME_ADMIN_URL)
.to("kc.spi-hostname-default-admin-url")
.paramLabel("url"),
fromOption(HostnameV1Options.HOSTNAME_STRICT)
.to("kc.spi-hostname-default-strict"),
fromOption(HostnameV1Options.HOSTNAME_STRICT_HTTPS)
.to("kc.spi-hostname-default-strict-https"),
fromOption(HostnameV1Options.HOSTNAME_STRICT_BACKCHANNEL)
.to("kc.spi-hostname-default-strict-backchannel"),
fromOption(HostnameV1Options.HOSTNAME_PATH)
.to("kc.spi-hostname-default-path")
.paramLabel("path"),
fromOption(HostnameV1Options.HOSTNAME_PORT)
.to("kc.spi-hostname-default-hostname-port")
.paramLabel("port"),
fromOption(HostnameV1Options.HOSTNAME_DEBUG)
.to("kc.spi-hostname-default-hostname-debug")
)
.map(b -> b.isEnabled(() -> Profile.isFeatureEnabled(Profile.Feature.HOSTNAME_V1), "hostname:v1 feature is enabled").build())
.toArray(s -> new PropertyMapper<?>[s]);
}
}

View file

@ -48,7 +48,6 @@ public final class PropertyMappers {
MAPPERS.addAll(CachingPropertyMappers.getClusteringPropertyMappers());
MAPPERS.addAll(DatabasePropertyMappers.getDatabasePropertyMappers());
MAPPERS.addAll(HostnameV2PropertyMappers.getHostnamePropertyMappers());
MAPPERS.addAll(HostnameV1PropertyMappers.getHostnamePropertyMappers());
MAPPERS.addAll(HttpPropertyMappers.getHttpPropertyMappers());
MAPPERS.addAll(HealthPropertyMappers.getHealthPropertyMappers());
MAPPERS.addAll(ConfigKeystorePropertyMappers.getConfigKeystorePropertyMappers());

View file

@ -1,12 +1,10 @@
package org.keycloak.quarkus.runtime.configuration.mappers;
import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue;
import org.keycloak.config.ProxyOptions;
import java.util.Optional;
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
final class ProxyPropertyMappers {
@ -20,9 +18,6 @@ final class ProxyPropertyMappers {
.transformer((v, c) -> proxyEnabled(null, v, c))
.paramLabel("headers")
.build(),
fromOption(ProxyOptions.PROXY)
.paramLabel("mode")
.build(),
fromOption(ProxyOptions.PROXY_FORWARDED_HOST)
.to("quarkus.http.proxy.enable-forwarded-host")
.mapFrom("proxy-headers")
@ -42,7 +37,7 @@ final class ProxyPropertyMappers {
}
private static Optional<String> proxyEnabled(ProxyOptions.Headers testHeader, Optional<String> value, ConfigSourceInterceptorContext context) {
boolean enabled;
boolean enabled = false;
if (value.isPresent()) { // proxy-headers explicitly configured
if (testHeader != null) {
@ -50,18 +45,6 @@ final class ProxyPropertyMappers {
} else {
enabled = true;
}
} else { // fallback to the deprecated proxy option
String proxyKey = NS_KEYCLOAK_PREFIX + ProxyOptions.PROXY.getKey();
ConfigValue proxyOptionConfigValue = context.proceed(proxyKey);
ProxyOptions.Mode proxyMode;
if (proxyOptionConfigValue == null) { // neither proxy-headers nor proxy options are configured, falling back to default proxy value which is "none"
proxyMode = (ProxyOptions.Mode) PropertyMappers.getMapper(proxyKey).getDefaultValue().orElseThrow();
} else {
proxyMode = ProxyOptions.Mode.valueOf(proxyOptionConfigValue.getValue());
}
enabled = proxyMode.isProxyHeadersEnabled();
}
return Optional.of(String.valueOf(enabled));

View file

@ -1,365 +0,0 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.quarkus.runtime.hostname;
import static org.keycloak.common.util.UriUtils.checkUrl;
import static org.keycloak.config.ProxyOptions.PROXY;
import static org.keycloak.config.ProxyOptions.PROXY_HEADERS;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getConfigValue;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getKcConfigValue;
import static org.keycloak.urls.UrlType.ADMIN;
import static org.keycloak.urls.UrlType.LOCAL_ADMIN;
import static org.keycloak.urls.UrlType.BACKEND;
import static org.keycloak.urls.UrlType.FRONTEND;
import static org.keycloak.utils.StringUtil.isNotBlank;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.function.BiFunction;
import java.util.function.Function;
import jakarta.ws.rs.core.UriInfo;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.common.Profile;
import org.keycloak.common.Profile.Feature;
import org.keycloak.common.enums.SslRequired;
import org.keycloak.config.HostnameV1Options;
import org.keycloak.config.ProxyOptions;
import org.keycloak.config.ProxyOptions.Mode;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.urls.HostnameProvider;
import org.keycloak.urls.HostnameProviderFactory;
import org.keycloak.urls.UrlType;
import org.keycloak.utils.KeycloakSessionUtil;
public final class DefaultHostnameProvider implements HostnameProvider, HostnameProviderFactory, EnvironmentDependentProviderFactory {
private static final Logger LOGGER = Logger.getLogger(DefaultHostnameProvider.class);
private static final String REALM_URI_SESSION_ATTRIBUTE = DefaultHostnameProvider.class.getName() + ".realmUrl";
private static final int DEFAULT_HTTPS_PORT_VALUE = 443;
private static final int RESTEASY_DEFAULT_PORT_VALUE = -1;
private String frontEndHostName;
private String defaultPath;
private String defaultHttpScheme;
private int defaultTlsPort;
private boolean noProxy;
private String adminHostName;
private Boolean strictBackChannel;
private boolean hostnameEnabled;
private boolean strictHttps;
private int hostnamePort;
private URI frontEndBaseUri;
private URI adminBaseUri;
private URI localAdminUri;
@Override
public String getScheme(UriInfo originalUriInfo, UrlType urlType) {
if (ADMIN.equals(urlType)) {
return fromBaseUriOrDefault(URI::getScheme, adminBaseUri, getScheme(originalUriInfo));
}
if (LOCAL_ADMIN.equals(urlType)) {
return fromBaseUriOrDefault(URI::getScheme, localAdminUri, getScheme(originalUriInfo));
}
String scheme = forNonStrictBackChannel(originalUriInfo, urlType, this::getScheme, this::getScheme);
if (scheme != null) {
return scheme;
}
return fromFrontEndUrl(originalUriInfo, URI::getScheme, this::getScheme, defaultHttpScheme);
}
@Override
public String getHostname(UriInfo originalUriInfo, UrlType urlType) {
if (ADMIN.equals(urlType)) {
return fromBaseUriOrDefault(URI::getHost, adminBaseUri, adminHostName == null ? getHostname(originalUriInfo) : adminHostName);
}
if (LOCAL_ADMIN.equals(urlType)) {
return fromBaseUriOrDefault(URI::getHost, localAdminUri, getHostname(originalUriInfo));
}
String hostname = forNonStrictBackChannel(originalUriInfo, urlType, this::getHostname, this::getHostname);
if (hostname != null) {
return hostname;
}
return fromFrontEndUrl(originalUriInfo, URI::getHost, this::getHostname, frontEndHostName);
}
@Override
public String getContextPath(UriInfo originalUriInfo, UrlType urlType) {
if (ADMIN.equals(urlType)) {
return fromBaseUriOrDefault(URI::getPath, adminBaseUri, getContextPath(originalUriInfo));
}
if (LOCAL_ADMIN.equals(urlType)) {
return fromBaseUriOrDefault(URI::getPath, localAdminUri, getContextPath(originalUriInfo));
}
String path = forNonStrictBackChannel(originalUriInfo, urlType, this::getContextPath, this::getContextPath);
if (path != null) {
return path;
}
return fromFrontEndUrl(originalUriInfo, URI::getPath, this::getContextPath, defaultPath);
}
@Override
public int getPort(UriInfo originalUriInfo, UrlType urlType) {
if (ADMIN.equals(urlType)) {
return fromBaseUriOrDefault(URI::getPort, adminBaseUri, getRequestPort(originalUriInfo));
}
if (LOCAL_ADMIN.equals(urlType)) {
return fromBaseUriOrDefault(URI::getPort, localAdminUri, getRequestPort(originalUriInfo));
}
Integer port = forNonStrictBackChannel(originalUriInfo, urlType, this::getPort, this::getRequestPort);
if (port != null) {
return port;
}
if (hostnameEnabled && !noProxy) {
return fromBaseUriOrDefault(URI::getPort, frontEndBaseUri, hostnamePort);
}
return fromFrontEndUrl(originalUriInfo, URI::getPort, this::getPort, hostnamePort == -1 ? getPort(originalUriInfo) : hostnamePort);
}
@Override
public int getPort(UriInfo originalUriInfo) {
return noProxy && strictHttps ? defaultTlsPort : getRequestPort(originalUriInfo);
}
private <T> T forNonStrictBackChannel(UriInfo originalUriInfo, UrlType urlType,
BiFunction<UriInfo, UrlType, T> frontEndTypeResolver, Function<UriInfo, T> defaultResolver) {
if (BACKEND.equals(urlType) && !strictBackChannel) {
if (isHostFromFrontEndUrl(originalUriInfo)) {
return frontEndTypeResolver.apply(originalUriInfo, FRONTEND);
}
return defaultResolver.apply(originalUriInfo);
}
return null;
}
private <T> T fromFrontEndUrl(UriInfo originalUriInfo, Function<URI, T> frontEndTypeResolver, Function<UriInfo, T> defaultResolver,
T defaultValue) {
URI frontEndUrl = getRealmFrontEndUrl();
if (frontEndUrl != null) {
return frontEndTypeResolver.apply(frontEndUrl);
}
if (frontEndBaseUri != null) {
return frontEndTypeResolver.apply(frontEndBaseUri);
}
return defaultValue == null ? defaultResolver.apply(originalUriInfo) : defaultValue;
}
private boolean isHostFromFrontEndUrl(UriInfo originalUriInfo) {
String requestHost = getHostname(originalUriInfo);
String frontendUrlHost = getHostname(originalUriInfo, FRONTEND);
if (requestHost.equals(frontendUrlHost)) {
return true;
}
URI realmUrl = getRealmFrontEndUrl();
return realmUrl != null && requestHost.equals(realmUrl.getHost());
}
protected URI getRealmFrontEndUrl() {
KeycloakSession session = KeycloakSessionUtil.getKeycloakSession();
if (session == null) {
return null;
}
RealmModel realm = session.getContext().getRealm();
if (realm == null) {
return null;
}
String realmUriKey = realm.getId() + REALM_URI_SESSION_ATTRIBUTE;
URI realmUrl = (URI) session.getAttribute(realmUriKey);
if (realmUrl == null) {
String frontendUrl = realm.getAttribute("frontendUrl");
if (isNotBlank(frontendUrl)) {
try {
checkUrl(SslRequired.NONE, frontendUrl, "frontendUrl");
realmUrl = URI.create(frontendUrl);
session.setAttribute(realmUriKey, realmUrl);
return realmUrl;
} catch (IllegalArgumentException e) {
LOGGER.errorf(e, "Failed to parse realm frontendUrl '%s'. Falling back to global value.", frontendUrl);
}
}
}
return realmUrl;
}
@Override
public void close() {
}
@Override
public String getId() {
return "default";
}
@Override
public HostnameProvider create(KeycloakSession session) {
return this;
}
@Override
public void init(Config.Scope config) {
boolean isHttpEnabled = Boolean.parseBoolean(getConfigValue("kc.http-enabled").getValue());
String configPath = getConfigValue("kc.http-relative-path").getValue();
if (!configPath.startsWith("/")) {
configPath = "/" + configPath;
}
String httpsPort = getConfigValue("kc.https-port").getValue();
String configPort = isHttpEnabled ? getConfigValue("kc.http-port").getValue() : httpsPort ;
String scheme = isHttpEnabled ? "http://" : "https://";
localAdminUri = URI.create(scheme + "localhost:" + configPort + configPath);
frontEndHostName = config.get("hostname");
try {
String url = config.get("hostname-url");
if (url != null) {
frontEndBaseUri = new URL(url).toURI();
}
} catch (MalformedURLException | URISyntaxException cause) {
throw new RuntimeException("Invalid base URL for FrontEnd URLs: " + config.get("hostname-url"), cause);
}
if (frontEndHostName != null && frontEndBaseUri != null) {
throw new RuntimeException("You can not set both '" + HostnameV1Options.HOSTNAME.getKey() + "' and '" + HostnameV1Options.HOSTNAME_URL.getKey() + "' options");
}
if (config.getBoolean("strict", false) && (frontEndHostName == null && frontEndBaseUri == null)) {
throw new RuntimeException("Strict hostname resolution configured but no hostname setting provided");
}
hostnameEnabled = (frontEndHostName != null || frontEndBaseUri != null);
if (frontEndBaseUri == null) {
strictHttps = hostnameEnabled && config.getBoolean("strict-https", false);
} else {
frontEndHostName = frontEndBaseUri.getHost();
strictHttps = "https".equals(frontEndBaseUri.getScheme());
}
if (strictHttps) {
defaultHttpScheme = "https";
}
defaultPath = config.get("path", frontEndBaseUri == null ? null : frontEndBaseUri.getPath());
if (getKcConfigValue(PROXY_HEADERS.getKey()).getValue() != null) { // proxy-headers option was explicitly configured
noProxy = false;
} else { // falling back to proxy option
noProxy = Mode.none.equals(ProxyOptions.Mode.valueOf(getKcConfigValue(PROXY.getKey()).getValue()));
}
defaultTlsPort = Integer.parseInt(httpsPort);
if (defaultTlsPort == DEFAULT_HTTPS_PORT_VALUE) {
defaultTlsPort = RESTEASY_DEFAULT_PORT_VALUE;
}
if (frontEndBaseUri == null) {
hostnamePort = Integer.parseInt(getConfigValue("kc.hostname-port").getValue());
} else {
hostnamePort = frontEndBaseUri.getPort();
}
adminHostName = config.get("admin");
try {
String url = config.get("admin-url");
if (url != null) {
adminBaseUri = new URL(url).toURI();
}
} catch (MalformedURLException | URISyntaxException cause) {
throw new RuntimeException("Invalid base URL for Admin URLs: " + config.get("admin-url"), cause);
}
if (adminHostName != null && adminBaseUri != null) {
throw new RuntimeException("You can not set both '" + HostnameV1Options.HOSTNAME_ADMIN.getKey() + "' and '" + HostnameV1Options.HOSTNAME_ADMIN_URL.getKey() + "' options");
}
if (adminBaseUri != null) {
adminHostName = adminBaseUri.getHost();
}
strictBackChannel = config.getBoolean("strict-backchannel", false);
LOGGER.infov("Hostname settings: Base URL: {0}, Hostname: {1}, Strict HTTPS: {2}, Path: {3}, Strict BackChannel: {4}, Admin URL: {5}, Admin: {6}, Port: {7}, Proxied: {8}",
frontEndBaseUri == null ? "<unset>" : frontEndBaseUri,
frontEndHostName == null ? frontEndBaseUri == null ? "<request>" : frontEndBaseUri : frontEndHostName,
strictHttps,
defaultPath == null ? "<request>" : "".equals(defaultPath) ? "/" : defaultPath,
strictBackChannel,
adminBaseUri == null ? "<unset>" : adminBaseUri,
adminHostName == null ? adminBaseUri == null ? "<request>" : adminBaseUri : adminHostName,
String.valueOf(hostnamePort),
!noProxy);
}
private int getRequestPort(UriInfo uriInfo) {
return uriInfo.getBaseUri().getPort();
}
private <T> T fromBaseUriOrDefault(Function<URI, T> resolver, URI baseUri, T defaultValue) {
if (baseUri != null) {
return resolver.apply(baseUri);
}
return defaultValue;
}
@Override
public boolean isSupported(Config.Scope config) {
return Profile.isFeatureEnabled(Feature.HOSTNAME_V1);
}
}

View file

@ -1 +0,0 @@
org.keycloak.quarkus.runtime.hostname.DefaultHostnameProvider

View file

@ -145,15 +145,11 @@ public class ConfigurationTest extends AbstractConfigurationTest {
@Test
public void testResolveTransformedValue() {
ConfigArgsConfigSource.setCliArgs("");
assertEquals("none", createConfig().getConfigValue("kc.proxy").getValue());
ConfigArgsConfigSource.setCliArgs("--proxy=none");
assertEquals("none", createConfig().getConfigValue("kc.proxy").getValue());
ConfigArgsConfigSource.setCliArgs("");
assertEquals("none", createConfig().getConfigValue("kc.proxy").getValue());
ConfigArgsConfigSource.setCliArgs("--proxy=none", "--http-enabled=false");
assertEquals("false", createConfig().getConfigValue("kc.http-enabled").getValue());
ConfigArgsConfigSource.setCliArgs("--proxy=none", "--http-enabled=true");
assertEquals("true", createConfig().getConfigValue("kc.http-enabled").getValue());
assertEquals("false", createConfig().getConfigValue("kc.proxy-allow-forwarded-header").getValue());
ConfigArgsConfigSource.setCliArgs("--proxy-headers=xforwarded");
assertEquals("false", createConfig().getConfigValue("kc.proxy-allow-forwarded-header").getValue());
ConfigArgsConfigSource.setCliArgs("--proxy-headers=forwarded");
assertEquals("true", createConfig().getConfigValue("kc.proxy-allow-forwarded-header").getValue());
}
@Test

View file

@ -1,235 +0,0 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.it.cli.dist;
import io.quarkus.test.junit.main.Launch;
import io.restassured.RestAssured;
import io.restassured.config.RedirectConfig;
import io.restassured.config.RestAssuredConfig;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpHeaders;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.keycloak.it.junit5.extension.DistributionTest;
import org.keycloak.it.junit5.extension.RawDistOnly;
import org.keycloak.it.junit5.extension.WithEnvVars;
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
import org.keycloak.quarkus.runtime.services.resources.DebugHostnameSettingsResource;
import java.util.function.Consumer;
import static io.restassured.RestAssured.when;
import static org.hamcrest.MatcherAssert.assertThat;
@DistributionTest(keepAlive = true, enableTls = true, defaultOptions = { "--http-enabled=true", "--features=hostname:v1" })
@WithEnvVars({"KC_BOOTSTRAP_ADMIN_USERNAME", "admin123", "KC_BOOTSTRAP_ADMIN_PASSWORD", "admin123"})
@RawDistOnly(reason = "Containers are immutable")
public class HostnameV1DistTest {
@BeforeAll
public static void onBeforeAll() {
RestAssured.useRelaxedHTTPSValidation();
RestAssuredConfig config = RestAssured.config;
RestAssured.config = config.redirect(RedirectConfig.redirectConfig().followRedirects(false));
}
@Test
@Launch({ "start", "--hostname=mykeycloak.org", "--hostname-strict-https=false" })
public void testSchemeAndPortFromRequestWhenNoProxySet() {
assertFrontEndUrl("http://mykeycloak.org:8080", "http://mykeycloak.org:8080/");
assertFrontEndUrl("http://localhost:8080", "http://mykeycloak.org:8080/");
assertFrontEndUrl("https://localhost:8443", "https://mykeycloak.org:8443/");
}
@Test
@Launch({ "start", "--hostname=mykeycloak.org" })
public void testForceHttpsSchemeAndPortWhenStrictHttpsEnabled() {
assertFrontEndUrl("http://mykeycloak.org:8080", "https://mykeycloak.org:8443/");
assertFrontEndUrl("http://localhost:8080", "https://mykeycloak.org:8443/");
}
@Test
@Launch({ "start", "--hostname=mykeycloak.org", "--hostname-port=1234" })
public void testForceHostnamePortWhenNoProxyIsSet() {
assertFrontEndUrl("http://mykeycloak.org:8080", "https://mykeycloak.org:1234/");
assertFrontEndUrl("https://mykeycloak.org:8443", "https://mykeycloak.org:1234/");
}
@Test
@Launch({ "start", "--hostname=mykeycloak.org", "--proxy=edge" })
public void testUseDefaultPortsWhenProxyIsSet() {
assertFrontEndUrl("http://mykeycloak.org:8080", "https://mykeycloak.org/");
assertFrontEndUrl("https://mykeycloak.org:8443", "https://mykeycloak.org/");
}
@Test
@Launch({ "start", "--hostname=mykeycloak.org", "--proxy-headers=forwarded" })
public void testUseDefaultPortsWhenProxyHeadersIsSet() {
assertFrontEndUrl("http://mykeycloak.org:8080", "https://mykeycloak.org/");
assertFrontEndUrl("https://mykeycloak.org:8443", "https://mykeycloak.org/");
}
@Test
@Launch({ "start", "--hostname=mykeycloak.org", "--proxy=edge", "--hostname-strict-https=false" })
public void testUseDefaultPortsWhenProxyIsSetNoStrictHttps() {
assertFrontEndUrl("http://mykeycloak.org:8080", "http://mykeycloak.org/");
assertFrontEndUrl("https://mykeycloak.org:8443", "https://mykeycloak.org/");
}
@Test
@Launch({ "start", "--hostname=mykeycloak.org", "--proxy=edge", "--hostname-strict-https=true" })
public void testUseDefaultPortsAndHttpsSchemeWhenProxyIsSetAndStrictHttpsEnabled() {
assertFrontEndUrl("http://mykeycloak.org:8080", "https://mykeycloak.org/");
}
@Test
@Launch({ "start", "--hostname=mykeycloak.org" })
public void testBackEndUrlFromRequest() {
assertBackEndUrl("http://localhost:8080", "http://localhost:8080/");
assertBackEndUrl("https://localhost:8443", "https://localhost:8443/");
}
@Test
@Launch({ "start", "--hostname=mykeycloak.org", "--hostname-strict-backchannel=true" })
public void testBackEndUrlSameAsFrontEndUrl() {
assertBackEndUrl("http://localhost:8080", "https://mykeycloak.org:8443/");
}
@Test
@Launch({ "start", "--hostname=mykeycloak.org", "--hostname-path=/auth", "--hostname-strict-backchannel=true" })
public void testSetHostnamePath() {
assertFrontEndUrl("http://localhost:8080", "https://mykeycloak.org:8443/auth/");
assertBackEndUrl("http://localhost:8080", "https://mykeycloak.org:8443/auth/");
}
@Test
@Launch({ "start", "--hostname=mykeycloak.org", "--https-port=8543", "--hostname-strict-https=true" })
public void testDefaultTlsPortChangeWhenHttpPortSet() {
assertFrontEndUrl("http://mykeycloak.org:8080", "https://mykeycloak.org:8543/");
}
@Test
@Launch({ "start", "--hostname=mykeycloak.org", "--hostname-port=8543" })
public void testWelcomePageAdminUrl() {
when().get("http://mykeycloak.org:8080").then().header(HttpHeaders.LOCATION, Matchers.containsString("http://mykeycloak.org:8080/admin/"));
when().get("https://mykeycloak.org:8443").then().header(HttpHeaders.LOCATION, Matchers.containsString("https://mykeycloak.org:8443/admin/"));
when().get("http://localhost:8080").then().header(HttpHeaders.LOCATION, Matchers.containsString("http://localhost:8080/admin/"));
when().get("https://localhost:8443").then().header(HttpHeaders.LOCATION, Matchers.containsString("https://localhost:8443/admin/"));
}
@Test
@Launch({ "start", "--hostname=mykeycloak.org", "--hostname-debug=true" })
public void testDebugHostnameSettingsEnabled() {
when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().statusCode(200);
when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().body(Matchers.containsString("Configuration property"));
when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().body(Matchers.containsString("Server mode"));
when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().body(Matchers.containsString("production [start]"));
when().get("http://mykeycloak.org:8080/realms/master/" +
DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX +
"/" + DebugHostnameSettingsResource.PATH_FOR_TEST_CORS_IN_HEADERS
).then().statusCode(200);
when().get("http://localhost:8080/realms/master/" +
DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX +
"/" + DebugHostnameSettingsResource.PATH_FOR_TEST_CORS_IN_HEADERS)
.then()
.body(Matchers.containsString(DebugHostnameSettingsResource.PATH_FOR_TEST_CORS_IN_HEADERS + "-OK"));
when().get("http://localhost:8080/realms/non-existent/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().statusCode(404);
}
@Test
@Launch({ "start", "--hostname=mykeycloak.org", "--hostname-debug=false" })
public void testDebugHostnameSettingsDisabledBySetting() {
when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().statusCode(404);
}
@Test
@Launch({ "start", "--hostname=mykeycloak.org"})
public void testDebugHostnameSettingsDisabledByDefault() {
when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().statusCode(404);
}
@Test
@Launch({ "start", "--hostname=mykeycloak.org", "--hostname-admin=mykeycloakadmin.org" })
public void testHostnameAdminSet() {
when().get("https://mykeycloak.org:8443/admin/master/console/").then().body(Matchers.containsString("\"authUrl\": \"https://mykeycloakadmin.org:8443\""));
when().get("https://mykeycloak.org:8443/realms/master/protocol/openid-connect/auth?client_id=security-admin-console&redirect_uri=https://mykeycloakadmin.org:8443/admin/master/console&state=02234324-d91e-4bf2-8396-57498e96b12a&response_mode=fragment&response_type=code&scope=openid&nonce=f8f3812e-e349-4bbf-8d15-cbba4927f5e5&code_challenge=7qjD_v11WGkt1ig-ZFHxJdrEvuTlzjFRgRGQ_5ADcko&code_challenge_method=S256").then().body(Matchers.containsString("Sign in to your account"));
when().get("http://localhost:8080/admin/master/console/").then().body(Matchers.containsString("\"authUrl\": \"http://mykeycloakadmin.org:8080\""));
}
@Test
@Launch({"start", "--hostname=mykeycloak.org", "--hostname-debug=true"})
public void testHostnameAdminFromHeaders() {
when().get("https://mykeycloak.org:8443/admin/master/console/").then().body(Matchers.containsString("\"authUrl\": \"https://mykeycloak.org:8443\""));
when().get("https://mykeycloak.org:8443/realms/master/protocol/openid-connect/auth?client_id=security-admin-console&redirect_uri=https://mykeycloak.org:8443/admin/master/console&state=02234324-d91e-4bf2-8396-57498e96b12a&response_mode=fragment&response_type=code&scope=openid&nonce=f8f3812e-e349-4bbf-8d15-cbba4927f5e5&code_challenge=7qjD_v11WGkt1ig-ZFHxJdrEvuTlzjFRgRGQ_5ADcko&code_challenge_method=S256").then().body(Matchers.containsString("Sign in to your account"));
// Admin URL should be resolved from headers
when().get("http://localhost:8080/admin/master/console/").then().body(Matchers.containsString("\"authUrl\": \"http://localhost:8080\""));
when().get("http://localhost:8080/realms/master/protocol/openid-connect/auth?client_id=security-admin-console&redirect_uri=http://localhost:8080/admin/master/console&state=02234324-d91e-4bf2-8396-57498e96b12a&response_mode=fragment&response_type=code&scope=openid&nonce=f8f3812e-e349-4bbf-8d15-cbba4927f5e5&code_challenge=7qjD_v11WGkt1ig-ZFHxJdrEvuTlzjFRgRGQ_5ADcko&code_challenge_method=S256").then().body(Matchers.containsString("Sign in to your account"));
Consumer<String> assertDebugAdmin = (url) -> {
final var body = StringUtils.deleteWhitespace(when().get(url + "/realms/master/hostname-debug").then().extract().response().body().asString());
assertThat(body, Matchers.containsString("<td>Admin</td><td>" + url));
};
assertDebugAdmin.accept("https://mykeycloak.org:8443");
assertDebugAdmin.accept("http://localhost:8080");
}
@Test
@Launch({ "start", "--hostname=mykeycloak.org" })
public void testInvalidRedirectUriWhenAdminNotSet() {
when().get("https://mykeycloak.org:8443/realms/master/protocol/openid-connect/auth?client_id=security-admin-console&redirect_uri=https://mykeycloakadmin.127.0.0.1.nip.io:8443/admin/master/console&state=02234324-d91e-4bf2-8396-57498e96b12a&response_mode=fragment&response_type=code&scope=openid&nonce=f8f3812e-e349-4bbf-8d15-cbba4927f5e5&code_challenge=7qjD_v11WGkt1ig-ZFHxJdrEvuTlzjFRgRGQ_5ADcko&code_challenge_method=S256").then().body(Matchers.containsString("Invalid parameter: redirect_uri"));
}
@Test
@Launch({ "start", "--proxy=edge", "--hostname-url=http://mykeycloak.org:1234" })
public void testFrontendUrl() {
assertFrontEndUrl("https://mykeycloak.org:8443", "http://mykeycloak.org:1234/");
}
@Test
@Launch({ "start", "--proxy=edge", "--hostname=mykeycloak.org", "--hostname-admin-url=http://mykeycloakadmin.org:1234" })
public void testAdminUrl() {
when().get("https://mykeycloak.org:8443").then().header(HttpHeaders.LOCATION, Matchers.containsString("http://mykeycloakadmin.org:1234/admin/"));
when().get("http://localhost:8080/admin/master/console/").then().body(Matchers.containsString("\"authUrl\": \"http://mykeycloakadmin.org:1234\""));
}
@Test
@Launch({ "start", "--hostname-strict=false" })
public void testStrictHttpsDisabledIfHostnameDisabled() {
assertFrontEndUrl("http://mykeycloak.org:8080", "http://mykeycloak.org:8080/");
}
private OIDCConfigurationRepresentation getServerMetadata(String baseUrl) {
return when().get(baseUrl + "/realms/master/.well-known/openid-configuration").as(OIDCConfigurationRepresentation.class);
}
private void assertFrontEndUrl(String requestBaseUrl, String expectedBaseUrl) {
Assert.assertEquals(expectedBaseUrl + "realms/master/protocol/openid-connect/auth", getServerMetadata(requestBaseUrl)
.getAuthorizationEndpoint());
}
private void assertBackEndUrl(String requestBaseUrl, String expectedBaseUrl) {
Assert.assertEquals(expectedBaseUrl + "realms/master/protocol/openid-connect/token", getServerMetadata(requestBaseUrl)
.getTokenEndpoint());
}
}

View file

@ -1,155 +0,0 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.it.cli.dist;
import io.quarkus.test.junit.main.Launch;
import io.restassured.RestAssured;
import io.restassured.config.RedirectConfig;
import io.restassured.config.RestAssuredConfig;
import org.apache.http.HttpHeaders;
import org.junit.Assert;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.keycloak.it.junit5.extension.DistributionTest;
import org.keycloak.it.junit5.extension.RawDistOnly;
import org.keycloak.it.junit5.extension.WithEnvVars;
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
import static io.restassured.RestAssured.given;
import static io.restassured.RestAssured.when;
import static org.hamcrest.Matchers.containsString;
@DistributionTest(keepAlive = true, enableTls = true, defaultOptions = "--features=hostname:v1")
@WithEnvVars({"KC_BOOTSTRAP_ADMIN_USERNAME", "admin123", "KC_BOOTSTRAP_ADMIN_PASSWORD", "admin123"})
@RawDistOnly(reason = "Containers are immutable")
public class ProxyHostnameV1DistTest {
@BeforeAll
public static void onBeforeAll() {
RestAssured.useRelaxedHTTPSValidation();
RestAssuredConfig config = RestAssured.config;
RestAssured.config = config.redirect(RedirectConfig.redirectConfig().followRedirects(false));
}
@Test
@Launch({ "start-dev", "--hostname=mykeycloak.org" })
public void testSchemeAndPortFromRequestWhenNoProxySet() {
assertFrontEndUrl("http://mykeycloak.org:8080", "http://mykeycloak.org:8080/");
assertFrontEndUrl("http://localhost:8080", "http://mykeycloak.org:8080/");
assertFrontEndUrl("https://localhost:8443", "https://mykeycloak.org:8443/");
assertForwardedHeaderIsIgnored();
assertXForwardedHeadersAreIgnored();
}
@Test
@Launch({ "start-dev", "--hostname=mykeycloak.org", "--proxy=edge" })
public void testXForwardedHeadersWithEdge() {
assertXForwardedHeaders();
}
@Test
@Launch({ "start-dev", "--hostname=mykeycloak.org", "--proxy=edge" })
public void testForwardedHeadersWithEdge() {
assertForwardedHeader();
}
@Test
@Launch({ "start-dev", "--hostname=mykeycloak.org", "--proxy=reencrypt" })
public void testXForwardedHeadersWithReencrypt() {
assertXForwardedHeaders();
}
@Test
@Launch({ "start-dev", "--hostname=mykeycloak.org", "--proxy=passthrough" })
public void testProxyHeadersIgnoredWithPassthrough() {
assertForwardedHeaderIsIgnored();
assertXForwardedHeadersAreIgnored();
}
@Test
@Launch({ "start-dev", "--hostname=mykeycloak.org", "--proxy-headers=forwarded" })
public void testForwardedProxyHeaders() {
assertForwardedHeader();
assertXForwardedHeadersAreIgnored();
}
@Test
@Launch({ "start-dev", "--hostname=mykeycloak.org", "--proxy-headers=xforwarded" })
public void testXForwardedProxyHeaders() {
assertForwardedHeaderIsIgnored();
assertXForwardedHeaders();
}
@Test
@Launch({ "start-dev", "--hostname=mykeycloak.org", "--proxy-headers=xforwarded", "--proxy=reencrypt" })
public void testProxyHeadersTakePrecedenceOverProxyReencryptOption() {
assertForwardedHeaderIsIgnored();
assertXForwardedHeaders();
}
@Test
@Launch({ "start-dev", "--hostname=mykeycloak.org", "--proxy-headers=xforwarded", "--proxy=none" })
public void testProxyHeadersTakePrecedenceOverProxyNoneOption() {
assertForwardedHeaderIsIgnored();
assertXForwardedHeaders();
}
@Test
@Launch({ "start-dev", "--hostname-url=http://mykeycloak.org:1234", "--hostname-admin-url=http://mykeycloakadmin.127.0.0.1.nip.io:1234", "--proxy=edge" })
public void testIgnoreForwardedHeadersWhenFrontendUrlSet() {
given().header("X-Forwarded-Host", "test").when().get("http://mykeycloak.org:8080").then().header(HttpHeaders.LOCATION, containsString("http://mykeycloakadmin.127.0.0.1.nip.io:1234/admin"));
given().header("X-Forwarded-Proto", "https").when().get("http://localhost:8080").then().header(HttpHeaders.LOCATION, containsString("http://mykeycloakadmin.127.0.0.1.nip.io:1234/admin"));
}
private void assertForwardedHeader() {
given()
.header("Forwarded", "for=12.34.56.78;host=test:1234;proto=https, for=23.45.67.89")
.when().get("http://mykeycloak.org:8080")
.then().header(HttpHeaders.LOCATION, containsString("https://test:1234/admin"));
}
private void assertForwardedHeaderIsIgnored() {
given().header("Forwarded", "for=12.34.56.78;host=test:1234;proto=https, for=23.45.67.89").when().get("http://localhost:8080").then().header(HttpHeaders.LOCATION, containsString("http://localhost:8080"));
}
private void assertXForwardedHeaders() {
given().header("X-Forwarded-Host", "test").when().get("http://mykeycloak.org:8080").then().header(HttpHeaders.LOCATION, containsString("http://test:8080/admin"));
given().header("X-Forwarded-Host", "test").when().get("http://localhost:8080").then().header(HttpHeaders.LOCATION, containsString("http://test:8080/admin"));
given().header("X-Forwarded-Host", "test").when().get("https://localhost:8443").then().header(HttpHeaders.LOCATION, containsString("https://test:8443/admin"));
//given().header("X-Forwarded-Host", "mykeycloak.org").when().get("https://localhost:8443/admin/master/console").then().body(containsString("<script src=\"/js/keycloak.js?version="));
given().header("X-Forwarded-Proto", "https").when().get("http://localhost:8080").then().header(HttpHeaders.LOCATION, containsString("https://localhost/admin"));
given().header("X-Forwarded-Proto", "https").header("X-Forwarded-Port", "8443").when().get("http://localhost:8080").then().header(HttpHeaders.LOCATION, containsString("https://localhost:8443/admin"));
}
private void assertXForwardedHeadersAreIgnored() {
given().header("X-Forwarded-Host", "test").when().get("http://mykeycloak.org:8080").then().header(HttpHeaders.LOCATION, containsString("http://mykeycloak.org:8080/admin"));
given().header("X-Forwarded-Host", "test").when().get("http://localhost:8080").then().header(HttpHeaders.LOCATION, containsString("http://localhost:8080/admin"));
given().header("X-Forwarded-Host", "test").when().get("https://localhost:8443").then().header(HttpHeaders.LOCATION, containsString("https://localhost:8443/admin"));
given().header("X-Forwarded-Proto", "https").when().get("http://localhost:8080").then().header(HttpHeaders.LOCATION, containsString("http://localhost:8080/admin"));
given().header("X-Forwarded-Proto", "https").header("X-Forwarded-Port", "8443").when().get("http://localhost:8080").then().header(HttpHeaders.LOCATION, containsString("http://localhost:8080/admin"));
}
private OIDCConfigurationRepresentation getServerMetadata(String baseUrl) {
return when().get(baseUrl + "/realms/master/.well-known/openid-configuration").as(OIDCConfigurationRepresentation.class);
}
private void assertFrontEndUrl(String requestBaseUrl, String expectedBaseUrl) {
Assert.assertEquals(expectedBaseUrl + "realms/master/protocol/openid-connect/auth", getServerMetadata(requestBaseUrl)
.getAuthorizationEndpoint());
}
}

View file

@ -70,32 +70,6 @@ public class ProxyHostnameV2DistTest {
assertXForwardedHeaders();
}
@Test
@Launch({ "start-dev", "--hostname-strict=false", "--proxy-headers=xforwarded", "--proxy=reencrypt" })
public void testProxyHeadersTakePrecedenceOverProxyReencryptOption() {
assertForwardedHeaderIsIgnored();
assertXForwardedHeaders();
}
@Test
@Launch({ "start-dev", "--hostname-strict=false", "--proxy-headers=xforwarded", "--proxy=none" })
public void testProxyHeadersTakePrecedenceOverProxyNoneOption() {
assertForwardedHeaderIsIgnored();
assertXForwardedHeaders();
}
@Test
@Launch({ "start-dev", "--hostname=mykeycloak.org", "--proxy-headers=forwarded", "--proxy=none" })
public void testExplicitlySetHostnameTakesPrecedenceOverProxyHeaders() {
assertForwardedHeader("https://mykeycloak.org:1234/admin");
}
@Test
@Launch({ "start-dev", "--hostname=http://mykeycloak.org:8080", "--proxy-headers=forwarded", "--proxy=none" })
public void testExplicitlySetHostnameUrlTakesPrecedenceOverProxyHeaders() {
assertForwardedHeader("http://mykeycloak.org:8080/admin");
}
private void assertForwardedHeader() {
assertForwardedHeader("https://test:1234/admin");
}

View file

@ -237,10 +237,6 @@ Metrics:
Proxy:
--proxy <mode> DEPRECATED. The proxy address forwarding mode if the server is behind a
reverse proxy. Possible values are: none, edge, reencrypt, passthrough.
Default: none. Use the following options instead: http-enabled,
proxy-headers.
--proxy-headers <headers>
The proxy headers that should be accepted by the server. Misconfiguration
might leave the server exposed to security vulnerabilities. Takes precedence

View file

@ -158,43 +158,6 @@ Hostname v2:
the Host header. If enabled, the 'hostname' option needs to be specified.
Default: true. Available only when hostname:v2 feature is enabled.
Hostname v1 (Deprecated):
--hostname <hostname>
DEPRECATED. Hostname for the Keycloak server. Available only when hostname:v1
feature is enabled.
--hostname-admin <hostname>
DEPRECATED. The hostname for accessing the administration console. Use this
option if you are exposing the administration console using a hostname other
than the value set to the 'hostname' option. Available only when hostname:v1
feature is enabled.
--hostname-admin-url <url>
DEPRECATED. Set the base URL for accessing the administration console,
including scheme, host, port and path Available only when hostname:v1
feature is enabled.
--hostname-debug <true|false>
DEPRECATED. Toggle the hostname debug page that is accessible at
/realms/master/hostname-debug Default: false. Available only when hostname:
v1 feature is enabled.
--hostname-path <path>
DEPRECATED. This should be set if proxy uses a different context-path for
Keycloak. Available only when hostname:v1 feature is enabled.
--hostname-port <port>
DEPRECATED. The port used by the proxy when exposing the hostname. Set this
option if the proxy uses a port other than the default HTTP and HTTPS ports.
Default: -1. Available only when hostname:v1 feature is enabled.
--hostname-strict <true|false>
DEPRECATED. Disables dynamically resolving the hostname from request headers.
Should always be set to true in production, unless proxy verifies the Host
header. Default: true. Available only when hostname:v1 feature is enabled.
--hostname-strict-backchannel <true|false>
DEPRECATED. By default backchannel URLs are dynamically resolved from request
headers to allow internal and external applications. If all applications use
the public URL this option should be enabled. Default: false. Available only
when hostname:v1 feature is enabled.
--hostname-url <url> DEPRECATED. Set the base URL for frontend URLs, including scheme, host, port
and path. Available only when hostname:v1 feature is enabled.
HTTP(S):
--http-enabled <true|false>
@ -309,10 +272,6 @@ Metrics:
Proxy:
--proxy <mode> DEPRECATED. The proxy address forwarding mode if the server is behind a
reverse proxy. Possible values are: none, edge, reencrypt, passthrough.
Default: none. Use the following options instead: http-enabled,
proxy-headers.
--proxy-headers <headers>
The proxy headers that should be accepted by the server. Misconfiguration
might leave the server exposed to security vulnerabilities. Takes precedence

View file

@ -238,10 +238,6 @@ Metrics:
Proxy:
--proxy <mode> DEPRECATED. The proxy address forwarding mode if the server is behind a
reverse proxy. Possible values are: none, edge, reencrypt, passthrough.
Default: none. Use the following options instead: http-enabled,
proxy-headers.
--proxy-headers <headers>
The proxy headers that should be accepted by the server. Misconfiguration
might leave the server exposed to security vulnerabilities. Takes precedence

View file

@ -159,43 +159,6 @@ Hostname v2:
the Host header. If enabled, the 'hostname' option needs to be specified.
Default: true. Available only when hostname:v2 feature is enabled.
Hostname v1 (Deprecated):
--hostname <hostname>
DEPRECATED. Hostname for the Keycloak server. Available only when hostname:v1
feature is enabled.
--hostname-admin <hostname>
DEPRECATED. The hostname for accessing the administration console. Use this
option if you are exposing the administration console using a hostname other
than the value set to the 'hostname' option. Available only when hostname:v1
feature is enabled.
--hostname-admin-url <url>
DEPRECATED. Set the base URL for accessing the administration console,
including scheme, host, port and path Available only when hostname:v1
feature is enabled.
--hostname-debug <true|false>
DEPRECATED. Toggle the hostname debug page that is accessible at
/realms/master/hostname-debug Default: false. Available only when hostname:
v1 feature is enabled.
--hostname-path <path>
DEPRECATED. This should be set if proxy uses a different context-path for
Keycloak. Available only when hostname:v1 feature is enabled.
--hostname-port <port>
DEPRECATED. The port used by the proxy when exposing the hostname. Set this
option if the proxy uses a port other than the default HTTP and HTTPS ports.
Default: -1. Available only when hostname:v1 feature is enabled.
--hostname-strict <true|false>
DEPRECATED. Disables dynamically resolving the hostname from request headers.
Should always be set to true in production, unless proxy verifies the Host
header. Default: true. Available only when hostname:v1 feature is enabled.
--hostname-strict-backchannel <true|false>
DEPRECATED. By default backchannel URLs are dynamically resolved from request
headers to allow internal and external applications. If all applications use
the public URL this option should be enabled. Default: false. Available only
when hostname:v1 feature is enabled.
--hostname-url <url> DEPRECATED. Set the base URL for frontend URLs, including scheme, host, port
and path. Available only when hostname:v1 feature is enabled.
HTTP(S):
--http-enabled <true|false>
@ -310,10 +273,6 @@ Metrics:
Proxy:
--proxy <mode> DEPRECATED. The proxy address forwarding mode if the server is behind a
reverse proxy. Possible values are: none, edge, reencrypt, passthrough.
Default: none. Use the following options instead: http-enabled,
proxy-headers.
--proxy-headers <headers>
The proxy headers that should be accepted by the server. Misconfiguration
might leave the server exposed to security vulnerabilities. Takes precedence

View file

@ -190,10 +190,6 @@ Management:
Proxy:
--proxy <mode> DEPRECATED. The proxy address forwarding mode if the server is behind a
reverse proxy. Possible values are: none, edge, reencrypt, passthrough.
Default: none. Use the following options instead: http-enabled,
proxy-headers.
--proxy-headers <headers>
The proxy headers that should be accepted by the server. Misconfiguration
might leave the server exposed to security vulnerabilities. Takes precedence

View file

@ -144,43 +144,6 @@ Hostname v2:
the Host header. If enabled, the 'hostname' option needs to be specified.
Default: true. Available only when hostname:v2 feature is enabled.
Hostname v1 (Deprecated):
--hostname <hostname>
DEPRECATED. Hostname for the Keycloak server. Available only when hostname:v1
feature is enabled.
--hostname-admin <hostname>
DEPRECATED. The hostname for accessing the administration console. Use this
option if you are exposing the administration console using a hostname other
than the value set to the 'hostname' option. Available only when hostname:v1
feature is enabled.
--hostname-admin-url <url>
DEPRECATED. Set the base URL for accessing the administration console,
including scheme, host, port and path Available only when hostname:v1
feature is enabled.
--hostname-debug <true|false>
DEPRECATED. Toggle the hostname debug page that is accessible at
/realms/master/hostname-debug Default: false. Available only when hostname:
v1 feature is enabled.
--hostname-path <path>
DEPRECATED. This should be set if proxy uses a different context-path for
Keycloak. Available only when hostname:v1 feature is enabled.
--hostname-port <port>
DEPRECATED. The port used by the proxy when exposing the hostname. Set this
option if the proxy uses a port other than the default HTTP and HTTPS ports.
Default: -1. Available only when hostname:v1 feature is enabled.
--hostname-strict <true|false>
DEPRECATED. Disables dynamically resolving the hostname from request headers.
Should always be set to true in production, unless proxy verifies the Host
header. Default: true. Available only when hostname:v1 feature is enabled.
--hostname-strict-backchannel <true|false>
DEPRECATED. By default backchannel URLs are dynamically resolved from request
headers to allow internal and external applications. If all applications use
the public URL this option should be enabled. Default: false. Available only
when hostname:v1 feature is enabled.
--hostname-url <url> DEPRECATED. Set the base URL for frontend URLs, including scheme, host, port
and path. Available only when hostname:v1 feature is enabled.
HTTP(S):
--http-enabled <true|false>
@ -262,10 +225,6 @@ Management:
Proxy:
--proxy <mode> DEPRECATED. The proxy address forwarding mode if the server is behind a
reverse proxy. Possible values are: none, edge, reencrypt, passthrough.
Default: none. Use the following options instead: http-enabled,
proxy-headers.
--proxy-headers <headers>
The proxy headers that should be accepted by the server. Misconfiguration
might leave the server exposed to security vulnerabilities. Takes precedence