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 5a7663a5262..9accaa924b5 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
@@ -19,28 +19,41 @@
import com.nimbusds.oauth2.sdk.GrantType;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.as.AuthorizationServerMetadata;
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
+import org.springframework.util.Assert;
import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import java.util.Collections;
+import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Allows creating a {@link ClientRegistration.Builder} from an
- * OpenID Provider Configuration.
+ * OpenID Provider Configuration
+ * or Authorization Server Metadata based on
+ * provided issuer.
*
* @author Rob Winch
* @author Josh Cummings
+ * @author Rafiullah Hamedy
* @since 5.1
*/
public final class ClientRegistrations {
+ private static final String OIDC_METADATA_PATH = "/.well-known/openid-configuration";
+ private static final String OAUTH2_METADATA_PATH = "/.well-known/oauth-authorization-server";
+
+ enum ProviderType {
+ OIDCV1, OIDC, OAUTH2;
+ }
/**
* Creates a {@link ClientRegistration.Builder} using the provided
@@ -50,6 +63,12 @@ public final class ClientRegistrations {
* OpenID
* Provider Configuration Response to initialize the {@link ClientRegistration.Builder}.
*
+ * When deployed in legacy environments using OpenID Connect Discovery 1.0 and if the provided issuer has
+ * a path i.e. /issuer1 then as per Compatibility Notes
+ * first make an OpenID Provider
+ * Configuration Request using path /.well-known/openid-configuration/issuer1 and only if the retrieval
+ * fail then a subsequent request to path /issuer1/.well-known/openid-configuration should be made.
+ *
*
* For example, if the issuer provided is "https://example.com", then an "OpenID Provider Configuration Request" will
* be made to "https://example.com/.well-known/openid-configuration". The result is expected to be an "OpenID
@@ -69,11 +88,77 @@ public final class ClientRegistrations {
* @return a {@link ClientRegistration.Builder} that was initialized by the OpenID Provider Configuration.
*/
public static ClientRegistration.Builder fromOidcIssuerLocation(String issuer) {
- String openidConfiguration = getOpenidConfiguration(issuer);
- OIDCProviderMetadata metadata = parse(openidConfiguration);
+ Map configuration = getIssuerConfiguration(issuer, OIDC_METADATA_PATH);
+ OIDCProviderMetadata metadata = parse(configuration.get(ProviderType.OIDCV1), OIDCProviderMetadata::parse);
+ return withProviderConfiguration(metadata, issuer)
+ .userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString());
+ }
+
+ /**
+ * Unlike fromOidcIssuerLocation the fromIssuerLocation queries three different endpoints and uses the
+ * returned response from whichever that returns successfully. When fromIssuerLocation is invoked with an issuer
+ * the following sequence of actions take place
+ *
+ *
+ * -
+ * The first request is made against {host}/.well-known/openid-configuration/issuer1 where issuer is equal to
+ * issuer1. See Compatibility Notes of RFC 8414
+ * specification for more details.
+ *
+ * -
+ * If the first attempt request returned non-Success (i.e. 200 status code) response then based on Compatibility Notes of
+ * RFC 8414 a fallback
+ * OpenID Provider Configuration Request is made to {host}/issuer1/.well-known/openid-configuration
+ *
+ * -
+ * If the second attempted request returns a non-Success (i.e. 200 status code) response then based a final
+ * Authorization Server Metadata Request is being made to
+ * {host}/.well-known/oauth-authorization-server/issuer1.
+ *
+ *
+ *
+ *
+ * As explained above, fromIssuerLocation would behave the exact same way as fromOidcIssuerLocation and that is
+ * because fromIssuerLocation does the exact same processing as fromOidcIssuerLocation behind the scene. Use of
+ * fromIssuerLocation is encouraged due to the fact that it is well-aligned with RFC 8414 specification and more specifically
+ * it queries latest OIDC metadata endpoint with a fallback to legacy OIDC v1 discovery endpoint.
+ *
+ * The fromIssuerLocation is based on RFC 8414 specification.
+ *
+ *
+ * Example usage:
+ *
+ *
+ * ClientRegistration registration = ClientRegistrations.fromIssuerLocation("https://example.com")
+ * .clientId("client-id")
+ * .clientSecret("client-secret")
+ * .build();
+ *
+ *
+ * @param issuer
+ * @return a {@link ClientRegistration.Builder} that was initialized by the Authorization Sever Metadata Provider
+ */
+ public static ClientRegistration.Builder fromIssuerLocation(String issuer) {
+ Map configuration = getIssuerConfiguration(issuer, OIDC_METADATA_PATH, OAUTH2_METADATA_PATH);
+
+ if (configuration.containsKey(ProviderType.OAUTH2)) {
+ AuthorizationServerMetadata metadata = parse(configuration.get(ProviderType.OAUTH2), AuthorizationServerMetadata::parse);
+ ClientRegistration.Builder builder = withProviderConfiguration(metadata, issuer);
+ return builder;
+ } else {
+ String response = configuration.getOrDefault(ProviderType.OIDC, configuration.get(ProviderType.OIDCV1));
+ OIDCProviderMetadata metadata = parse(response, OIDCProviderMetadata::parse);
+ ClientRegistration.Builder builder = withProviderConfiguration(metadata, issuer)
+ .userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString());
+ return builder;
+ }
+ }
+
+ private static ClientRegistration.Builder withProviderConfiguration(AuthorizationServerMetadata metadata, String issuer) {
String metadataIssuer = metadata.getIssuer().getValue();
if (!issuer.equals(metadataIssuer)) {
- throw new IllegalStateException("The Issuer \"" + metadataIssuer + "\" provided in the OpenID Configuration did not match the requested issuer \"" + issuer + "\"");
+ throw new IllegalStateException("The Issuer \"" + metadataIssuer + "\" provided in the configuration metadata did "
+ + "not match the requested issuer \"" + issuer + "\"");
}
String name = URI.create(issuer).getHost();
@@ -81,7 +166,8 @@ public static ClientRegistration.Builder fromOidcIssuerLocation(String issuer) {
List grantTypes = metadata.getGrantTypes();
// If null, the default includes authorization_code
if (grantTypes != null && !grantTypes.contains(GrantType.AUTHORIZATION_CODE)) {
- throw new IllegalArgumentException("Only AuthorizationGrantType.AUTHORIZATION_CODE is supported. The issuer \"" + issuer + "\" returned a configuration of " + grantTypes);
+ throw new IllegalArgumentException("Only AuthorizationGrantType.AUTHORIZATION_CODE is supported. The issuer \"" + issuer +
+ "\" returned a configuration of " + grantTypes);
}
List scopes = getScopes(metadata);
Map configurationMetadata = new LinkedHashMap<>(metadata.toJSONObject());
@@ -95,21 +181,118 @@ public static ClientRegistration.Builder fromOidcIssuerLocation(String issuer) {
.authorizationUri(metadata.getAuthorizationEndpointURI().toASCIIString())
.jwkSetUri(metadata.getJWKSetURI().toASCIIString())
.providerConfigurationMetadata(configurationMetadata)
- .userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString())
.tokenUri(metadata.getTokenEndpointURI().toASCIIString())
.clientName(issuer);
}
- private static String getOpenidConfiguration(String issuer) {
+ /**
+ * When the length of paths is equal to one (1) then it's a request for OpenId v1 discovery endpoint
+ * hence the request is made to {host}/issuer1/.well-known/openid-configuration.
+ * Otherwise, all three (3) metadata endpoints are queried one after another.
+ *
+ * @param issuer
+ * @param paths
+ * @throws IllegalArgumentException if the paths is null or empty or if none of the providers
+ * responded to given issuer and paths requests
+ * @return Map - Configuration Metadata from the given issuer
+ */
+ private static Map getIssuerConfiguration(String issuer, String... paths) {
+ Assert.notEmpty(paths, "paths cannot be empty or null.");
+
+ Map providersUrl = buildIssuerConfigurationUrls(issuer, paths);
+ Map providerResponse = new HashMap<>();
+
+ if (providersUrl.containsKey(ProviderType.OIDC)) {
+ providerResponse = mapResponse(providersUrl, ProviderType.OIDC);
+ }
+
+ // Fallback to OpenId v1 Discovery Endpoint based on RFC 8414 Compatibility Notes
+ if (providerResponse.isEmpty() && providersUrl.containsKey(ProviderType.OIDCV1)) {
+ providerResponse = mapResponse(providersUrl, ProviderType.OIDCV1);
+ }
+
+ if (providerResponse.isEmpty() && providersUrl.containsKey(ProviderType.OAUTH2)) {
+ providerResponse = mapResponse(providersUrl, ProviderType.OAUTH2);
+ }
+
+ if (providerResponse.isEmpty()) {
+ throw new IllegalArgumentException("Unable to resolve Configuration with the provided Issuer of \"" + issuer + "\"");
+ }
+ return providerResponse;
+ }
+
+ private static Map mapResponse(Map providersUrl, ProviderType providerType) {
+ Map providerResponse = new HashMap<>();
+ String response = makeIssuerRequest(providersUrl.get(providerType));
+ if (response != null) {
+ providerResponse.put(providerType, response);
+ }
+ return providerResponse;
+ }
+
+ private static String makeIssuerRequest(String uri) {
RestTemplate rest = new RestTemplate();
try {
- return rest.getForObject(issuer + "/.well-known/openid-configuration", String.class);
- } catch(RuntimeException e) {
- throw new IllegalArgumentException("Unable to resolve the OpenID Configuration with the provided Issuer of \"" + issuer + "\"", e);
+ return rest.getForObject(uri, String.class);
+ } catch(RuntimeException ex) {
+ return null;
+ }
+ }
+
+ /**
+ * When invoked with a path then make a
+ *
+ * OpenID Provider Configuration Request by querying the OpenId Connection Discovery 1.0 endpoint
+ * and the url would look as follow {host}/issuer1/.well-known/openid-configuration
+ *
+ *
+ * When more than one path is provided then query all the three (3) endpoints for metadata configuration
+ * as per Section 5 of RF 8414 specification
+ * and the URLs would look as follow
+ *
+ *
+ *
+ * -
+ * {host}/.well-known/openid-configuration/issuer1 - OpenID as per RFC 8414
+ *
+ * -
+ * {host}/issuer1/.well-known/openid-configuration - OpenID Connect 1.0 Discovery Compatibility as per RFC 8414
+ *
+ * -
+ * /.well-known/oauth-authorization-server/issuer1 - OAuth2 Authorization Server Metadata as per RFC 8414
+ *
+ *
+ *
+ * @param issuer
+ * @param paths
+ * @throws IllegalArgumentException throws exception if paths length is not 1 or 3, 1 for fromOidcLocationIssuer
+ * and 3 for the newly introduced fromIssuerLocation to support querying 3 different metadata provider endpoints
+ * @return Map key-value map of provider with its request url
+ */
+ private static Map buildIssuerConfigurationUrls(String issuer, String... paths) {
+ Assert.isTrue(paths.length != 1 || paths.length != 3, "paths length can either be 1 or 3");
+
+ Map providersUrl = new HashMap<>();
+
+ URI issuerURI = URI.create(issuer);
+
+ if (paths.length == 1) {
+ providersUrl.put(ProviderType.OIDCV1,
+ UriComponentsBuilder.fromUri(issuerURI).replacePath(issuerURI.getPath() + paths[0]).toUriString());
+ } else {
+ providersUrl.put(ProviderType.OIDC,
+ UriComponentsBuilder.fromUri(issuerURI).replacePath(paths[0] + issuerURI.getPath()).toUriString());
+ providersUrl.put(ProviderType.OIDCV1,
+ UriComponentsBuilder.fromUri(issuerURI).replacePath(issuerURI.getPath() + paths[0]).toUriString());
+ providersUrl.put(ProviderType.OAUTH2,
+ UriComponentsBuilder.fromUri(issuerURI).replacePath(paths[1] + issuerURI.getPath()).toUriString());
}
+
+ return providersUrl;
}
- private static ClientAuthenticationMethod getClientAuthenticationMethod(String issuer, List metadataAuthMethods) {
+ private static ClientAuthenticationMethod getClientAuthenticationMethod(String issuer,
+ List metadataAuthMethods) {
if (metadataAuthMethods == null || metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) {
// If null, the default includes client_secret_basic
return ClientAuthenticationMethod.BASIC;
@@ -120,10 +303,11 @@ private static ClientAuthenticationMethod getClientAuthenticationMethod(String i
if (metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.NONE)) {
return ClientAuthenticationMethod.NONE;
}
- throw new IllegalArgumentException("Only ClientAuthenticationMethod.BASIC, ClientAuthenticationMethod.POST and ClientAuthenticationMethod.NONE are supported. The issuer \"" + issuer + "\" returned a configuration of " + metadataAuthMethods);
+ throw new IllegalArgumentException("Only ClientAuthenticationMethod.BASIC, ClientAuthenticationMethod.POST and "
+ + "ClientAuthenticationMethod.NONE are supported. The issuer \"" + issuer + "\" returned a configuration of " + metadataAuthMethods);
}
- private static List getScopes(OIDCProviderMetadata metadata) {
+ private static List getScopes(AuthorizationServerMetadata metadata) {
Scope scope = metadata.getScopes();
if (scope == null) {
// If null, default to "openid" which must be supported
@@ -133,15 +317,18 @@ private static List getScopes(OIDCProviderMetadata metadata) {
}
}
- private static OIDCProviderMetadata parse(String body) {
+ private static T parse(String body, ThrowingFunction parser) {
try {
- return OIDCProviderMetadata.parse(body);
- }
- catch (ParseException e) {
+ return parser.apply(body);
+ } catch (ParseException e) {
throw new RuntimeException(e);
}
}
+ private interface ThrowingFunction {
+ T apply(S src) throws E;
+ }
+
private ClientRegistrations() {}
}
diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTest.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTest.java
index 5911b299af7..26394d04ee9 100644
--- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTest.java
+++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTest.java
@@ -18,8 +18,12 @@
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
+
+import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -36,6 +40,7 @@
/**
* @author Rob Winch
+ * @author Rafiullah Hamedy
* @since 5.1
*/
public class ClientRegistrationsTest {
@@ -122,7 +127,34 @@ public void cleanup() throws Exception {
public void issuerWhenAllInformationThenSuccess() throws Exception {
ClientRegistration registration = registration("").build();
ClientRegistration.ProviderDetails provider = registration.getProviderDetails();
+ assertIssuerMetadata(registration, provider);
+ assertThat(provider.getUserInfoEndpoint().getUri()).isEqualTo("https://example.com/oauth2/v3/userinfo");
+ }
+
+ /**
+ *
+ * Test compatibility with OpenID v1 discovery endpoint by making a
+ * OpenID Provider
+ * Configuration Request as highlighted
+ * Compatibility Notes of RFC 8414 specification.
+ */
+ @Test
+ public void issuerWhenOidcFallbackAllInformationThenSuccess() throws Exception {
+ ClientRegistration registration = registrationOidcFallback("issuer1", null).build();
+ ClientRegistration.ProviderDetails provider = registration.getProviderDetails();
+ assertIssuerMetadata(registration, provider);
+ assertThat(provider.getUserInfoEndpoint().getUri()).isEqualTo("https://example.com/oauth2/v3/userinfo");
+ }
+
+ @Test
+ public void issuerWhenOauth2AllInformationThenSuccess() throws Exception {
+ ClientRegistration registration = registrationOauth2("", null).build();
+ ClientRegistration.ProviderDetails provider = registration.getProviderDetails();
+ assertIssuerMetadata(registration, provider);
+ }
+ private void assertIssuerMetadata(ClientRegistration registration,
+ ClientRegistration.ProviderDetails provider) {
assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC);
assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
assertThat(registration.getRegistrationId()).isEqualTo(this.server.getHostName());
@@ -135,7 +167,6 @@ public void issuerWhenAllInformationThenSuccess() throws Exception {
"code_challenge_methods_supported", "id_token_signing_alg_values_supported", "issuer", "jwks_uri",
"response_types_supported", "revocation_endpoint", "scopes_supported", "subject_types_supported",
"grant_types_supported", "token_endpoint", "token_endpoint_auth_methods_supported", "userinfo_endpoint");
- assertThat(provider.getUserInfoEndpoint().getUri()).isEqualTo("https://example.com/oauth2/v3/userinfo");
}
@Test
@@ -144,6 +175,18 @@ public void issuerWhenContainsTrailingSlashThenSuccess() throws Exception {
assertThat(this.issuer).endsWith("/");
}
+ @Test
+ public void issuerWhenOidcFallbackContainsTrailingSlashThenSuccess() throws Exception {
+ assertThat(registrationOidcFallback("", null)).isNotNull();
+ assertThat(this.issuer).endsWith("/");
+ }
+
+ @Test
+ public void issuerWhenOauth2ContainsTrailingSlashThenSuccess() throws Exception {
+ assertThat(registrationOauth2("", null)).isNotNull();
+ assertThat(this.issuer).endsWith("/");
+ }
+
/**
* https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
*
@@ -160,6 +203,25 @@ public void issuerWhenScopesNullThenScopesDefaulted() throws Exception {
assertThat(registration.getScopes()).containsOnly("openid");
}
+ @Test
+ public void issuerWhenOidcFallbackScopesNullThenScopesDefaulted() throws Exception {
+ this.response.remove("scopes_supported");
+
+ ClientRegistration registration = registrationOidcFallback("", null).build();
+
+ assertThat(registration.getScopes()).containsOnly("openid");
+ }
+
+ @Test
+ public void issuerWhenOauth2ScopesNullThenScopesDefaulted() throws Exception {
+ this.response.remove("scopes_supported");
+
+ ClientRegistration registration = registrationOauth2("", null).build();
+
+ assertThat(registration.getScopes()).containsOnly("openid");
+ }
+
+
@Test
public void issuerWhenGrantTypesSupportedNullThenDefaulted() throws Exception {
this.response.remove("grant_types_supported");
@@ -169,10 +231,20 @@ public void issuerWhenGrantTypesSupportedNullThenDefaulted() throws Exception {
assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
}
+ @Test
+ public void issuerWhenOauth2GrantTypesSupportedNullThenDefaulted() throws Exception {
+ this.response.remove("grant_types_supported");
+
+ ClientRegistration registration = registrationOauth2("", null).build();
+
+ assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
+ }
+
/**
* We currently only support authorization_code, so verify we have a meaningful error until we add support.
* @throws Exception
*/
+ @Test
public void issuerWhenGrantTypesSupportedInvalidThenException() throws Exception {
this.response.put("grant_types_supported", Arrays.asList("implicit"));
@@ -181,6 +253,15 @@ public void issuerWhenGrantTypesSupportedInvalidThenException() throws Exception
.hasMessageContaining("Only AuthorizationGrantType.AUTHORIZATION_CODE is supported. The issuer \"" + this.issuer + "\" returned a configuration of [implicit]");
}
+ @Test
+ public void issuerWhenOauth2GrantTypesSupportedInvalidThenException() throws Exception {
+ this.response.put("grant_types_supported", Arrays.asList("implicit"));
+
+ assertThatThrownBy(() -> registrationOauth2("", null))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Only AuthorizationGrantType.AUTHORIZATION_CODE is supported. The issuer \"" + this.issuer + "\" returned a configuration of [implicit]");
+ }
+
@Test
public void issuerWhenTokenEndpointAuthMethodsNullThenDefaulted() throws Exception {
this.response.remove("token_endpoint_auth_methods_supported");
@@ -190,6 +271,15 @@ public void issuerWhenTokenEndpointAuthMethodsNullThenDefaulted() throws Excepti
assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC);
}
+ @Test
+ public void issuerWhenOauth2TokenEndpointAuthMethodsNullThenDefaulted() throws Exception {
+ this.response.remove("token_endpoint_auth_methods_supported");
+
+ ClientRegistration registration = registrationOauth2("", null).build();
+
+ assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC);
+ }
+
@Test
public void issuerWhenTokenEndpointAuthMethodsPostThenMethodIsPost() throws Exception {
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_post"));
@@ -199,6 +289,15 @@ public void issuerWhenTokenEndpointAuthMethodsPostThenMethodIsPost() throws Exce
assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.POST);
}
+ @Test
+ public void issuerWhenOauth2TokenEndpointAuthMethodsPostThenMethodIsPost() throws Exception {
+ this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_post"));
+
+ ClientRegistration registration = registrationOauth2("", null).build();
+
+ assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.POST);
+ }
+
@Test
public void issuerWhenTokenEndpointAuthMethodsNoneThenMethodIsNone() throws Exception {
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("none"));
@@ -208,6 +307,15 @@ public void issuerWhenTokenEndpointAuthMethodsNoneThenMethodIsNone() throws Exce
assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.NONE);
}
+ @Test
+ public void issuerWhenOauth2TokenEndpointAuthMethodsNoneThenMethodIsNone() throws Exception {
+ this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("none"));
+
+ ClientRegistration registration = registrationOauth2("", null).build();
+
+ assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.NONE);
+ }
+
/**
* We currently only support client_secret_basic, so verify we have a meaningful error until we add support.
* @throws Exception
@@ -221,10 +329,25 @@ public void issuerWhenTokenEndpointAuthMethodsInvalidThenException() throws Exce
.hasMessageContaining("Only ClientAuthenticationMethod.BASIC, ClientAuthenticationMethod.POST and ClientAuthenticationMethod.NONE are supported. The issuer \"" + this.issuer + "\" returned a configuration of [tls_client_auth]");
}
+ @Test
+ public void issuerWhenOauth2TokenEndpointAuthMethodsInvalidThenException() throws Exception {
+ this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("tls_client_auth"));
+
+ assertThatThrownBy(() -> registrationOauth2("", null))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Only ClientAuthenticationMethod.BASIC, ClientAuthenticationMethod.POST and ClientAuthenticationMethod.NONE are supported. The issuer \"" + this.issuer + "\" returned a configuration of [tls_client_auth]");
+ }
+
+ @Test
+ public void issuerWhenOauth2EmptyStringThenMeaningfulErrorMessage() {
+ assertThatThrownBy(() -> ClientRegistrations.fromIssuerLocation(""))
+ .hasMessageContaining("Unable to resolve Configuration with the provided Issuer of \"\"");
+ }
+
@Test
public void issuerWhenEmptyStringThenMeaningfulErrorMessage() {
assertThatThrownBy(() -> ClientRegistrations.fromOidcIssuerLocation(""))
- .hasMessageContaining("Unable to resolve the OpenID Configuration with the provided Issuer of \"\"");
+ .hasMessageContaining("Unable to resolve Configuration with the provided Issuer of \"\"");
}
@Test
@@ -236,7 +359,19 @@ public void issuerWhenOpenIdConfigurationDoesNotMatchThenMeaningfulErrorMessage(
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
this.server.enqueue(mockResponse);
assertThatThrownBy(() -> ClientRegistrations.fromOidcIssuerLocation(this.issuer))
- .hasMessageContaining("The Issuer \"https://example.com\" provided in the OpenID Configuration did not match the requested issuer \"" + this.issuer + "\"");
+ .hasMessageContaining("The Issuer \"https://example.com\" provided in the configuration metadata did not match the requested issuer \"" + this.issuer + "\"");
+ }
+
+ @Test
+ public void issuerWhenOauth2ConfigurationDoesNotMatchThenMeaningfulErrorMessage() throws Exception {
+ this.issuer = createIssuerFromServer("");
+ String body = this.mapper.writeValueAsString(this.response);
+ MockResponse mockResponse = new MockResponse()
+ .setBody(body)
+ .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
+ this.server.enqueue(mockResponse);
+ assertThatThrownBy(() -> ClientRegistrations.fromIssuerLocation(this.issuer))
+ .hasMessageContaining("The Issuer \"https://example.com\" provided in the configuration metadata did not match the requested issuer \"" + this.issuer + "\"");
}
private ClientRegistration.Builder registration(String path) throws Exception {
@@ -253,7 +388,72 @@ private ClientRegistration.Builder registration(String path) throws Exception {
.clientSecret("client-secret");
}
+ private ClientRegistration.Builder registrationOauth2(String path, String body) throws Exception {
+ this.issuer = createIssuerFromServer(path);
+ this.response.put("issuer", this.issuer);
+ this.issuer = this.server.url(path).toString();
+ final String responseBody = body != null ? body : this.mapper.writeValueAsString(this.response);
+
+ final Dispatcher dispatcher = new Dispatcher() {
+ @Override
+ public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
+ switch(request.getPath()) {
+ case "/.well-known/oauth-authorization-server/issuer1":
+ case "/.well-known/oauth-authorization-server/":
+ return buildSuccessMockResponse(responseBody);
+ }
+ return new MockResponse().setResponseCode(404);
+ }
+ };
+
+ this.server.setDispatcher(dispatcher);
+
+ return ClientRegistrations.fromIssuerLocation(this.issuer)
+ .clientId("client-id")
+ .clientSecret("client-secret");
+ }
+
+
private String createIssuerFromServer(String path) {
return this.server.url(path).toString();
}
+
+ /**
+ * Simulates a situation when the ClientRegistration is used with a legacy application where the OIDC
+ * Discovery Endpoint is "/issuer1/.well-known/openid-configuration" instead of
+ * "/.well-known/openid-configuration/issuer1" in which case the first attempt results in HTTP 404 and
+ * the subsequent call results in 200 OK.
+ *
+ * @see Section 5 for more details.
+ */
+ private ClientRegistration.Builder registrationOidcFallback(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) throws InterruptedException {
+ System.out.println("request.getPath:" + request.getPath());
+ switch(request.getPath()) {
+ case "/issuer1/.well-known/openid-configuration":
+ case "/.well-known/openid-configuration/":
+ return buildSuccessMockResponse(responseBody);
+ }
+ return new MockResponse().setResponseCode(404);
+ }
+ };
+ this.server.setDispatcher(dispatcher);
+
+ return ClientRegistrations.fromIssuerLocation(this.issuer)
+ .clientId("client-id")
+ .clientSecret("client-secret");
+ }
+
+ private MockResponse buildSuccessMockResponse(String body) {
+ return new MockResponse().setResponseCode(200)
+ .setBody(body)
+ .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
+ }
}
diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoders.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoders.java
index e407d25b7f7..2f72da63c68 100644
--- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoders.java
+++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoders.java
@@ -15,25 +15,32 @@
*/
package org.springframework.security.oauth2.jwt;
+import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSetUri;
+
import java.net.URI;
import java.util.Map;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.RequestEntity;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
+import org.springframework.util.Assert;
+import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
-import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSetUri;
-
/**
* Allows creating a {@link JwtDecoder} from an
- * OpenID Provider Configuration.
+ * OpenID Provider Configuration or
+ * Authorization Server Metadata Request based on provided
+ * issuer and method invoked.
*
* @author Josh Cummings
+ * @author Rafiullah Hamedy
* @since 5.1
*/
public final class JwtDecoders {
+ private static final String OIDC_METADATA_PATH = "/.well-known/openid-configuration";
+ private static final String OAUTH2_METADATA_PATH = "/.well-known/oauth-authorization-server";
/**
* Creates a {@link JwtDecoder} using the provided
@@ -47,37 +54,162 @@ public final class JwtDecoders {
* @return a {@link JwtDecoder} that was initialized by the OpenID Provider Configuration.
*/
public static JwtDecoder fromOidcIssuerLocation(String oidcIssuerLocation) {
- Map openidConfiguration = getOpenidConfiguration(oidcIssuerLocation);
+ Map configuration = getIssuerConfiguration(oidcIssuerLocation, OIDC_METADATA_PATH);
+ return withProviderConfiguration(configuration, oidcIssuerLocation);
+ }
+
+ /**
+ * Creates a {@link JwtDecoder} using the provided issuer by querying configuration metadata endpoints for
+ * OpenID (including fallback to legacy) and OAuth2 in order.
+ *
+ *
+ * -
+ * {host}/.well-known/openid-configuration/issuer1 - OpenID Provider Configuration Request based on
+ * Section 5 of
+ * RFC 8414 Specification
+ *
+ * -
+ * {host}/issuer1/.well-known/openid-configuration - OpenID v1 Discovery endpoint based on
+ * OpenID Provider
+ * Configuration Request with backward compatibility highlighted on
+ * Section 5 of RF 8414
+ *
+ * -
+ * {host}/.well-known/oauth-authorization-server/issuer1 - OAuth2 Authorization Server Metadata based on
+ * Section 3.1 of RFC 8414
+ *
+ *
+ *
+ * @param issuer
+ * @return a {@link JwtDecoder} that is initialized using
+ *
+ * OpenID Provider Configuration Response or
+ * Authorization Server Metadata Response depending on provided issuer
+ */
+ public static JwtDecoder fromIssuerLocation(String issuer) {
+ Map configuration = getIssuerConfiguration(issuer, OIDC_METADATA_PATH, OAUTH2_METADATA_PATH);
+ return withProviderConfiguration(configuration, issuer);
+ }
+
+ /**
+ * Validate provided issuer and build {@link JwtDecoder} from
+ * OpenID Provider
+ * Configuration Response and Authorization Server Metadata
+ * Response.
+ *
+ * @param configuration
+ * @param issuer
+ * @return {@link JwtDecoder}
+ */
+ private static JwtDecoder withProviderConfiguration(Map configuration, String issuer) {
String metadataIssuer = "(unavailable)";
- if (openidConfiguration.containsKey("issuer")) {
- metadataIssuer = openidConfiguration.get("issuer").toString();
+ if (configuration.containsKey("issuer")) {
+ metadataIssuer = configuration.get("issuer").toString();
}
- if (!oidcIssuerLocation.equals(metadataIssuer)) {
- throw new IllegalStateException("The Issuer \"" + metadataIssuer + "\" provided in the OpenID Configuration " +
- "did not match the requested issuer \"" + oidcIssuerLocation + "\"");
+ if (!issuer.equals(metadataIssuer)) {
+ throw new IllegalStateException("The Issuer \"" + metadataIssuer + "\" provided in the configuration did not "
+ + "match the requested issuer \"" + issuer + "\"");
}
- OAuth2TokenValidator jwtValidator =
- JwtValidators.createDefaultWithIssuer(oidcIssuerLocation);
-
- NimbusJwtDecoder jwtDecoder = withJwkSetUri(openidConfiguration.get("jwks_uri").toString()).build();
+ OAuth2TokenValidator jwtValidator = JwtValidators.createDefaultWithIssuer(issuer);
+ NimbusJwtDecoder jwtDecoder = withJwkSetUri(configuration.get("jwks_uri").toString()).build();
jwtDecoder.setJwtValidator(jwtValidator);
return jwtDecoder;
}
- private static Map getOpenidConfiguration(String issuer) {
- ParameterizedTypeReference