From d716b8680447549302e3844968312deaa9a02aa5 Mon Sep 17 00:00:00 2001
From: Bodo Graumann <mail@bodograumann.de>
Date: Wed, 25 Sep 2024 16:57:44 +0200
Subject: [PATCH 1/3] Avoid UriComponentsBuilder.fromUri

Closes gh-15852
---
 .../JwtDecoderProviderConfigurationUtils.java  | 18 ++++++++++--------
 ...veJwtDecoderProviderConfigurationUtils.java | 15 ++++++++-------
 2 files changed, 18 insertions(+), 15 deletions(-)

diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtils.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtils.java
index e69286c2d67..cfe0d0e538b 100644
--- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtils.java
+++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtils.java
@@ -45,6 +45,7 @@
 import org.springframework.web.client.HttpClientErrorException;
 import org.springframework.web.client.RestOperations;
 import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponents;
 import org.springframework.web.util.UriComponentsBuilder;
 
 /**
@@ -82,11 +83,12 @@ private JwtDecoderProviderConfigurationUtils() {
 	}
 
 	static Map<String, Object> getConfigurationForOidcIssuerLocation(String oidcIssuerLocation) {
-		return getConfiguration(oidcIssuerLocation, rest, oidc(URI.create(oidcIssuerLocation)));
+		UriComponents uri = UriComponentsBuilder.fromUriString(oidcIssuerLocation).build();
+		return getConfiguration(oidcIssuerLocation, rest, oidc(uri));
 	}
 
 	static Map<String, Object> getConfigurationForIssuerLocation(String issuer, RestOperations rest) {
-		URI uri = URI.create(issuer);
+		UriComponents uri = UriComponentsBuilder.fromUriString(issuer).build();
 		return getConfiguration(issuer, rest, oidc(uri), oidcRfc8414(uri), oauth(uri));
 	}
 
@@ -183,25 +185,25 @@ private static Map<String, Object> getConfiguration(String issuer, RestOperation
 		throw new IllegalArgumentException(errorMessage);
 	}
 
-	private static URI oidc(URI issuer) {
+	private static URI oidc(UriComponents issuer) {
 		// @formatter:off
-		return UriComponentsBuilder.fromUri(issuer)
+		return UriComponentsBuilder.newInstance().uriComponents(issuer)
 				.replacePath(issuer.getPath() + OIDC_METADATA_PATH)
 				.build(Collections.emptyMap());
 		// @formatter:on
 	}
 
-	private static URI oidcRfc8414(URI issuer) {
+	private static URI oidcRfc8414(UriComponents issuer) {
 		// @formatter:off
-		return UriComponentsBuilder.fromUri(issuer)
+		return UriComponentsBuilder.newInstance().uriComponents(issuer)
 				.replacePath(OIDC_METADATA_PATH + issuer.getPath())
 				.build(Collections.emptyMap());
 		// @formatter:on
 	}
 
-	private static URI oauth(URI issuer) {
+	private static URI oauth(UriComponents issuer) {
 		// @formatter:off
-		return UriComponentsBuilder.fromUri(issuer)
+		return UriComponentsBuilder.newInstance().uriComponents(issuer)
 				.replacePath(OAUTH_METADATA_PATH + issuer.getPath())
 				.build(Collections.emptyMap());
 		// @formatter:on
diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtils.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtils.java
index 7b0b98e6eed..bda44358a34 100644
--- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtils.java
+++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtils.java
@@ -41,6 +41,7 @@
 import org.springframework.util.Assert;
 import org.springframework.web.reactive.function.client.WebClient;
 import org.springframework.web.reactive.function.client.WebClientResponseException;
+import org.springframework.web.util.UriComponents;
 import org.springframework.web.util.UriComponentsBuilder;
 
 final class ReactiveJwtDecoderProviderConfigurationUtils {
@@ -93,29 +94,29 @@ else if (jwk.getKeyType() == KeyType.EC) {
 	}
 
 	static Mono<Map<String, Object>> getConfigurationForIssuerLocation(String issuer, WebClient web) {
-		URI uri = URI.create(issuer);
+		UriComponents uri = UriComponentsBuilder.fromUriString(issuer).build();
 		return getConfiguration(issuer, web, oidc(uri), oidcRfc8414(uri), oauth(uri));
 	}
 
-	private static URI oidc(URI issuer) {
+	private static URI oidc(UriComponents issuer) {
 		// @formatter:off
-		return UriComponentsBuilder.fromUri(issuer)
+		return UriComponentsBuilder.newInstance().uriComponents(issuer)
 				.replacePath(issuer.getPath() + OIDC_METADATA_PATH)
 				.build(Collections.emptyMap());
 		// @formatter:on
 	}
 
-	private static URI oidcRfc8414(URI issuer) {
+	private static URI oidcRfc8414(UriComponents issuer) {
 		// @formatter:off
-		return UriComponentsBuilder.fromUri(issuer)
+		return UriComponentsBuilder.newInstance().uriComponents(issuer)
 				.replacePath(OIDC_METADATA_PATH + issuer.getPath())
 				.build(Collections.emptyMap());
 		// @formatter:on
 	}
 
-	private static URI oauth(URI issuer) {
+	private static URI oauth(UriComponents issuer) {
 		// @formatter:off
-		return UriComponentsBuilder.fromUri(issuer)
+		return UriComponentsBuilder.newInstance().uriComponents(issuer)
 				.replacePath(OAUTH_METADATA_PATH + issuer.getPath())
 				.build(Collections.emptyMap());
 		// @formatter:on

From 571135d2ab06b24d30c91249ca8c7d0b0285a346 Mon Sep 17 00:00:00 2001
From: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
Date: Thu, 20 Feb 2025 14:53:58 -0700
Subject: [PATCH 2/3] JwtDecoders Supports Hostnames with Underscores

In the process of verifying gh-15852, another issue with URI was discovered.
This commit adds tests to the uri-computing methods and changes them to use
UriComponents instead of URI.

Issue gh-15852
---
 .../registration/ClientRegistrations.java     | 14 +++++--
 .../JwtDecoderProviderConfigurationUtils.java | 41 +++++++++----------
 ...eJwtDecoderProviderConfigurationUtils.java | 36 ++++++++--------
 ...ecoderProviderConfigurationUtilsTests.java | 13 ++++++
 ...ecoderProviderConfigurationUtilsTests.java | 13 ++++++
 5 files changed, 74 insertions(+), 43 deletions(-)

diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java
index 91517d640a5..d9ae09f3298 100644
--- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java
@@ -37,6 +37,7 @@
 import org.springframework.util.Assert;
 import org.springframework.web.client.HttpClientErrorException;
 import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponents;
 import org.springframework.web.util.UriComponentsBuilder;
 
 /**
@@ -211,13 +212,18 @@ private static Supplier<ClientRegistration.Builder> oidc(URI issuer) {
 		};
 	}
 
-	private static Supplier<ClientRegistration.Builder> oidcRfc8414(URI issuer) {
+	private static Supplier<ClientRegistration.Builder> oidcRfc8414(String issuer) {
+		URI uri = oidcRfc8414Uri(issuer);
+		return getRfc8414Builder(issuer, uri);
+	}
+
+	static URI oidcRfc8414Uri(String issuer) {
+		UriComponents uri = UriComponentsBuilder.fromUriString(issuer).build();
 		// @formatter:off
-		URI uri = UriComponentsBuilder.fromUri(issuer)
-				.replacePath(OIDC_METADATA_PATH + issuer.getPath())
+		return UriComponentsBuilder.newInstance().uriComponents(uri)
+				.replacePath(OIDC_METADATA_PATH + uri.getPath())
 				.build(Collections.emptyMap());
 		// @formatter:on
-		return getRfc8414Builder(issuer, uri);
 	}
 
 	private static Supplier<ClientRegistration.Builder> oauth(URI issuer) {
diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtils.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtils.java
index cfe0d0e538b..d901743e371 100644
--- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtils.java
+++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtils.java
@@ -16,8 +16,6 @@
 
 package org.springframework.security.oauth2.jwt;
 
-import java.net.URI;
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -83,13 +81,11 @@ private JwtDecoderProviderConfigurationUtils() {
 	}
 
 	static Map<String, Object> getConfigurationForOidcIssuerLocation(String oidcIssuerLocation) {
-		UriComponents uri = UriComponentsBuilder.fromUriString(oidcIssuerLocation).build();
-		return getConfiguration(oidcIssuerLocation, rest, oidc(uri));
+		return getConfiguration(oidcIssuerLocation, rest, oidc(oidcIssuerLocation));
 	}
 
 	static Map<String, Object> getConfigurationForIssuerLocation(String issuer, RestOperations rest) {
-		UriComponents uri = UriComponentsBuilder.fromUriString(issuer).build();
-		return getConfiguration(issuer, rest, oidc(uri), oidcRfc8414(uri), oauth(uri));
+		return getConfiguration(issuer, rest, oidc(issuer), oidcRfc8414(issuer), oauth(issuer));
 	}
 
 	static Map<String, Object> getConfigurationForIssuerLocation(String issuer) {
@@ -161,11 +157,11 @@ private static String getMetadataIssuer(Map<String, Object> configuration) {
 		return "(unavailable)";
 	}
 
-	private static Map<String, Object> getConfiguration(String issuer, RestOperations rest, URI... uris) {
+	private static Map<String, Object> getConfiguration(String issuer, RestOperations rest, UriComponents... uris) {
 		String errorMessage = "Unable to resolve the Configuration with the provided Issuer of " + "\"" + issuer + "\"";
-		for (URI uri : uris) {
+		for (UriComponents uri : uris) {
 			try {
-				RequestEntity<Void> request = RequestEntity.get(uri).build();
+				RequestEntity<Void> request = RequestEntity.get(uri.toUriString()).build();
 				ResponseEntity<Map<String, Object>> response = rest.exchange(request, STRING_OBJECT_MAP);
 				Map<String, Object> configuration = response.getBody();
 				Assert.isTrue(configuration.get("jwks_uri") != null, "The public JWK set URI must not be null");
@@ -185,27 +181,30 @@ private static Map<String, Object> getConfiguration(String issuer, RestOperation
 		throw new IllegalArgumentException(errorMessage);
 	}
 
-	private static URI oidc(UriComponents issuer) {
+	static UriComponents oidc(String issuer) {
+		UriComponents uri = UriComponentsBuilder.fromUriString(issuer).build();
 		// @formatter:off
-		return UriComponentsBuilder.newInstance().uriComponents(issuer)
-				.replacePath(issuer.getPath() + OIDC_METADATA_PATH)
-				.build(Collections.emptyMap());
+		return UriComponentsBuilder.newInstance().uriComponents(uri)
+				.replacePath(uri.getPath() + OIDC_METADATA_PATH)
+				.build();
 		// @formatter:on
 	}
 
-	private static URI oidcRfc8414(UriComponents issuer) {
+	static UriComponents oidcRfc8414(String issuer) {
+		UriComponents uri = UriComponentsBuilder.fromUriString(issuer).build();
 		// @formatter:off
-		return UriComponentsBuilder.newInstance().uriComponents(issuer)
-				.replacePath(OIDC_METADATA_PATH + issuer.getPath())
-				.build(Collections.emptyMap());
+		return UriComponentsBuilder.newInstance().uriComponents(uri)
+				.replacePath(OIDC_METADATA_PATH + uri.getPath())
+				.build();
 		// @formatter:on
 	}
 
-	private static URI oauth(UriComponents issuer) {
+	static UriComponents oauth(String issuer) {
+		UriComponents uri = UriComponentsBuilder.fromUriString(issuer).build();
 		// @formatter:off
-		return UriComponentsBuilder.newInstance().uriComponents(issuer)
-				.replacePath(OAUTH_METADATA_PATH + issuer.getPath())
-				.build(Collections.emptyMap());
+		return UriComponentsBuilder.newInstance().uriComponents(uri)
+				.replacePath(OAUTH_METADATA_PATH + uri.getPath())
+				.build();
 		// @formatter:on
 	}
 
diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtils.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtils.java
index bda44358a34..d9506d900df 100644
--- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtils.java
+++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtils.java
@@ -16,8 +16,6 @@
 
 package org.springframework.security.oauth2.jwt;
 
-import java.net.URI;
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
@@ -94,38 +92,40 @@ else if (jwk.getKeyType() == KeyType.EC) {
 	}
 
 	static Mono<Map<String, Object>> getConfigurationForIssuerLocation(String issuer, WebClient web) {
-		UriComponents uri = UriComponentsBuilder.fromUriString(issuer).build();
-		return getConfiguration(issuer, web, oidc(uri), oidcRfc8414(uri), oauth(uri));
+		return getConfiguration(issuer, web, oidc(issuer), oidcRfc8414(issuer), oauth(issuer));
 	}
 
-	private static URI oidc(UriComponents issuer) {
+	static UriComponents oidc(String issuer) {
+		UriComponents uri = UriComponentsBuilder.fromUriString(issuer).build();
 		// @formatter:off
-		return UriComponentsBuilder.newInstance().uriComponents(issuer)
-				.replacePath(issuer.getPath() + OIDC_METADATA_PATH)
-				.build(Collections.emptyMap());
+		return UriComponentsBuilder.newInstance().uriComponents(uri)
+				.replacePath(uri.getPath() + OIDC_METADATA_PATH)
+				.build();
 		// @formatter:on
 	}
 
-	private static URI oidcRfc8414(UriComponents issuer) {
+	static UriComponents oidcRfc8414(String issuer) {
+		UriComponents uri = UriComponentsBuilder.fromUriString(issuer).build();
 		// @formatter:off
-		return UriComponentsBuilder.newInstance().uriComponents(issuer)
-				.replacePath(OIDC_METADATA_PATH + issuer.getPath())
-				.build(Collections.emptyMap());
+		return UriComponentsBuilder.newInstance().uriComponents(uri)
+				.replacePath(OIDC_METADATA_PATH + uri.getPath())
+				.build();
 		// @formatter:on
 	}
 
-	private static URI oauth(UriComponents issuer) {
+	static UriComponents oauth(String issuer) {
+		UriComponents uri = UriComponentsBuilder.fromUriString(issuer).build();
 		// @formatter:off
-		return UriComponentsBuilder.newInstance().uriComponents(issuer)
-				.replacePath(OAUTH_METADATA_PATH + issuer.getPath())
-				.build(Collections.emptyMap());
+		return UriComponentsBuilder.newInstance().uriComponents(uri)
+				.replacePath(OAUTH_METADATA_PATH + uri.getPath())
+				.build();
 		// @formatter:on
 	}
 
-	private static Mono<Map<String, Object>> getConfiguration(String issuer, WebClient web, URI... uris) {
+	private static Mono<Map<String, Object>> getConfiguration(String issuer, WebClient web, UriComponents... uris) {
 		String errorMessage = "Unable to resolve the Configuration with the provided Issuer of " + "\"" + issuer + "\"";
 		return Flux.just(uris)
-			.concatMap((uri) -> web.get().uri(uri).retrieve().bodyToMono(STRING_OBJECT_MAP))
+			.concatMap((uri) -> web.get().uri(uri.toUriString()).retrieve().bodyToMono(STRING_OBJECT_MAP))
 			.flatMap((configuration) -> {
 				if (configuration.get("jwks_uri") == null) {
 					return Mono.error(() -> new IllegalArgumentException("The public JWK set URI must not be null"));
diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtilsTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtilsTests.java
index 1b88d6f3f35..ec87e7a1de1 100644
--- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtilsTests.java
+++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtilsTests.java
@@ -35,6 +35,7 @@
 import org.springframework.security.oauth2.jose.TestKeys;
 import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
 import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
+import org.springframework.web.util.UriComponents;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -90,4 +91,16 @@ public void getSignatureAlgorithmsWhenAlgorithmThenParses() throws Exception {
 		assertThat(algorithms).containsOnly(SignatureAlgorithm.RS256);
 	}
 
+	// gh-15852
+	@Test
+	public void oidcWhenHostContainsUnderscoreThenRetains() {
+		UriComponents oidc = JwtDecoderProviderConfigurationUtils.oidc("https://elated_sutherland:8080/path");
+		assertThat(oidc.getHost()).isEqualTo("elated_sutherland");
+		UriComponents oauth = JwtDecoderProviderConfigurationUtils.oauth("https://elated_sutherland:8080/path");
+		assertThat(oauth.getHost()).isEqualTo("elated_sutherland");
+		UriComponents oidcRfc8414 = JwtDecoderProviderConfigurationUtils
+			.oidcRfc8414("https://elated_sutherland:8080/path");
+		assertThat(oidcRfc8414.getHost()).isEqualTo("elated_sutherland");
+	}
+
 }
diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtilsTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtilsTests.java
index 30a0affd144..12ccd7c46fb 100644
--- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtilsTests.java
+++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtilsTests.java
@@ -37,6 +37,7 @@
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
 import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.util.UriComponents;
 import org.springframework.web.util.UriComponentsBuilder;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -227,6 +228,18 @@ public void issuerWhenOidcFallbackRequestedIssuerIsUnresponsiveThenThrowsIllegal
 		// @formatter:on
 	}
 
+	// gh-15852
+	@Test
+	public void oidcWhenHostContainsUnderscoreThenRetains() {
+		UriComponents oidc = ReactiveJwtDecoderProviderConfigurationUtils.oidc("https://elated_sutherland:8080/path");
+		assertThat(oidc.getHost()).isEqualTo("elated_sutherland");
+		UriComponents oauth = ReactiveJwtDecoderProviderConfigurationUtils.oauth("https://elated_sutherland:8080/path");
+		assertThat(oauth.getHost()).isEqualTo("elated_sutherland");
+		UriComponents oidcRfc8414 = ReactiveJwtDecoderProviderConfigurationUtils
+			.oidcRfc8414("https://elated_sutherland:8080/path");
+		assertThat(oidcRfc8414.getHost()).isEqualTo("elated_sutherland");
+	}
+
 	private void prepareConfigurationResponse() {
 		String body = String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer, this.issuer);
 		prepareConfigurationResponse(body);

From 49e5d3796e019d8ba03c68481e7c1a4327cad751 Mon Sep 17 00:00:00 2001
From: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
Date: Thu, 20 Feb 2025 15:08:37 -0700
Subject: [PATCH 3/3] ClientRegistrations supports hostnames with underscores

Issue gh-15852
---
 .../registration/ClientRegistrations.java     | 56 +++++++++++--------
 .../ClientRegistrationsTests.java             | 12 ++++
 2 files changed, 44 insertions(+), 24 deletions(-)

diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java
index d9ae09f3298..54efef5107d 100644
--- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java
@@ -17,7 +17,6 @@
 package org.springframework.security.oauth2.client.registration;
 
 import java.net.URI;
-import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -146,7 +145,7 @@ public static ClientRegistration.Builder fromOidcConfiguration(Map<String, Objec
 	 */
 	public static ClientRegistration.Builder fromOidcIssuerLocation(String issuer) {
 		Assert.hasText(issuer, "issuer cannot be empty");
-		return getBuilder(issuer, oidc(URI.create(issuer)));
+		return getBuilder(issuer, oidc(issuer));
 	}
 
 	/**
@@ -189,21 +188,17 @@ public static ClientRegistration.Builder fromOidcIssuerLocation(String issuer) {
 	 */
 	public static ClientRegistration.Builder fromIssuerLocation(String issuer) {
 		Assert.hasText(issuer, "issuer cannot be empty");
-		URI uri = URI.create(issuer);
-		return getBuilder(issuer, oidc(uri), oidcRfc8414(uri), oauth(uri));
+		return getBuilder(issuer, oidc(issuer), oidcRfc8414(issuer), oauth(issuer));
 	}
 
