Skip to content

Commit 2300b3f

Browse files
authored
Handle canonical hostname checks for localhost on Windows (#42799)
Closes: #42794 Signed-off-by: Peter Zaoral <[email protected]>
1 parent 4dc3983 commit 2300b3f

File tree

3 files changed

+44
-4
lines changed

3 files changed

+44
-4
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Release notes should contain only headline-worthy new features,
2+
// assuming that people who migrate will read the upgrading guide anyway.
3+
//
4+
== Breaking Fix for Windows in Loopback Hostname Verification
5+
6+
This release introduces a breaking change for Windows users: setups that previously relied on custom machine names or non-standard hostnames for loopback (e.g., `127.0.0.1` resolving to a custom name) may require updates to their trusted domain configuration. Only `localhost` and `*.localhost` are now recognized for loopback verification.
7+
8+
Keycloak now consistently normalizes loopback addresses to `localhost` for domain verification across all platforms. This change ensures predictable behavior for trusted domain checks, regardless of the underlying OS.
9+

services/src/main/java/org/keycloak/services/clientregistration/policy/impl/TrustedHostClientRegistrationPolicy.java

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,14 +175,22 @@ protected String verifyHostInTrustedDomains(String hostAddress, List<String> tru
175175

176176
logger.debugf("Trying verify request from address '%s' of host '%s' by domains", hostAddress, hostname);
177177

178+
// On Windows, reverse lookup for loopback may return the IP (e.g., 127.0.0.1) instead of 'localhost'. Normalize to 'localhost' for consistent domain checks.
179+
if (hostname.equals(address.getHostAddress()) && address.isLoopbackAddress()) {
180+
hostname = "localhost";
181+
}
182+
178183
if (hostname.equals(address.getHostAddress())) {
179184
logger.debugf("The hostAddress '%s' was not resolved to a hostname", hostAddress);
180185
return null;
181186
}
182187

183-
if (Arrays.stream(InetAddress.getAllByName(hostname)).filter(a -> address.equals(a)).findAny().isEmpty()) {
184-
logger.debugf("The hostAddress '%s' is not among the direct lookups returned resolving '%s'", hostAddress, hostname);
185-
return null;
188+
// For loopback, skip strict forward-confirm check after normalizing to 'localhost' to avoid platform differences.
189+
if (!address.isLoopbackAddress()) {
190+
if (Arrays.stream(InetAddress.getAllByName(hostname)).filter(a -> address.equals(a)).findAny().isEmpty()) {
191+
logger.debugf("The hostAddress '%s' is not among the direct lookups returned resolving '%s'", hostAddress, hostname);
192+
return null;
193+
}
186194
}
187195

188196
for (String confDomain : trustedDomains) {
@@ -192,7 +200,17 @@ protected String verifyHostInTrustedDomains(String hostAddress, List<String> tru
192200
}
193201
}
194202
} catch (UnknownHostException uhe) {
195-
logger.debugf(uhe, "Request of address '%s' came from unknown host. Skip verification by domains", hostAddress);
203+
logger.debugf(uhe, "Request of address '%s' came from unknown host. Skip verification by domains unless it's within localhost domain", hostAddress);
204+
205+
String lower = hostAddress == null ? null : hostAddress.toLowerCase();
206+
if (lower != null && ("localhost".equals(lower) || lower.endsWith(".localhost"))) {
207+
for (String confDomain : trustedDomains) {
208+
if (checkTrustedDomain(lower, confDomain)) {
209+
logger.debugf("Treating host '%s' as loopback due to localhost domain and returning success by trusted domain '%s'", lower, confDomain);
210+
return lower;
211+
}
212+
}
213+
}
196214
}
197215

198216
return null;

services/src/test/java/org/keycloak/services/clientregistration/policy/impl/TrustedHostClientRegistrationPolicyTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,19 @@ public void testGoogleCrawlBot() {
102102
policy.getTrustedHosts(), policy.getTrustedDomains()));
103103
}
104104

105+
@Test
106+
public void testLocalhostDomainFallback() {
107+
TrustedHostClientRegistrationPolicyFactory factory = new TrustedHostClientRegistrationPolicyFactory();
108+
ComponentModel model = createComponentModel("*.localhost");
109+
TrustedHostClientRegistrationPolicy policy = (TrustedHostClientRegistrationPolicy) factory.create(session, model);
110+
111+
// Simulate a hostname that would fail DNS resolution on some platforms
112+
// but matches the trusted domain fallback logic
113+
assertTrue(policy.verifyHost("other.localhost"));
114+
assertTrue(policy.verifyHost("localhost"));
115+
assertFalse(policy.verifyHost("otherlocalhost"));
116+
}
117+
105118
private ComponentModel createComponentModel(String... hosts) {
106119
ComponentModel model = new ComponentModel();
107120
model.put(TrustedHostClientRegistrationPolicyFactory.HOST_SENDING_REGISTRATION_REQUEST_MUST_MATCH, "true");

0 commit comments

Comments
 (0)