Port of the custom extension 'Hostname Debug Tool' to Keycloak.

Co-authored-by: stianst <stian@redhat.com>

Closes #15910
This commit is contained in:
Andre Nascimento 2023-03-03 18:26:05 +01:00 committed by Václav Muzikář
parent 399bd42124
commit a7153af7b0
14 changed files with 412 additions and 4 deletions

View file

@ -122,4 +122,18 @@ In this example, the server is accessible using a port other than the default po
.Keycloak configuration:
<@kc.start parameters="--hostname-url=https://mykeycloak:8989"/>
== Troubleshooting
To troubleshoot the hostname configuration, you can use a dedicated debug tool which can be enabled as:
.Keycloak configuration:
<@kc.start parameters="--hostname=mykeycloak --hostname-debug=true"/>
Then after Keycloak 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

@ -50,4 +50,11 @@ public class HostnameOptions {
.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 HOSTNAME_DEBUG = new OptionBuilder<>("hostname-debug", Boolean.class)
.category(OptionCategory.HOSTNAME)
.description("Toggle the hostname debug page that is accessible at /realms/master/hostname-debug")
.defaultValue(Boolean.FALSE)
.build();
}

View file

@ -42,6 +42,9 @@ final class HostnamePropertyMappers {
fromOption(HostnameOptions.HOSTNAME_PORT)
.to("kc.spi-hostname-default-hostname-port")
.paramLabel("port")
.build(),
fromOption(HostnameOptions.HOSTNAME_DEBUG)
.to("kc.spi-hostname-default-hostname-debug")
.build()
};
}

View file