-	private static Supplier<ClientRegistration.Builder> oidc(URI issuer) {
-		// @formatter:off
-		URI uri = UriComponentsBuilder.fromUri(issuer)
-				.replacePath(issuer.getPath() + OIDC_METADATA_PATH)
-				.build(Collections.emptyMap());
+	static Supplier<ClientRegistration.Builder> oidc(String issuer) {
+		UriComponents uri = oidcUri(issuer);
 		// @formatter:on
 		return () -> {
-			RequestEntity<Void> request = RequestEntity.get(uri).build();
+			RequestEntity<Void> request = RequestEntity.get(uri.toUriString()).build();
 			Map<String, Object> configuration = rest.exchange(request, typeReference).getBody();
 			OIDCProviderMetadata metadata = parse(configuration, OIDCProviderMetadata::parse);
-			ClientRegistration.Builder builder = withProviderConfiguration(metadata, issuer.toASCIIString())
+			ClientRegistration.Builder builder = withProviderConfiguration(metadata, issuer)
 				.jwkSetUri(metadata.getJWKSetURI().toASCIIString());
 			if (metadata.getUserInfoEndpointURI() != null) {
 				builder.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString());
@@ -212,35 +207,48 @@ private static Supplier<ClientRegistration.Builder> oidc(URI issuer) {
 		};
 	}
 
-	private static Supplier<ClientRegistration.Builder> oidcRfc8414(String issuer) {
-		URI uri = oidcRfc8414Uri(issuer);
+	static UriComponents oidcUri(String issuer) {
+		UriComponents uri = UriComponentsBuilder.fromUriString(issuer).build();
+		// @formatter:off
+		return UriComponentsBuilder.newInstance().uriComponents(uri)
+				.replacePath(uri.getPath() + OIDC_METADATA_PATH)
+				.build();
+	}
+
+	static Supplier<ClientRegistration.Builder> oidcRfc8414(String issuer) {
+		UriComponents uri = oidcRfc8414Uri(issuer);
+		// @formatter:on
 		return getRfc8414Builder(issuer, uri);
 	}
 
-	static URI oidcRfc8414Uri(String issuer) {
+	static UriComponents oidcRfc8414Uri(String issuer) {
 		UriComponents uri = UriComponentsBuilder.fromUriString(issuer).build();
 		// @formatter:off
 		return UriComponentsBuilder.newInstance().uriComponents(uri)
 				.replacePath(OIDC_METADATA_PATH + uri.getPath())
-				.build(Collections.emptyMap());
-		// @formatter:on
+				.build();
+	}
+
+	static Supplier<ClientRegistration.Builder> oauth(String issuer) {
+		UriComponents uri = oauthUri(issuer);
+		return getRfc8414Builder(issuer, uri);
 	}
 
-	private static Supplier<ClientRegistration.Builder> oauth(URI issuer) {
+	static UriComponents oauthUri(String issuer) {
+		UriComponents uri = UriComponentsBuilder.fromUriString(issuer).build();
 		// @formatter:off
-		URI uri = UriComponentsBuilder.fromUri(issuer)
-				.replacePath(OAUTH_METADATA_PATH + issuer.getPath())
-				.build(Collections.emptyMap());
+		return UriComponentsBuilder.newInstance().uriComponents(uri)
+				.replacePath(OAUTH_METADATA_PATH + uri.getPath())
+				.build();
 		// @formatter:on
-		return getRfc8414Builder(issuer, uri);
 	}
 
-	private static Supplier<ClientRegistration.Builder> getRfc8414Builder(URI issuer, URI uri) {
+	private static Supplier<ClientRegistration.Builder> getRfc8414Builder(String issuer, UriComponents uri) {
 		return () -> {
-			RequestEntity<Void> request = RequestEntity.get(uri).build();
+			RequestEntity<Void> request = RequestEntity.get(uri.toUriString()).build();
 			Map<String, Object> configuration = rest.exchange(request, typeReference).getBody();
 			AuthorizationServerMetadata metadata = parse(configuration, AuthorizationServerMetadata::parse);
-			ClientRegistration.Builder builder = withProviderConfiguration(metadata, issuer.toASCIIString());
+			ClientRegistration.Builder builder = withProviderConfiguration(metadata, issuer);
 			URI jwkSetUri = metadata.getJWKSetURI();
 			if (jwkSetUri != null) {
 				builder.jwkSetUri(jwkSetUri.toASCIIString());
diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTests.java
index 59c0fb05288..f66fe394548 100644
--- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTests.java
+++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTests.java
@@ -34,6 +34,7 @@
 import org.springframework.http.MediaType;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
 import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.web.util.UriComponents;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -569,6 +570,17 @@ public void issuerWhenOidcConfigurationTlsClientAuthMethodThenSuccess() throws E
 			.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
 	}
 
+	// gh-15852
+	@Test
+	public void oidcWhenHostContainsUnderscoreThenRetains() {
+		UriComponents oidc = ClientRegistrations.oidcUri("https://elated_sutherland:8080/path");
+		assertThat(oidc.getHost()).isEqualTo("elated_sutherland");
+		UriComponents oauth = ClientRegistrations.oauthUri("https://elated_sutherland:8080/path");
+		assertThat(oauth.getHost()).isEqualTo("elated_sutherland");
+		UriComponents oidcRfc8414 = ClientRegistrations.oidcRfc8414Uri("https://elated_sutherland:8080/path");
+		assertThat(oidcRfc8414.getHost()).isEqualTo("elated_sutherland");
+	}
+
 	private ClientRegistration.Builder registration(String path) throws Exception {
 		this.issuer = createIssuerFromServer(path);
 		this.response.put("issuer", this.issuer);