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)