@ -22,10 +22,14 @@ import java.util.stream.Collectors;
import javax.ws.rs.ApplicationPath;
import org.keycloak.config.HostnameOptions;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.keycloak.quarkus.runtime.integration.QuarkusKeycloakSessionFactory;
import org.keycloak.quarkus.runtime.services.resources.DebugHostnameSettingsResource;
import org.keycloak.services.resources.KeycloakApplication;
import org.keycloak.quarkus.runtime.services.resources.QuarkusWelcomeResource;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.resources.WelcomeResource;
@ApplicationPath("/")
@ -55,6 +59,10 @@ public class QuarkusKeycloakApplication extends KeycloakApplication {
singletons.add(new QuarkusWelcomeResource());
if (Configuration.getOptionalBooleanValue("--" + HostnameOptions.HOSTNAME_DEBUG.getKey()).orElse(Boolean.FALSE)) {
singletons.add(new DebugHostnameSettingsResource());
}
return singletons;
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright 2016 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.services.resources;
public class ConstantsDebugHostname {
public static final String[] RELEVANT_HEADERS = new String[] {
"Host",
"Forwarded",
"X-Forwarded-Host",
"X-Forwarded-Proto",
"X-Forwarded-Port",
"X-Forwarded-For"
};
public static final String[] RELEVANT_OPTIONS = {
"hostname",
"hostname-url",
"hostname-admin",
"hostname-admin-url",
"hostname-strict",
"hostname-strict-backchannel",
"hostname-strict-https",
"hostname-path",
"hostname-port",
"proxy",
"http-enabled",
"http-relative-path",
"http-port",
"https-port"
};
}

View file

@ -0,0 +1,146 @@
/*
* Copyright 2016 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.services.resources;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.keycloak.services.Urls;
import org.keycloak.services.resources.Cors;
import org.keycloak.theme.FreeMarkerException;
import org.keycloak.theme.Theme;
import org.keycloak.theme.freemarker.FreeMarkerProvider;
import org.keycloak.urls.UrlType;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.net.URI;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
@Path("/realms")
public class DebugHostnameSettingsResource {
public static final String DEFAULT_PATH_SUFFIX = "hostname-debug";
public static final String PATH_FOR_TEST_CORS_IN_HEADERS = "test";
@Context
private KeycloakSession keycloakSession;
private final Map<String, String> allConfigPropertiesMap;
public DebugHostnameSettingsResource() {
this.allConfigPropertiesMap = new LinkedHashMap<>();
for (String key : ConstantsDebugHostname.RELEVANT_OPTIONS) {
addOption(key);
}
}
@GET
@Path("/{realmName}/" + DEFAULT_PATH_SUFFIX)
@Produces(MediaType.TEXT_HTML)
public String debug(final @PathParam("realmName") String realmName) throws IOException, FreeMarkerException {
FreeMarkerProvider freeMarkerProvider = keycloakSession.getProvider(FreeMarkerProvider.class);
RealmModel realmModel = keycloakSession.realms().getRealmByName(realmName);
URI frontendUri = keycloakSession.getContext().getUri(UrlType.FRONTEND).getBaseUri();
URI backendUri = keycloakSession.getContext().getUri(UrlType.BACKEND).getBaseUri();
URI adminUri = keycloakSession.getContext().getUri(UrlType.ADMIN).getBaseUri();
String frontendTestUrl = getTest(realmModel, frontendUri);
String backendTestUrl = getTest(realmModel, backendUri);
String adminTestUrl = getTest(realmModel, adminUri);
Map<String, Object> attributes = new HashMap<>();
attributes.put("frontendUrl", frontendUri.toString());
attributes.put("backendUrl", backendUri.toString());
attributes.put("adminUrl", adminUri.toString());
attributes.put("realm", realmModel.getName());
attributes.put("realmUrl", realmModel.getAttribute("frontendUrl"));
attributes.put("frontendTestUrl", frontendTestUrl);
attributes.put("backendTestUrl", backendTestUrl);
attributes.put("adminTestUrl", adminTestUrl);
attributes.put("serverMode", Environment.isDevMode() ? "dev [start-dev]" : "production [start]");
attributes.put("config", this.allConfigPropertiesMap);
attributes.put("headers", getHeaders());
return freeMarkerProvider.processTemplate(
attributes,
"debug-hostname-settings.ftl",
keycloakSession.theme().getTheme("base", Theme.Type.LOGIN)
);
}
@GET
@Path("/{realmName}/" + DEFAULT_PATH_SUFFIX + "/" + PATH_FOR_TEST_CORS_IN_HEADERS)
@Produces(MediaType.TEXT_PLAIN)
public Response test(final @PathParam("realmName") String realmName) {
Response.ResponseBuilder builder = Response.ok(PATH_FOR_TEST_CORS_IN_HEADERS + "-OK");
String origin = keycloakSession.getContext().getRequestHeaders().getHeaderString(Cors.ORIGIN_HEADER);
builder.header(Cors.ACCESS_CONTROL_ALLOW_ORIGIN, origin);
builder.header(Cors.ACCESS_CONTROL_ALLOW_METHODS, "GET");
return builder.build();
}
private void addOption(String key) {
String rawValue = Configuration.getRawValue("kc." + key);
if (rawValue != null && !rawValue.isEmpty()) {
this.allConfigPropertiesMap.put(key, rawValue);
}
}
private Map<String, String> getHeaders() {
Map<String, String> headers = new TreeMap<>();
HttpHeaders requestHeaders = keycloakSession.getContext().getRequestHeaders();
for (String h : ConstantsDebugHostname.RELEVANT_HEADERS) {
addProxyHeader(h, headers, requestHeaders);
}
return headers;
}
private void addProxyHeader(String header, Map<String, String> proxyHeaders, HttpHeaders requestHeaders) {
String value = requestHeaders.getHeaderString(header);
if (value != null && !value.isEmpty()) {
proxyHeaders.put(header, value);
}
}
private String getTest(RealmModel realmModel, URI baseUri) {
return Urls.realmBase(baseUri)
.path("/{realmName}/{debugHostnameSettingsPath}/{pathForTestCORSInHeaders}")
.build(realmModel.getName(), DEFAULT_PATH_SUFFIX, PATH_FOR_TEST_CORS_IN_HEADERS)
.toString();
}
}

View file

@ -0,0 +1,129 @@
<html>
<head>
<style>
body {
font-family: Sans;
}
table, th, td {
border: 1px solid #bbb;
border-collapse: collapse;
}
th {
text-align: left;
}
th, td {
padding: 8px 15px;
font-size: 14px;
}
tr:nth-child(even) {
background-color: #f3f3f3;
}
</style>
</head>
<body>
<table>
<tr>
<th>URL</th>
<th>Value</th>
</tr>
<tr>
<td>Request</td>
<td><span id="requestUrl"></span></td>
</tr>
<tr>
<td>Frontend</td>
<td>${frontendUrl} [<span id="frontendStatus"></span>]</td>
</tr>
<tr>
<td>Backend</td>
<td>${backendUrl} [<span id="backendStatus"></span>]</td>
</tr>
<tr>
<td>Admin</td>
<td>${adminUrl} [<span id="adminStatus"></span>]</td>
</tr>
<tr>
<th>Runtime</th>
<th>Value</th>
</tr>
<tr>
<td>Server mode</td>
<td>${serverMode}</td>
</tr>
<tr>
<td>Realm</td>
<td>${realm}</td>
</tr>
<#if realmUrl??>
<tr>
<td>Realm URL</td>
<td>${realmUrl}</td>
</tr>
</#if>
<tr>
<th>Configuration property</th>
<th>Value</th>
</tr>
<#list config as key, value>
<tr>
<td>${key}</td>
<td>${value}</td>
</tr>
</#list>
<#if headers?has_content>
<tr>
<th>Header</th>
<th>Value</th>
</tr>
<#list headers as key, value>
<tr>
<td>${key}</td>
<td>${value}</td>
</tr>
</#list>
</#if>
</table>
<script>
function testUrl(url, responseId) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
clearTimeout(timeout);
if (xhr.status == 200) {
document.getElementById(responseId).textContent='OK';
} else {
document.getElementById(responseId).textContent='FAILED';
}
}
}
var timeout = setTimeout(function() {
xhr.abort();
document.getElementById(responseId).textContent='TIMEOUT';
}, 5000);
xhr.open('GET', url, true);
xhr.send();
}
document.getElementById("requestUrl").textContent=document.location.href
testUrl('${frontendTestUrl}', 'frontendStatus');
testUrl('${backendTestUrl}', 'backendStatus');
testUrl('${adminTestUrl}', 'adminStatus');
</script>
</body>
</html>

View file

@ -17,17 +17,17 @@
package org.keycloak.it.cli.dist;
import static io.restassured.RestAssured.when;
import io.quarkus.test.junit.main.Launch;
import io.restassured.RestAssured;
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.protocol.oidc.representations.OIDCConfigurationRepresentation;
import org.keycloak.quarkus.runtime.services.resources.DebugHostnameSettingsResource;
import io.quarkus.test.junit.main.Launch;
import io.restassured.RestAssured;
import static io.restassured.RestAssured.when;
@DistributionTest(keepAlive = true, enableTls = true, defaultOptions = { "--http-enabled=true" })
@RawDistOnly(reason = "Containers are immutable")
@ -115,6 +115,43 @@ public class HostnameDistTest {
Assert.assertTrue(when().get("https://localhost:8443").asString().contains("https://localhost:8443/admin/"));
}
@Test
@Launch({ "start", "--hostname=mykeycloak.org", "--hostname-debug=true" })
public void testDebugHostnameSettingsEnabled() {
Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).getStatusCode() == 200);
Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).asString().contains("Configuration property"));
Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).asString().contains("Server mode"));
Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).asString().contains("production [start]"));
Assert.assertTrue(
when().get("http://mykeycloak.org:8080/realms/master/" +
DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX +
"/" + DebugHostnameSettingsResource.PATH_FOR_TEST_CORS_IN_HEADERS)
.getStatusCode() == 200
);
Assert.assertTrue(
when().get("http://localhost:8080/realms/master/" +
DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX +
"/" + DebugHostnameSettingsResource.PATH_FOR_TEST_CORS_IN_HEADERS)
.asString()
.contains(DebugHostnameSettingsResource.PATH_FOR_TEST_CORS_IN_HEADERS + "-OK")
);
}
@Test
@Launch({ "start", "--hostname=mykeycloak.org", "--hostname-debug=false" })
public void testDebugHostnameSettingsDisabledBySetting() {
Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).getStatusCode() == 404);
Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).asString().contains("404"));
}
@Test
@Launch({ "start", "--hostname=mykeycloak.org"})
public void testDebugHostnameSettingsDisabledByDefault() {
Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).getStatusCode() == 404);
Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).asString().contains("404"));
}
@Test
@Launch({ "start", "--hostname=mykeycloak.org", "--hostname-admin=mykeycloakadmin.org" })
public void testHostnameAdminSet() {

View file

@ -95,6 +95,9 @@ Hostname:
--hostname-admin-url <url>
Set the base URL for accessing the administration console, including scheme,
host, port and path
--hostname-debug <true|false>
Toggle the hostname debug page that is accessible at
/realms/master/hostname-debug Default: false.
--hostname-path <path>
This should be set if proxy uses a different context-path for Keycloak.
--hostname-port <port>

View file

@ -158,6 +158,9 @@ Hostname:
--hostname-admin-url <url>
Set the base URL for accessing the administration console, including scheme,
host, port and path
--hostname-debug <true|false>
Toggle the hostname debug page that is accessible at
/realms/master/hostname-debug Default: false.
--hostname-path <path>
This should be set if proxy uses a different context-path for Keycloak.
--hostname-port <port>

View file

@ -101,6 +101,9 @@ Hostname:
--hostname-admin-url <url>
Set the base URL for accessing the administration console, including scheme,
host, port and path
--hostname-debug <true|false>
Toggle the hostname debug page that is accessible at
/realms/master/hostname-debug Default: false.
--hostname-path <path>
This should be set if proxy uses a different context-path for Keycloak.
--hostname-port <port>

View file

@ -164,6 +164,9 @@ Hostname:
--hostname-admin-url <url>
Set the base URL for accessing the administration console, including scheme,
host, port and path
--hostname-debug <true|false>
Toggle the hostname debug page that is accessible at
/realms/master/hostname-debug Default: false.
--hostname-path <path>
This should be set if proxy uses a different context-path for Keycloak.
--hostname-port <port>

View file

@ -61,6 +61,9 @@ Hostname:
--hostname-admin-url <url>
Set the base URL for accessing the administration console, including scheme,
host, port and path
--hostname-debug <true|false>
Toggle the hostname debug page that is accessible at
/realms/master/hostname-debug Default: false.
--hostname-path <path>
This should be set if proxy uses a different context-path for Keycloak.
--hostname-port <port>

View file

@ -80,6 +80,9 @@ Hostname:
--hostname-admin-url <url>
Set the base URL for accessing the administration console, including scheme,
host, port and path
--hostname-debug <true|false>
Toggle the hostname debug page that is accessible at
/realms/master/hostname-debug Default: false.
--hostname-path <path>
This should be set if proxy uses a different context-path for Keycloak.
--hostname-port <port>