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 d8ace6bb5d9..232254a43cc 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.client.UnknownContentTypeException;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
@@ -279,6 +280,9 @@ private static ClientRegistration.Builder getBuilder(String issuer,
errors.add(ex.getMessage());
// else try another endpoint
}
+ catch (UnknownContentTypeException ex) {
+ errors.add(ex.getMessage());
+ }
catch (IllegalArgumentException | IllegalStateException ex) {
throw ex;
}
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 bf7dba47183..d8c1ccec078 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
@@ -581,6 +581,39 @@ public void oidcWhenHostContainsUnderscoreThenRetains() {
assertThat(oidcRfc8414.getHost()).isEqualTo("elated_sutherland");
}
+ @Test
+ public void issuerWhenOidcHtmlThenFallbackToOAuth2ThenSuccess() throws Exception {
+ ClientRegistration registration = registrationOAuth2WithOidcHtml("issuer1", null).build();
+ ClientRegistration.ProviderDetails provider = registration.getProviderDetails();
+ assertThat(provider.getAuthorizationUri()).isEqualTo("https://example.com/o/oauth2/v2/auth");
+ assertThat(provider.getTokenUri()).isEqualTo("https://example.com/oauth2/v4/token");
+ assertThat(provider.getIssuerUri()).isEqualTo(this.issuer);
+
+ // order: OIDC(issuer-prefixed) -> OIDC(host-prefixed) -> OAuth
+ RecordedRequest request1 = this.server.takeRequest();
+ assertThat(request1.getPath()).isEqualTo("/issuer1/.well-known/openid-configuration");
+ RecordedRequest request2 = this.server.takeRequest();
+ assertThat(request2.getPath()).isEqualTo("/.well-known/openid-configuration/issuer1");
+ RecordedRequest request3 = this.server.takeRequest();
+ assertThat(request3.getPath()).isEqualTo("/.well-known/oauth-authorization-server/issuer1");
+ }
+
+ @Test
+ public void issuerWhenFirstEndpoint5xxThenThrowsIllegalArgumentException() throws Exception {
+ this.issuer = createIssuerFromServer("issuer1");
+ this.server.setDispatcher(new Dispatcher() {
+ @Override
+ public MockResponse dispatch(RecordedRequest req) {
+ return switch (req.getPath()) {
+ case "/issuer1/.well-known/openid-configuration" -> new MockResponse().setResponseCode(500);
+ default -> new MockResponse().setResponseCode(404);
+ };
+ }
+ });
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> ClientRegistrations.fromIssuerLocation(this.issuer).build());
+ }
+
@Test
public void issuerWhenAllEndpointsFailedThenExceptionIncludesFailureInformation() {
this.issuer = createIssuerFromServer("issuer1");
@@ -673,6 +706,41 @@ public MockResponse dispatch(RecordedRequest request) {
return ClientRegistrations.fromIssuerLocation(this.issuer).clientId("client-id").clientSecret("client-secret");
}
+ /**
+ * Simulates a situation when the OIDC discovery endpoints
+ * "/issuer1/.well-known/openid-configuration" and
+ * "/.well-known/openid-configuration/issuer1" respond with HTTP 200 and text/html
+ * (non-JSON), so discovery falls back to
+ * "/.well-known/oauth-authorization-server/issuer1", which responds with HTTP 200 and
+ * JSON.
+ *
+ * @see Section 3.1
+ * @see Section 5
+ */
+ private ClientRegistration.Builder registrationOAuth2WithOidcHtml(String path, String body) throws Exception {
+ this.issuer = createIssuerFromServer(path);
+ this.response.put("issuer", this.issuer);
+ String responseBody = (body != null) ? body : this.mapper.writeValueAsString(this.response);
+ final Dispatcher dispatcher = new Dispatcher() {
+ @Override
+ public MockResponse dispatch(RecordedRequest request) {
+ return switch (request.getPath()) {
+ case "/issuer1/.well-known/openid-configuration", "/.well-known/openid-configuration/issuer1",
+ "/.well-known/openid-configuration/" ->
+ new MockResponse().setResponseCode(200)
+ .setBody("not json")
+ .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_HTML_VALUE);
+ case "/.well-known/oauth-authorization-server/issuer1",
+ "/.well-known/oauth-authorization-server/" ->
+ buildSuccessMockResponse(responseBody);
+ default -> new MockResponse().setResponseCode(404);
+ };
+ }
+ };
+ this.server.setDispatcher(dispatcher);
+ return ClientRegistrations.fromIssuerLocation(this.issuer).clientId("client-id").clientSecret("client-secret");
+ }
+
private MockResponse buildSuccessMockResponse(String body) {
// @formatter:off
return new MockResponse().setResponseCode(200)