Support for OAuth 2.0 Authorization Server Metadata

Added support for OAuth 2.0 Authorization Server Metadata as per the
RFC 8414 specification. Updated the existing implementation of OpenId to
comply with the Compatibility Section of RFC 8414 specification.

Fixes: gh-6500
This commit is contained in:
Rafiullah Hamedy 2019-04-02 21:26:56 -04:00 committed by Josh Cummings
parent b6e8997e95
commit f5b7706942
No known key found for this signature in database
GPG Key ID: 49EF60DD7FF83443
4 changed files with 728 additions and 58 deletions
oauth2
oauth2-client/src
main/java/org/springframework/security/oauth2/client/registration
test/java/org/springframework/security/oauth2/client/registration
oauth2-jose/src
main/java/org/springframework/security/oauth2/jwt
test/java/org/springframework/security/oauth2/jwt

@ -19,28 +19,41 @@ package org.springframework.security.oauth2.client.registration;
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
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig">OpenID Provider Configuration</a>.
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig">OpenID Provider Configuration</a>
* or <a href="https://tools.ietf.org/html/rfc8414#section-3">Authorization Server Metadata</a> 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 {
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">OpenID
* Provider Configuration Response</a> 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 <a href="https://tools.ietf.org/html/rfc8414#section-5">Compatibility Notes</a>
* first make an <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest">OpenID Provider
* Configuration Request</a> 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.
*
* <p>
* 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<ProviderType, String> configuration = getIssuerConfiguration(issuer, OIDC_METADATA_PATH);
OIDCProviderMetadata metadata = parse(configuration.get(ProviderType.OIDCV1), OIDCProviderMetadata::parse);
return withProviderConfiguration(metadata, issuer)
.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString());
}
/**
* Unlike <strong>fromOidcIssuerLocation</strong> the <strong>fromIssuerLocation</strong> queries three different endpoints and uses the
* returned response from whichever that returns successfully. When <strong>fromIssuerLocation</strong> is invoked with an issuer
* the following sequence of actions take place
*
* <ol>
* <li>
* The first request is made against <i>{host}/.well-known/openid-configuration/issuer1</i> where issuer is equal to
* <strong>issuer1</strong>. See <a href="https://tools.ietf.org/html/rfc8414#section-5">Compatibility Notes</a> of RFC 8414
* specification for more details.
* </li>
* <li>
* If the first attempt request returned non-Success (i.e. 200 status code) response then based on <strong>Compatibility Notes</strong> of
* <strong>RFC 8414</strong> a fallback <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest">
* OpenID Provider Configuration Request</a> is made to <i>{host}/issuer1/.well-known/openid-configuration</i>
* </li>
* <li>
* If the second attempted request returns a non-Success (i.e. 200 status code) response then based a final
* <a href="https://tools.ietf.org/html/rfc8414#section-3.1">Authorization Server Metadata Request</a> is being made to
* <i>{host}/.well-known/oauth-authorization-server/issuer1</i>.
* </li>
* </ol>
*
*
* As explained above, <strong>fromIssuerLocation</strong> would behave the exact same way as <strong>fromOidcIssuerLocation</strong> and that is
* because <strong>fromIssuerLocation</strong> does the exact same processing as <strong>fromOidcIssuerLocation</strong> behind the scene. Use of
* <strong>fromIssuerLocation</strong> 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 <strong>fromIssuerLocation</strong> is based on <a href="https://tools.ietf.org/html/rfc8414">RFC 8414</a> specification.
*
* <p>
* Example usage:
* </p>
* <pre>
* ClientRegistration registration = ClientRegistrations.fromIssuerLocation("https://example.com")
* .clientId("client-id")
* .clientSecret("client-secret")
* .build();
* </pre>
*
* @param issuer
* @return a {@link ClientRegistration.Builder} that was initialized by the Authorization Sever Metadata Provider
*/
public static ClientRegistration.Builder fromIssuerLocation(String issuer) {
Map<ProviderType, String> 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 final class ClientRegistrations {
List<GrantType> 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<String> scopes = getScopes(metadata);
Map<String, Object> configurationMetadata = new LinkedHashMap<>(metadata.toJSONObject());
@ -95,21 +181,118 @@ public final class ClientRegistrations {
.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 <strong>{host}/issuer1/.well-known/openid-configuration</strong>.
* 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<String, Object> - Configuration Metadata from the given issuer
*/
private static Map<ProviderType, String> getIssuerConfiguration(String issuer, String... paths) {
Assert.notEmpty(paths, "paths cannot be empty or null.");
Map<ProviderType, String> providersUrl = buildIssuerConfigurationUrls(issuer, paths);
Map<ProviderType, String> 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<ProviderType, String> mapResponse(Map<ProviderType, String> providersUrl, ProviderType providerType) {
Map<ProviderType, String> 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;
}
}
private static ClientAuthenticationMethod getClientAuthenticationMethod(String issuer, List<com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod> metadataAuthMethods) {
/**
* When invoked with a path then make a
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest">
* OpenID Provider Configuration Request</a> by querying the OpenId Connection Discovery 1.0 endpoint
* and the url would look as follow <strong>{host}/issuer1/.well-known/openid-configuration</strong>
*
* <p>
* When more than one path is provided then query all the three (3) endpoints for metadata configuration
* as per <a href="https://tools.ietf.org/html/rfc8414#section-5">Section 5</a> of RF 8414 specification
* and the URLs would look as follow
* </p>
*
* <ol>
* <li>
* <strong>{host}/.well-known/openid-configuration/issuer1</strong> - OpenID as per RFC 8414
* </li>
* <li>
* <strong>{host}/issuer1/.well-known/openid-configuration</strong> - OpenID Connect 1.0 Discovery Compatibility as per RFC 8414
* </li>
* <li>
* <strong>/.well-known/oauth-authorization-server/issuer1</strong> - OAuth2 Authorization Server Metadata as per RFC 8414
* </li>
* </ol>
*
* @param issuer
* @param paths
* @throws IllegalArgumentException throws exception if paths length is not 1 or 3, 1 for <strong>fromOidcLocationIssuer</strong>
* and 3 for the newly introduced <strong>fromIssuerLocation</strong> to support querying 3 different metadata provider endpoints
* @return Map<ProviderType, String> key-value map of provider with its request url
*/
private static Map<ProviderType, String> buildIssuerConfigurationUrls(String issuer, String... paths) {
Assert.isTrue(paths.length != 1 || paths.length != 3, "paths length can either be 1 or 3");
Map<ProviderType, String> 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<com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod> 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 @@ public final class ClientRegistrations {
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<String> getScopes(OIDCProviderMetadata metadata) {
private static List<String> getScopes(AuthorizationServerMetadata metadata) {
Scope scope = metadata.getScopes();
if (scope == null) {
// If null, default to "openid" which must be supported
@ -133,15 +317,18 @@ public final class ClientRegistrations {
}
}
private static OIDCProviderMetadata parse(String body) {
private static <T> T parse(String body, ThrowingFunction<String, T, ParseException> parser) {
try {
return OIDCProviderMetadata.parse(body);
}
catch (ParseException e) {
return parser.apply(body);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
private interface ThrowingFunction<S, T, E extends Throwable> {
T apply(S src) throws E;
}
private ClientRegistrations() {}
}

@ -18,8 +18,12 @@ package org.springframework.security.oauth2.client.registration;
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 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* @author Rob Winch
* @author Rafiullah Hamedy
* @since 5.1
*/
public class ClientRegistrationsTest {
@ -122,7 +127,34 @@ public class ClientRegistrationsTest {
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
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest">OpenID Provider
* Configuration Request</a> as highlighted <a href="https://tools.ietf.org/html/rfc8414#section-5">
* Compatibility Notes</a> of <a href="https://tools.ietf.org/html/rfc8414">RFC 8414</a> 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 class ClientRegistrationsTest {
"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 class ClientRegistrationsTest {
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 class ClientRegistrationsTest {
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 class ClientRegistrationsTest {
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 class ClientRegistrationsTest {
.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 class ClientRegistrationsTest {
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 class ClientRegistrationsTest {
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 class ClientRegistrationsTest {
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 class ClientRegistrationsTest {
.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 class ClientRegistrationsTest {
.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 @@ public class ClientRegistrationsTest {
.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 <a href="https://tools.ietf.org/html/rfc8414#section-5">Section 5</a> 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);
}
}

@ -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
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig">OpenID Provider Configuration</a>.
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig">OpenID Provider Configuration</a> or
* <a href="https://tools.ietf.org/html/rfc8414#section-3.1">Authorization Server Metadata Request</a> 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<String, Object> openidConfiguration = getOpenidConfiguration(oidcIssuerLocation);
Map<String, Object> 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.
*
* <ol>
* <li>
* <strong>{host}/.well-known/openid-configuration/issuer1</strong> - OpenID Provider Configuration Request based on
* <a href="https://tools.ietf.org/html/rfc8414#section-5">Section 5</a> of <a href="https://tools.ietf.org/html/rfc8414">
* RFC 8414 Specification</a>
* </li>
* <li>
* <strong>{host}/issuer1/.well-known/openid-configuration</strong> - OpenID v1 Discovery endpoint based on
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest">OpenID Provider
* Configuration Request</a> with backward compatibility highlighted on <a href="https://tools.ietf.org/html/rfc8414#section-5">
* Section 5</a> of RF 8414
* </li>
* <li>
* <strong>{host}/.well-known/oauth-authorization-server/issuer1</strong> - OAuth2 Authorization Server Metadata based on
* <a href="https://tools.ietf.org/html/rfc8414#section-3.1">Section 3.1</a> of RFC 8414
* </li>
* </ol>
*
* @param issuer
* @return a {@link JwtDecoder} that is initialized using
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">
* OpenID Provider Configuration Response</a> or <a href="https://tools.ietf.org/html/rfc8414#section-3.2">
* Authorization Server Metadata Response</a> depending on provided issuer
*/
public static JwtDecoder fromIssuerLocation(String issuer) {
Map<String, Object> configuration = getIssuerConfiguration(issuer, OIDC_METADATA_PATH, OAUTH2_METADATA_PATH);
return withProviderConfiguration(configuration, issuer);
}
/**
* Validate provided issuer and build {@link JwtDecoder} from
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">OpenID Provider
* Configuration Response</a> and <a href="https://tools.ietf.org/html/rfc8414#section-3.2">Authorization Server Metadata
* Response</a>.
*
* @param configuration
* @param issuer
* @return {@link JwtDecoder}
*/
private static JwtDecoder withProviderConfiguration(Map<String, Object> 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<Jwt> jwtValidator =
JwtValidators.createDefaultWithIssuer(oidcIssuerLocation);
NimbusJwtDecoder jwtDecoder = withJwkSetUri(openidConfiguration.get("jwks_uri").toString()).build();
OAuth2TokenValidator<Jwt> jwtValidator = JwtValidators.createDefaultWithIssuer(issuer);
NimbusJwtDecoder jwtDecoder = withJwkSetUri(configuration.get("jwks_uri").toString()).build();
jwtDecoder.setJwtValidator(jwtValidator);
return jwtDecoder;
}
private static Map<String, Object> getOpenidConfiguration(String issuer) {
ParameterizedTypeReference<Map<String, Object>> typeReference = new ParameterizedTypeReference<Map<String, Object>>() {};
/**
* When the length of paths is equal to one (1) then it's a request for OpenId v1 discovery endpoint
* hence a request to <strong>{host}/issuer1/.well-known/openid-configuration</strong> is being made.
* Otherwise, all three (3) discovery endpoint are queried one after another depending one after another
* until one endpoint returns successful response.
*
* @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<String, Object> - Configuration Metadata from the given issuer
*/
private static Map<String, Object> getIssuerConfiguration(String issuer, String... paths) {
Assert.notEmpty(paths, "paths cannot be empty or null.");
URI[] uris = buildIssuerConfigurationUrls(issuer, paths);
for (URI uri: uris) {
Map<String, Object> response = makeIssuerRequest(uri);
if (response != null) {
return response;
}
}
throw new IllegalArgumentException("Unable to resolve Configuration with the provided Issuer of \"" + issuer + "\"");
}
/**
* Make a rest API request to the given URI that is either of OpenId, OpenId Connection Discovery 1.0 or OAuth2 and if
* successful then return the Response as key-value map. If the request is not successful then the thrown exception is
* caught and null is returned indicating no provider available.
*
* @param uri
* @return Map<String, Object> Configuration Metadata of the given provider if not null
*/
private static Map<String, Object> makeIssuerRequest(URI uri) {
RestTemplate rest = new RestTemplate();
ParameterizedTypeReference<Map<String, Object>> typeReference = new ParameterizedTypeReference<Map<String, Object>>() {};
try {
URI uri = UriComponentsBuilder.fromUriString(issuer + "/.well-known/openid-configuration")
.build()
.toUri();
RequestEntity<Void> request = RequestEntity.get(uri).build();
return rest.exchange(request, typeReference).getBody();
} catch(RuntimeException e) {
throw new IllegalArgumentException("Unable to resolve the OpenID Configuration with the provided Issuer of " +
"\"" + issuer + "\"", e);
} catch(RestClientException ex) {
return null;
} catch(RuntimeException ex) {
return null;
}
}
/**
* When invoked with a path then make a
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest">
* OpenID Provider Configuration Request</a> by querying the OpenId Connection Discovery 1.0 endpoint
* and the url would look as follow <strong>{host}/issuer1/.well-known/openid-configuration</strong>
*
* <p>
* When more than one path is provided then query all the three (3) endpoints for metadata configuration
* as per <a href="https://tools.ietf.org/html/rfc8414#section-5">Section 5</a> of RF 8414 specification
* and the urls would look as follow
* </p>
*
* <ol>
* <li>
* <strong>{host}/.well-known/openid-configuration/issuer1</strong> - OpenID as per RFC 8414
* </li>
* <li>
* <strong>{host}/issuer1/.well-known/openid-configuration</strong> - OpenID Connect 1.0 Discovery Compatibility as per RFC 8414
* </li>
* <li>
* <strong>{host}/.well-known/oauth-authorization-server/issuer1</strong> - OAuth2 Authorization Server Metadata as per RFC 8414
* </li>
* </ol>
*
* @param issuer
* @param paths
* @throws IllegalArgumentException throws exception if paths length is not 1 or 3, 1 for <strong>fromOidcLocationIssuer</strong>
* and 3 for the newly introduced <strong>fromIssuerLocation</strong> to support querying 3 different metadata provider endpoints
* @return URI[] URIs for to <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">
* OpenID Provider Configuration Response</a> and <a href="https://tools.ietf.org/html/rfc8414#section-3.2">
* Authorization Server Metadata Response</a>
*/
private static URI[] buildIssuerConfigurationUrls(String issuer, String... paths) {
Assert.isTrue(paths.length != 1 || paths.length != 3, "paths length can either be 1 or 3");
URI issuerURI = URI.create(issuer);
if (paths.length == 1) {
return new URI[] {
UriComponentsBuilder.fromUri(issuerURI).replacePath(issuerURI.getPath() + paths[0]).build().toUri()
};
} else {
return new URI[] {
UriComponentsBuilder.fromUri(issuerURI).replacePath(paths[0] + issuerURI.getPath()).build().toUri(),
UriComponentsBuilder.fromUri(issuerURI).replacePath(issuerURI.getPath() + paths[0]).build().toUri(),
UriComponentsBuilder.fromUri(issuerURI).replacePath(paths[1] + issuerURI.getPath()).build().toUri()
};
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,8 +15,11 @@
*/
package org.springframework.security.oauth2.jwt;
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;
@ -31,6 +34,7 @@ import static org.assertj.core.api.Assertions.assertThatCode;
* Tests for {@link JwtDecoders}
*
* @author Josh Cummings
* @author Rafiullah Hamedy
*/
public class JwtDecodersTests {
/**
@ -65,14 +69,12 @@ public class JwtDecodersTests {
private MockWebServer server;
private String issuer;
private String jwkSetUri;
@Before
public void setup() throws Exception {
this.server = new MockWebServer();
this.server.start();
this.issuer = createIssuerFromServer();
this.jwkSetUri = this.issuer + "/.well-known/jwks.json";
}
@After
@ -82,48 +84,125 @@ public class JwtDecodersTests {
@Test
public void issuerWhenResponseIsTypicalThenReturnedDecoderValidatesIssuer() {
prepareOpenIdConfigurationResponse();
prepareConfigurationResponse();
this.server.enqueue(new MockResponse().setBody(JWK_SET));
JwtDecoder decoder = JwtDecoders.fromOidcIssuerLocation(this.issuer);
assertResponseIsTypicalThenReturnedDecoderValidatesIssuer(decoder);
}
@Test
public void issuerWhenOidcFallbackResponseIsTypicalThenReturnedDecoderValidatesIssuer() {
prepareConfigurationResponseForOidcFallback("issuer1", null);
JwtDecoder decoder = JwtDecoders.fromIssuerLocation(this.issuer);
assertResponseIsTypicalThenReturnedDecoderValidatesIssuer(decoder);
}
@Test
public void issuerWhenOauth2ResponseIsTypicalThenReturnedDecoderValidatesIssuer() {
prepareConfigurationResponseForOauth2("issuer1", null);
JwtDecoder decoder = JwtDecoders.fromIssuerLocation(this.issuer);
assertResponseIsTypicalThenReturnedDecoderValidatesIssuer(decoder);
}
private void assertResponseIsTypicalThenReturnedDecoderValidatesIssuer(JwtDecoder decoder) {
assertThatCode(() -> decoder.decode(ISSUER_MISMATCH))
.isInstanceOf(JwtValidationException.class)
.hasMessageContaining("This iss claim is not equal to the configured issuer");
.isInstanceOf(JwtValidationException.class)
.hasMessageContaining("This iss claim is not equal to the configured issuer");
}
@Test
public void issuerWhenContainsTrailingSlashThenSuccess() {
prepareOpenIdConfigurationResponse();
prepareConfigurationResponse();
this.server.enqueue(new MockResponse().setBody(JWK_SET));
assertThat(JwtDecoders.fromOidcIssuerLocation(this.issuer)).isNotNull();
assertThat(this.issuer).endsWith("/");
}
@Test
public void issuerWhenResponseIsNonCompliantThenThrowsRuntimeException() {
prepareOpenIdConfigurationResponse("{ \"missing_required_keys\" : \"and_values\" }");
public void issuerWhenOidcFallbackContainsTrailingSlashThenSuccess() {
prepareConfigurationResponse();
this.server.enqueue(new MockResponse().setBody(JWK_SET));
assertThat(JwtDecoders.fromIssuerLocation(this.issuer)).isNotNull();
assertThat(this.issuer).endsWith("/");
}
@Test
public void issuerWhenOauth2ContainsTrailingSlashThenSuccess() {
prepareConfigurationResponseForOauth2("", null);
assertThat(JwtDecoders.fromIssuerLocation(this.issuer)).isNotNull();
assertThat(this.issuer).endsWith("/");
}
@Test
public void issuerWhenResponseIsNonCompliantThenThrowsRuntimeException() {
prepareConfigurationResponse("{ \"missing_required_keys\" : \"and_values\" }");
assertThatCode(() -> JwtDecoders.fromOidcIssuerLocation(this.issuer))
.isInstanceOf(RuntimeException.class);
}
@Test
public void issuerWhenOidcFallbackResponseIsNonCompliantThenThrowsRuntimeException() {
prepareConfigurationResponseForOidcFallback("", "{ \"missing_required_keys\" : \"and_values\" }");
assertThatCode(() -> JwtDecoders.fromIssuerLocation(this.issuer))
.isInstanceOf(RuntimeException.class);
}
@Test
public void issuerWhenOauth2ResponseIsNonCompliantThenThrowsRuntimeException() {
prepareConfigurationResponseForOauth2("", "{ \"missing_required_keys\" : \"and_values\" }");
System.out.println("this.issuer = " + this.issuer);
assertThatCode(() -> JwtDecoders.fromIssuerLocation(this.issuer))
.isInstanceOf(RuntimeException.class);
}
@Test
public void issuerWhenResponseIsMalformedThenThrowsRuntimeException() {
prepareOpenIdConfigurationResponse("malformed");
prepareConfigurationResponse("malformed");
assertThatCode(() -> JwtDecoders.fromOidcIssuerLocation(this.issuer))
.isInstanceOf(RuntimeException.class);
}
@Test
public void issuerWhenRespondingIssuerMismatchesRequestedIssuerThenThrowsIllegalStateException() {
prepareOpenIdConfigurationResponse();
public void issuerWhenOidcFallbackResponseIsMalformedThenThrowsRuntimeException() {
prepareConfigurationResponseForOidcFallback("", "malformed");
assertThatCode(() -> JwtDecoders.fromOidcIssuerLocation(this.issuer))
.isInstanceOf(RuntimeException.class);
}
@Test
public void issuerWhenOauth2ResponseIsMalformedThenThrowsRuntimeException() {
prepareConfigurationResponseForOauth2("", "malformed");
assertThatCode(() -> JwtDecoders.fromIssuerLocation(this.issuer))
.isInstanceOf(RuntimeException.class);
}
@Test
public void issuerWhenRespondingIssuerMismatchesRequestedIssuerThenThrowsIllegalStateException() {
prepareConfigurationResponse();
assertThatCode(() -> JwtDecoders.fromOidcIssuerLocation(this.issuer + "/wrong"))
.isInstanceOf(IllegalStateException.class);
}
@Test
public void issuerWhenOidcFallbackRespondingIssuerMismatchesRequestedIssuerThenThrowsIllegalStateException() {
prepareConfigurationResponseForOidcFallback("", null);
assertThatCode(() -> JwtDecoders.fromIssuerLocation(this.issuer + "/wrong"))
.isInstanceOf(IllegalStateException.class);
}
@Test
public void issuerWhenOauth2RespondingIssuerMismatchesRequestedIssuerThenThrowsIllegalStateException() {
prepareConfigurationResponseForOauth2("", null);
assertThatCode(() -> JwtDecoders.fromIssuerLocation(this.issuer + "/wrong"))
.isInstanceOf(IllegalStateException.class);
}
@Test
public void issuerWhenRequestedIssuerIsUnresponsiveThenThrowsIllegalArgumentException()
throws Exception {
@ -134,18 +213,90 @@ public class JwtDecodersTests {
.isInstanceOf(IllegalArgumentException.class);
}
private void prepareOpenIdConfigurationResponse() {
String body = String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer, this.issuer);
prepareOpenIdConfigurationResponse(body);
@Test
public void issuerWhenOidcFallbackRequestedIssuerIsUnresponsiveThenThrowsIllegalArgumentException()
throws Exception {
this.server.shutdown();
assertThatCode(() -> JwtDecoders.fromIssuerLocation("https://issuer"))
.isInstanceOf(IllegalArgumentException.class);
}
private void prepareOpenIdConfigurationResponse(String body) {
private void prepareConfigurationResponse() {
String body = String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer, this.issuer);
prepareConfigurationResponse(body);
}
private void prepareConfigurationResponse(String body) {
MockResponse mockResponse = new MockResponse()
.setBody(body)
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
this.server.enqueue(mockResponse);
}
/**
* A mock server that responds to API requests for OIDC (i.e. openid-configuration) metadata endpoint when the
* request path matches the switch case.
*
* @param path
* @param body
*/
private void prepareConfigurationResponseForOidcFallback(String path, String body) {
this.issuer = this.server.url(path).toString();
String responseBody = body != null ? body : String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer, this.issuer);
final Dispatcher dispatcher = new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
switch(request.getPath()) {
case "/issuer1/.well-known/openid-configuration":
case "/wrong/.well-known/openid-configuration":
return buildSuccessMockResponse(responseBody);
case "/issuer1/.well-known/jwks.json":
return buildSuccessMockResponse(JWK_SET);
}
return new MockResponse().setResponseCode(404);
}
};
this.server.setDispatcher(dispatcher);
}
/**
* A mock server that responds to API requests for OAuth2 (oauth-authorization-server) metadata endpoint when the
* request path matches the switch case.
*
* @param path
* @param body
*/
private void prepareConfigurationResponseForOauth2(String path, String body) {
this.issuer = this.server.url(path).toString();
final String responseBody = body != null ? body : String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer, this.issuer);
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/wrong":
case "/.well-known/oauth-authorization-server/":
return buildSuccessMockResponse(responseBody);
case "/issuer1/.well-known/jwks.json":
return buildSuccessMockResponse(JWK_SET);
}
return new MockResponse().setResponseCode(404);
}
};
this.server.setDispatcher(dispatcher);
}
private MockResponse buildSuccessMockResponse(String body) {
return new MockResponse().setResponseCode(200)
.setBody(body)
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
}
private String createIssuerFromServer() {
return this.server.url("").toString();
}