Browse Source

Fix OIDC discovery to fallback on text/html responses

Some providers return 200 OK with text/html for
/.well-known/openid-configuration.
Previously, discovery stopped at the first endpoint.

This commit records UnknownContentTypeException and moves to
the next candidate, enabling fallback to the OAuth 2.0 AS
metadata endpoint.

Closes gh-17036

Signed-off-by: 이현수 <znight1020@naver.com>
pull/17927/head
이현수 3 months ago
parent
commit
f24c3012bc
No known key found for this signature in database
GPG Key ID: 1B893C8A148AEBF0
  1. 4
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java
  2. 68
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTests.java

4
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java

@ -37,6 +37,7 @@ import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; @@ -37,6 +37,7 @@ import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
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 @@ public final class ClientRegistrations { @@ -279,6 +280,9 @@ public final class ClientRegistrations {
errors.add(ex.getMessage());
// else try another endpoint
}
catch (UnknownContentTypeException ex) {
errors.add(ex.getMessage());
}
catch (IllegalArgumentException | IllegalStateException ex) {
throw ex;
}

68
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTests.java

@ -581,6 +581,39 @@ public class ClientRegistrationsTests { @@ -581,6 +581,39 @@ public class ClientRegistrationsTests {
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 class ClientRegistrationsTests { @@ -673,6 +706,41 @@ public class ClientRegistrationsTests {
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 <a href="https://tools.ietf.org/html/rfc8414#section-3.1">Section 3.1</a>
* @see <a href="https://tools.ietf.org/html/rfc8414#section-5">Section 5</a>
*/
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("<html>not json</html>")
.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)

Loading…
Cancel
Save