From 1739ef8d3c75871367e8bf9a0daf46547abf6d45 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 10 Jun 2019 10:18:21 -0600 Subject: [PATCH] Polish ClientRegistrations, (Reactive)JwtDecoders Simplifed some of the branching logic in the implementations. Updated the JavaDocs. Simplified some of the test support. Issue: gh-6500 --- .../registration/ClientRegistrations.java | 258 ++++++------------ .../registration/ClientRegistrationsTest.java | 56 ++-- .../security/oauth2/jwt/JwtDecoders.java | 195 +++++-------- .../oauth2/jwt/ReactiveJwtDecoders.java | 127 +++++++-- .../security/oauth2/jwt/JwtDecodersTests.java | 196 +++++++------ .../oauth2/jwt/ReactiveJwtDecodersTests.java | 173 ++++++++++-- 6 files changed, 540 insertions(+), 465 deletions(-) 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 9accaa924b..846d68867c 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 @@ -16,26 +16,31 @@ package org.springframework.security.oauth2.client.registration; +import java.net.URI; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + 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 net.minidev.json.JSONObject; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.RequestEntity; 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.HttpClientErrorException; 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 @@ -49,11 +54,10 @@ import java.util.Map; */ 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; - } + private static final String OAUTH_METADATA_PATH = "/.well-known/oauth-authorization-server"; + private static final RestTemplate rest = new RestTemplate(); + private static final ParameterizedTypeReference> typeReference = + new ParameterizedTypeReference>() {}; /** * Creates a {@link ClientRegistration.Builder} using the provided @@ -63,12 +67,6 @@ 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 @@ -88,42 +86,41 @@ public final class ClientRegistrations { * @return a {@link ClientRegistration.Builder} that was initialized by the OpenID Provider Configuration. */ public static ClientRegistration.Builder fromOidcIssuerLocation(String issuer) { - Map configuration = getIssuerConfiguration(issuer, OIDC_METADATA_PATH); - OIDCProviderMetadata metadata = parse(configuration.get(ProviderType.OIDCV1), OIDCProviderMetadata::parse); + Assert.hasText(issuer, "issuer cannot be empty"); + Map configuration = getConfiguration(issuer, oidc(URI.create(issuer))); + OIDCProviderMetadata metadata = parse(configuration, 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 + * Creates a {@link ClientRegistration.Builder} using the provided + * Issuer by querying + * three different discovery endpoints serially, using the values in the first successful response to + * initialize. If an endpoint returns anything other than a 200 or a 4xx, the method will exit without + * attempting subsequent endpoints. + * + * The three endpoints are computed as follows, given that the {@code issuer} is composed of a {@code host} + * and a {@code path}: * *

    *
  1. - * 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. + * {@code host/.well-known/openid-configuration/path}, as defined in + * RFC 8414's Compatibility Notes. *
  2. *
  3. - * 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 + * {@code issuer/.well-known/openid-configuration}, as defined in + * + * OpenID Provider Configuration. *
  4. *
  5. - * 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. + * {@code host/.well-known/oauth-authorization-server/path}, as defined in + * Authorization Server Metadata Request. *
  6. *
* - * - * 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. + * Note that the second endpoint is the equivalent of calling + * {@link ClientRegistrations#fromOidcIssuerLocation(String)}. * *

* Example usage: @@ -136,22 +133,63 @@ public final class ClientRegistrations { * * * @param issuer - * @return a {@link ClientRegistration.Builder} that was initialized by the Authorization Sever Metadata Provider + * @return a {@link ClientRegistration.Builder} that was initialized by one of the described endpoints */ public static ClientRegistration.Builder fromIssuerLocation(String issuer) { - Map configuration = getIssuerConfiguration(issuer, OIDC_METADATA_PATH, OAUTH2_METADATA_PATH); + Assert.hasText(issuer, "issuer cannot be empty"); + URI uri = URI.create(issuer); + Map configuration = getConfiguration(issuer, oidc(uri), oidcRfc8414(uri), oauth(uri)); + AuthorizationServerMetadata metadata = parse(configuration, AuthorizationServerMetadata::parse); + ClientRegistration.Builder builder = withProviderConfiguration(metadata, issuer); + return Optional.ofNullable((String) configuration.get("userinfo_endpoint")) + .map(builder::userInfoUri).orElse(builder); + } - 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 URI oidc(URI issuer) { + return UriComponentsBuilder.fromUri(issuer) + .replacePath(issuer.getPath() + OIDC_METADATA_PATH).build(Collections.emptyMap()); + } + + private static URI oidcRfc8414(URI issuer) { + return UriComponentsBuilder.fromUri(issuer) + .replacePath(OIDC_METADATA_PATH + issuer.getPath()).build(Collections.emptyMap()); + } + + private static URI oauth(URI issuer) { + return UriComponentsBuilder.fromUri(issuer) + .replacePath(OAUTH_METADATA_PATH + issuer.getPath()).build(Collections.emptyMap()); + } + + private static Map getConfiguration(String issuer, URI... uris) { + String errorMessage = "Unable to resolve Configuration with the provided Issuer of \"" + issuer + "\""; + for (URI uri : uris) { + try { + RequestEntity request = RequestEntity.get(uri).build(); + return rest.exchange(request, typeReference).getBody(); + } catch (HttpClientErrorException e) { + if (!e.getStatusCode().is4xxClientError()) { + throw e; + } + // else try another endpoint + } catch (RuntimeException e) { + throw new IllegalArgumentException(errorMessage, e); + } } + throw new IllegalArgumentException(errorMessage); + } + + private static T parse(Map body, + ThrowingFunction parser) { + + try { + return parser.apply(new JSONObject(body)); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + + private interface ThrowingFunction { + T apply(S src) throws E; } private static ClientRegistration.Builder withProviderConfiguration(AuthorizationServerMetadata metadata, String issuer) { @@ -185,112 +223,6 @@ public final class ClientRegistrations { .clientName(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(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 - *

- * - *
    - *
  1. - * {host}/.well-known/openid-configuration/issuer1 - OpenID as per RFC 8414 - *
  2. - *
  3. - * {host}/issuer1/.well-known/openid-configuration - OpenID Connect 1.0 Discovery Compatibility as per RFC 8414 - *
  4. - *
  5. - * /.well-known/oauth-authorization-server/issuer1 - OAuth2 Authorization Server Metadata as per RFC 8414 - *
  6. - *
- * - * @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) { if (metadataAuthMethods == null || metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) { @@ -317,18 +249,6 @@ public final class ClientRegistrations { } } - private static T parse(String body, ThrowingFunction parser) { - try { - 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 26394d04ee..903e26e02c 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 @@ -16,25 +16,24 @@ package org.springframework.security.oauth2.client.registration; +import java.util.Arrays; +import java.util.Map; + 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; + import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; -import java.util.Arrays; -import java.util.Map; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -147,8 +146,8 @@ public class ClientRegistrationsTest { } @Test - public void issuerWhenOauth2AllInformationThenSuccess() throws Exception { - ClientRegistration registration = registrationOauth2("", null).build(); + public void issuerWhenOAuth2AllInformationThenSuccess() throws Exception { + ClientRegistration registration = registrationOAuth2("", null).build(); ClientRegistration.ProviderDetails provider = registration.getProviderDetails(); assertIssuerMetadata(registration, provider); } @@ -182,8 +181,8 @@ public class ClientRegistrationsTest { } @Test - public void issuerWhenOauth2ContainsTrailingSlashThenSuccess() throws Exception { - assertThat(registrationOauth2("", null)).isNotNull(); + public void issuerWhenOAuth2ContainsTrailingSlashThenSuccess() throws Exception { + assertThat(registrationOAuth2("", null)).isNotNull(); assertThat(this.issuer).endsWith("/"); } @@ -213,10 +212,10 @@ public class ClientRegistrationsTest { } @Test - public void issuerWhenOauth2ScopesNullThenScopesDefaulted() throws Exception { + public void issuerWhenOAuth2ScopesNullThenScopesDefaulted() throws Exception { this.response.remove("scopes_supported"); - ClientRegistration registration = registrationOauth2("", null).build(); + ClientRegistration registration = registrationOAuth2("", null).build(); assertThat(registration.getScopes()).containsOnly("openid"); } @@ -232,10 +231,10 @@ public class ClientRegistrationsTest { } @Test - public void issuerWhenOauth2GrantTypesSupportedNullThenDefaulted() throws Exception { + public void issuerWhenOAuth2GrantTypesSupportedNullThenDefaulted() throws Exception { this.response.remove("grant_types_supported"); - ClientRegistration registration = registrationOauth2("", null).build(); + ClientRegistration registration = registrationOAuth2("", null).build(); assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); } @@ -254,10 +253,10 @@ public class ClientRegistrationsTest { } @Test - public void issuerWhenOauth2GrantTypesSupportedInvalidThenException() throws Exception { + public void issuerWhenOAuth2GrantTypesSupportedInvalidThenException() throws Exception { this.response.put("grant_types_supported", Arrays.asList("implicit")); - assertThatThrownBy(() -> registrationOauth2("", null)) + assertThatThrownBy(() -> registrationOAuth2("", null)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Only AuthorizationGrantType.AUTHORIZATION_CODE is supported. The issuer \"" + this.issuer + "\" returned a configuration of [implicit]"); } @@ -272,10 +271,10 @@ public class ClientRegistrationsTest { } @Test - public void issuerWhenOauth2TokenEndpointAuthMethodsNullThenDefaulted() throws Exception { + public void issuerWhenOAuth2TokenEndpointAuthMethodsNullThenDefaulted() throws Exception { this.response.remove("token_endpoint_auth_methods_supported"); - ClientRegistration registration = registrationOauth2("", null).build(); + ClientRegistration registration = registrationOAuth2("", null).build(); assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); } @@ -290,10 +289,10 @@ public class ClientRegistrationsTest { } @Test - public void issuerWhenOauth2TokenEndpointAuthMethodsPostThenMethodIsPost() throws Exception { + public void issuerWhenOAuth2TokenEndpointAuthMethodsPostThenMethodIsPost() throws Exception { this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_post")); - ClientRegistration registration = registrationOauth2("", null).build(); + ClientRegistration registration = registrationOAuth2("", null).build(); assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.POST); } @@ -308,10 +307,10 @@ public class ClientRegistrationsTest { } @Test - public void issuerWhenOauth2TokenEndpointAuthMethodsNoneThenMethodIsNone() throws Exception { + public void issuerWhenOAuth2TokenEndpointAuthMethodsNoneThenMethodIsNone() throws Exception { this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("none")); - ClientRegistration registration = registrationOauth2("", null).build(); + ClientRegistration registration = registrationOAuth2("", null).build(); assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.NONE); } @@ -330,24 +329,24 @@ public class ClientRegistrationsTest { } @Test - public void issuerWhenOauth2TokenEndpointAuthMethodsInvalidThenException() throws Exception { + public void issuerWhenOAuth2TokenEndpointAuthMethodsInvalidThenException() throws Exception { this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("tls_client_auth")); - assertThatThrownBy(() -> registrationOauth2("", null)) + 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() { + public void issuerWhenOAuth2EmptyStringThenMeaningfulErrorMessage() { assertThatThrownBy(() -> ClientRegistrations.fromIssuerLocation("")) - .hasMessageContaining("Unable to resolve Configuration with the provided Issuer of \"\""); + .hasMessageContaining("issuer cannot be empty"); } @Test public void issuerWhenEmptyStringThenMeaningfulErrorMessage() { assertThatThrownBy(() -> ClientRegistrations.fromOidcIssuerLocation("")) - .hasMessageContaining("Unable to resolve Configuration with the provided Issuer of \"\""); + .hasMessageContaining("issuer cannot be empty"); } @Test @@ -363,7 +362,7 @@ public class ClientRegistrationsTest { } @Test - public void issuerWhenOauth2ConfigurationDoesNotMatchThenMeaningfulErrorMessage() throws Exception { + public void issuerWhenOAuth2ConfigurationDoesNotMatchThenMeaningfulErrorMessage() throws Exception { this.issuer = createIssuerFromServer(""); String body = this.mapper.writeValueAsString(this.response); MockResponse mockResponse = new MockResponse() @@ -388,7 +387,7 @@ public class ClientRegistrationsTest { .clientSecret("client-secret"); } - private ClientRegistration.Builder registrationOauth2(String path, String body) throws Exception { + 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(); @@ -435,7 +434,6 @@ public class ClientRegistrationsTest { 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/": 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 2f72da63c6..685f5fb29a 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,19 +15,21 @@ */ package org.springframework.security.oauth2.jwt; -import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSetUri; - import java.net.URI; +import java.util.Collections; import java.util.Map; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; import org.springframework.security.oauth2.core.OAuth2TokenValidator; import org.springframework.util.Assert; -import org.springframework.web.client.RestClientException; +import org.springframework.web.client.HttpClientErrorException; 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 or @@ -40,7 +42,10 @@ import org.springframework.web.util.UriComponentsBuilder; */ 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"; + private static final String OAUTH_METADATA_PATH = "/.well-known/oauth-authorization-server"; + private static final RestTemplate rest = new RestTemplate(); + private static final ParameterizedTypeReference> typeReference = + new ParameterizedTypeReference>() {}; /** * Creates a {@link JwtDecoder} using the provided @@ -54,43 +59,84 @@ public final class JwtDecoders { * @return a {@link JwtDecoder} that was initialized by the OpenID Provider Configuration. */ public static JwtDecoder fromOidcIssuerLocation(String oidcIssuerLocation) { - Map configuration = getIssuerConfiguration(oidcIssuerLocation, OIDC_METADATA_PATH); + Assert.hasText(oidcIssuerLocation, "oidcIssuerLocation cannot be empty"); + Map configuration = getConfiguration(oidcIssuerLocation, oidc(URI.create(oidcIssuerLocation))); 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. + * Creates a {@link JwtDecoder} using the provided + * Issuer by querying + * three different discovery endpoints serially, using the values in the first successful response to + * initialize. If an endpoint returns anything other than a 200 or a 4xx, the method will exit without + * attempting subsequent endpoints. + * + * The three endpoints are computed as follows, given that the {@code issuer} is composed of a {@code host} + * and a {@code path}: * *
    - *
  1. - * {host}/.well-known/openid-configuration/issuer1 - OpenID Provider Configuration Request based on - * Section 5 of - * RFC 8414 Specification - *
  2. - *
  3. - * {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 - *
  4. - *
  5. - * {host}/.well-known/oauth-authorization-server/issuer1 - OAuth2 Authorization Server Metadata based on - * Section 3.1 of RFC 8414 - *
  6. + *
  7. + * {@code host/.well-known/openid-configuration/path}, as defined in + * RFC 8414's Compatibility Notes. + *
  8. + *
  9. + * {@code issuer/.well-known/openid-configuration}, as defined in + * + * OpenID Provider Configuration. + *
  10. + *
  11. + * {@code host/.well-known/oauth-authorization-server/path}, as defined in + * Authorization Server Metadata Request. + *
  12. *
* + * Note that the second endpoint is the equivalent of calling + * {@link JwtDecoders#fromOidcIssuerLocation(String)} + * * @param issuer - * @return a {@link JwtDecoder} that is initialized using - * - * OpenID Provider Configuration Response or - * Authorization Server Metadata Response depending on provided issuer + * @return a {@link JwtDecoder} that was initialized by one of the described endpoints */ public static JwtDecoder fromIssuerLocation(String issuer) { - Map configuration = getIssuerConfiguration(issuer, OIDC_METADATA_PATH, OAUTH2_METADATA_PATH); + Assert.hasText(issuer, "issuer cannot be empty"); + URI uri = URI.create(issuer); + Map configuration = getConfiguration(issuer, oidc(uri), oidcRfc8414(uri), oauth(uri)); return withProviderConfiguration(configuration, issuer); } + private static URI oidc(URI issuer) { + return UriComponentsBuilder.fromUri(issuer) + .replacePath(issuer.getPath() + OIDC_METADATA_PATH).build(Collections.emptyMap()); + } + + private static URI oidcRfc8414(URI issuer) { + return UriComponentsBuilder.fromUri(issuer) + .replacePath(OIDC_METADATA_PATH + issuer.getPath()).build(Collections.emptyMap()); + } + + private static URI oauth(URI issuer) { + return UriComponentsBuilder.fromUri(issuer) + .replacePath(OAUTH_METADATA_PATH + issuer.getPath()).build(Collections.emptyMap()); + } + + private static Map getConfiguration(String issuer, URI... uris) { + String errorMessage = "Unable to resolve the Configuration with the provided Issuer of " + + "\"" + issuer + "\""; + for (URI uri : uris) { + try { + RequestEntity request = RequestEntity.get(uri).build(); + ResponseEntity> response = rest.exchange(request, typeReference); + return response.getBody(); + } catch (RuntimeException e) { + if (!(e instanceof HttpClientErrorException && + ((HttpClientErrorException) e).getStatusCode().is4xxClientError())) { + throw new IllegalArgumentException(errorMessage, e); + } + // else try another endpoint + } + } + throw new IllegalArgumentException(errorMessage); + } + /** * Validate provided issuer and build {@link JwtDecoder} from * OpenID Provider @@ -118,100 +164,5 @@ public final class JwtDecoders { return jwtDecoder; } - /** - * When the length of paths is equal to one (1) then it's a request for OpenId v1 discovery endpoint - * hence a request to {host}/issuer1/.well-known/openid-configuration 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 - Configuration Metadata from the given issuer - */ - private static Map getIssuerConfiguration(String issuer, String... paths) { - Assert.notEmpty(paths, "paths cannot be empty or null."); - - URI[] uris = buildIssuerConfigurationUrls(issuer, paths); - for (URI uri: uris) { - Map 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 Configuration Metadata of the given provider if not null - */ - private static Map makeIssuerRequest(URI uri) { - RestTemplate rest = new RestTemplate(); - ParameterizedTypeReference> typeReference = new ParameterizedTypeReference>() {}; - try { - RequestEntity request = RequestEntity.get(uri).build(); - return rest.exchange(request, typeReference).getBody(); - } catch(RestClientException ex) { - return null; - } 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 - *

- * - *
    - *
  1. - * {host}/.well-known/openid-configuration/issuer1 - OpenID as per RFC 8414 - *
  2. - *
  3. - * {host}/issuer1/.well-known/openid-configuration - OpenID Connect 1.0 Discovery Compatibility as per RFC 8414 - *
  4. - *
  5. - * {host}/.well-known/oauth-authorization-server/issuer1 - OAuth2 Authorization Server Metadata as per RFC 8414 - *
  6. - *
- * - * @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 URI[] URIs for to - * OpenID Provider Configuration Response and - * Authorization Server Metadata Response - */ - 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() - }; - } - } - private JwtDecoders() {} } diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoders.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoders.java index f3788ac06f..df7e015841 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoders.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoders.java @@ -15,14 +15,20 @@ */ package org.springframework.security.oauth2.jwt; +import java.net.URI; +import java.util.Collections; +import java.util.Map; + import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.util.Assert; +import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; -import java.net.URI; -import java.util.Map; +import static org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.withJwkSetUri; /** * Allows creating a {@link ReactiveJwtDecoder} from an @@ -32,6 +38,11 @@ import java.util.Map; * @since 5.1 */ public final class ReactiveJwtDecoders { + private static final String OIDC_METADATA_PATH = "/.well-known/openid-configuration"; + private static final String OAUTH_METADATA_PATH = "/.well-known/oauth-authorization-server"; + private static final RestTemplate rest = new RestTemplate(); + private static final ParameterizedTypeReference> typeReference = + new ParameterizedTypeReference>() {}; /** * Creates a {@link ReactiveJwtDecoder} using the provided @@ -45,40 +56,100 @@ public final class ReactiveJwtDecoders { * @return a {@link ReactiveJwtDecoder} that was initialized by the OpenID Provider Configuration. */ public static ReactiveJwtDecoder fromOidcIssuerLocation(String oidcIssuerLocation) { - Map openidConfiguration = getOpenidConfiguration(oidcIssuerLocation); + Assert.hasText(oidcIssuerLocation, "oidcIssuerLocation cannot be empty"); + Map configuration = getConfiguration(oidcIssuerLocation, oidc(URI.create(oidcIssuerLocation))); + return withProviderConfiguration(configuration, oidcIssuerLocation); + } + + /** + * Creates a {@link ReactiveJwtDecoder} using the provided + * Issuer by querying + * three different discovery endpoints serially, using the values in the first successful response to + * initialize. If an endpoint returns anything other than a 200 or a 4xx, the method will exit without + * attempting subsequent endpoints. + * + * The three endpoints are computed as follows, given that the {@code issuer} is composed of a {@code host} + * and a {@code path}: + * + *
    + *
  1. + * {@code host/.well-known/openid-configuration/path}, as defined in + * RFC 8414's Compatibility Notes. + *
  2. + *
  3. + * {@code issuer/.well-known/openid-configuration}, as defined in + * + * OpenID Provider Configuration. + *
  4. + *
  5. + * {@code host/.well-known/oauth-authorization-server/path}, as defined in + * Authorization Server Metadata Request. + *
  6. + *
+ * + * Note that the second endpoint is the equivalent of calling + * {@link ReactiveJwtDecoders#fromOidcIssuerLocation(String)} + * + * @param issuer + * @return a {@link ReactiveJwtDecoder} that was initialized by one of the described endpoints + */ + public static ReactiveJwtDecoder fromIssuerLocation(String issuer) { + Assert.hasText(issuer, "issuer cannot be empty"); + URI uri = URI.create(issuer); + Map configuration = getConfiguration(issuer, oidc(uri), oidcRfc8414(uri), oauth(uri)); + return withProviderConfiguration(configuration, issuer); + } + + private static URI oidc(URI issuer) { + return UriComponentsBuilder.fromUri(issuer) + .replacePath(issuer.getPath() + OIDC_METADATA_PATH).build(Collections.emptyMap()); + } + + private static URI oidcRfc8414(URI issuer) { + return UriComponentsBuilder.fromUri(issuer) + .replacePath(OIDC_METADATA_PATH + issuer.getPath()).build(Collections.emptyMap()); + } + + private static URI oauth(URI issuer) { + return UriComponentsBuilder.fromUri(issuer) + .replacePath(OAUTH_METADATA_PATH + issuer.getPath()).build(Collections.emptyMap()); + } + + private static Map getConfiguration(String issuer, URI... uris) { + String errorMessage = "Unable to resolve the Configuration with the provided Issuer of " + + "\"" + issuer + "\""; + for (URI uri : uris) { + try { + RequestEntity request = RequestEntity.get(uri).build(); + ResponseEntity> response = rest.exchange(request, typeReference); + return response.getBody(); + } catch (RuntimeException e) { + if (!(e instanceof HttpClientErrorException && + ((HttpClientErrorException) e).getStatusCode().is4xxClientError())) { + throw new IllegalArgumentException(errorMessage, e); + } + // else try another endpoint + } + } + throw new IllegalArgumentException(errorMessage); + } + + private static ReactiveJwtDecoder 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); - - NimbusReactiveJwtDecoder jwtDecoder = - new NimbusReactiveJwtDecoder(openidConfiguration.get("jwks_uri").toString()); + OAuth2TokenValidator jwtValidator = JwtValidators.createDefaultWithIssuer(issuer); + NimbusReactiveJwtDecoder jwtDecoder = withJwkSetUri(configuration.get("jwks_uri").toString()).build(); jwtDecoder.setJwtValidator(jwtValidator); return jwtDecoder; } - private static Map getOpenidConfiguration(String issuer) { - ParameterizedTypeReference> typeReference = new ParameterizedTypeReference>() {}; - RestTemplate rest = new RestTemplate(); - try { - URI uri = UriComponentsBuilder.fromUriString(issuer + "/.well-known/openid-configuration") - .build() - .toUri(); - RequestEntity 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); - } - } - private ReactiveJwtDecoders() {} } diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtDecodersTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtDecodersTests.java index 09531914ac..e87bf55553 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtDecodersTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtDecodersTests.java @@ -15,17 +15,23 @@ */ package org.springframework.security.oauth2.jwt; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import okhttp3.HttpUrl; 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; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.web.util.UriComponentsBuilder; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; @@ -67,6 +73,9 @@ public class JwtDecodersTests { private static final String JWK_SET = "{\"keys\":[{\"p\":\"49neceJFs8R6n7WamRGy45F5Tv0YM-R2ODK3eSBUSLOSH2tAqjEVKOkLE5fiNA3ygqq15NcKRadB2pTVf-Yb5ZIBuKzko8bzYIkIqYhSh_FAdEEr0vHF5fq_yWSvc6swsOJGqvBEtuqtJY027u-G2gAQasCQdhyejer68zsTn8M\",\"kty\":\"RSA\",\"q\":\"tWR-ysspjZ73B6p2vVRVyHwP3KQWL5KEQcdgcmMOE_P_cPs98vZJfLhxobXVmvzuEWBpRSiqiuyKlQnpstKt94Cy77iO8m8ISfF3C9VyLWXi9HUGAJb99irWABFl3sNDff5K2ODQ8CmuXLYM25OwN3ikbrhEJozlXg_NJFSGD4E\",\"d\":\"FkZHYZlw5KSoqQ1i2RA2kCUygSUOf1OqMt3uomtXuUmqKBm_bY7PCOhmwbvbn4xZYEeHuTR8Xix-0KpHe3NKyWrtRjkq1T_un49_1LLVUhJ0dL-9_x0xRquVjhl_XrsRXaGMEHs8G9pLTvXQ1uST585gxIfmCe0sxPZLvwoic-bXf64UZ9BGRV3lFexWJQqCZp2S21HfoU7wiz6kfLRNi-K4xiVNB1gswm_8o5lRuY7zB9bRARQ3TS2G4eW7p5sxT3CgsGiQD3_wPugU8iDplqAjgJ5ofNJXZezoj0t6JMB_qOpbrmAM1EnomIPebSLW7Ky9SugEd6KMdL5lW6AuAQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"qi\":\"wdkFu_tV2V1l_PWUUimG516Zvhqk2SWDw1F7uNDD-Lvrv_WNRIJVzuffZ8WYiPy8VvYQPJUrT2EXL8P0ocqwlaSTuXctrORcbjwgxDQDLsiZE0C23HYzgi0cofbScsJdhcBg7d07LAf7cdJWG0YVl1FkMCsxUlZ2wTwHfKWf-v4\",\"dp\":\"uwnPxqC-IxG4r33-SIT02kZC1IqC4aY7PWq0nePiDEQMQWpjjNH50rlq9EyLzbtdRdIouo-jyQXB01K15-XXJJ60dwrGLYNVqfsTd0eGqD1scYJGHUWG9IDgCsxyEnuG3s0AwbW2UolWVSsU2xMZGb9PurIUZECeD1XDZwMp2s0\",\"dq\":\"hra786AunB8TF35h8PpROzPoE9VJJMuLrc6Esm8eZXMwopf0yhxfN2FEAvUoTpLJu93-UH6DKenCgi16gnQ0_zt1qNNIVoRfg4rw_rjmsxCYHTVL3-RDeC8X_7TsEySxW0EgFTHh-nr6I6CQrAJjPM88T35KHtdFATZ7BCBB8AE\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}"; private static final String ISSUER_MISMATCH = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczpcL1wvd3Jvbmdpc3N1ZXIiLCJleHAiOjQ2ODcyNTYwNDl9.Ax8LMI6rhB9Pv_CE3kFi1JPuLj9gZycifWrLeDpkObWEEVAsIls9zAhNFyJlG-Oo7up6_mDhZgeRfyKnpSF5GhKJtXJDCzwg0ZDVUE6rS0QadSxsMMGbl7c4y0lG_7TfLX2iWeNJukJj_oSW9KzW4FsBp1BoocWjrreesqQU3fZHbikH-c_Fs2TsAIpHnxflyEzfOFWpJ8D4DtzHXqfvieMwpy42xsPZK3LR84zlasf0Ne1tC_hLHvyHRdAXwn0CMoKxc7-8j0r9Mq8kAzUsPn9If7bMLqGkxUcTPdk5x7opAUajDZx95SXHLmtztNtBa2S6EfPJXuPKG6tM5Wq5Ug"; + private static final String OIDC_METADATA_PATH = "/.well-known/openid-configuration"; + private static final String OAUTH_METADATA_PATH = "/.well-known/oauth-authorization-server"; + private MockWebServer server; private String issuer; @@ -74,7 +83,7 @@ public class JwtDecodersTests { public void setup() throws Exception { this.server = new MockWebServer(); this.server.start(); - this.issuer = createIssuerFromServer(); + this.issuer = createIssuerFromServer() + "path"; } @After @@ -85,56 +94,50 @@ public class JwtDecodersTests { @Test public void issuerWhenResponseIsTypicalThenReturnedDecoderValidatesIssuer() { prepareConfigurationResponse(); - this.server.enqueue(new MockResponse().setBody(JWK_SET)); - JwtDecoder decoder = JwtDecoders.fromOidcIssuerLocation(this.issuer); - - assertResponseIsTypicalThenReturnedDecoderValidatesIssuer(decoder); + assertThatCode(() -> decoder.decode(ISSUER_MISMATCH)) + .isInstanceOf(JwtValidationException.class) + .hasMessageContaining("This iss claim is not equal to the configured issuer"); } @Test public void issuerWhenOidcFallbackResponseIsTypicalThenReturnedDecoderValidatesIssuer() { - prepareConfigurationResponseForOidcFallback("issuer1", null); - + prepareConfigurationResponseOidc(); JwtDecoder decoder = JwtDecoders.fromIssuerLocation(this.issuer); - - assertResponseIsTypicalThenReturnedDecoderValidatesIssuer(decoder); + assertThatCode(() -> decoder.decode(ISSUER_MISMATCH)) + .isInstanceOf(JwtValidationException.class) + .hasMessageContaining("This iss claim is not equal to the configured issuer"); } @Test - public void issuerWhenOauth2ResponseIsTypicalThenReturnedDecoderValidatesIssuer() { - prepareConfigurationResponseForOauth2("issuer1", null); - + public void issuerWhenOAuth2ResponseIsTypicalThenReturnedDecoderValidatesIssuer() { + prepareConfigurationResponseOAuth2(); 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() { + this.issuer += "/"; prepareConfigurationResponse(); - this.server.enqueue(new MockResponse().setBody(JWK_SET)); assertThat(JwtDecoders.fromOidcIssuerLocation(this.issuer)).isNotNull(); assertThat(this.issuer).endsWith("/"); } @Test public void issuerWhenOidcFallbackContainsTrailingSlashThenSuccess() { - prepareConfigurationResponse(); - this.server.enqueue(new MockResponse().setBody(JWK_SET)); + this.issuer += "/"; + prepareConfigurationResponseOidc(); assertThat(JwtDecoders.fromIssuerLocation(this.issuer)).isNotNull(); assertThat(this.issuer).endsWith("/"); } @Test - public void issuerWhenOauth2ContainsTrailingSlashThenSuccess() { - prepareConfigurationResponseForOauth2("", null); + public void issuerWhenOAuth2ContainsTrailingSlashThenSuccess() { + this.issuer += "/"; + prepareConfigurationResponseOAuth2(); assertThat(JwtDecoders.fromIssuerLocation(this.issuer)).isNotNull(); assertThat(this.issuer).endsWith("/"); } @@ -148,15 +151,14 @@ public class JwtDecodersTests { @Test public void issuerWhenOidcFallbackResponseIsNonCompliantThenThrowsRuntimeException() { - prepareConfigurationResponseForOidcFallback("", "{ \"missing_required_keys\" : \"and_values\" }"); + prepareConfigurationResponseOidc("{ \"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); + public void issuerWhenOAuth2ResponseIsNonCompliantThenThrowsRuntimeException() { + prepareConfigurationResponseOAuth2("{ \"missing_required_keys\" : \"and_values\" }"); assertThatCode(() -> JwtDecoders.fromIssuerLocation(this.issuer)) .isInstanceOf(RuntimeException.class); } @@ -170,36 +172,36 @@ public class JwtDecodersTests { @Test public void issuerWhenOidcFallbackResponseIsMalformedThenThrowsRuntimeException() { - prepareConfigurationResponseForOidcFallback("", "malformed"); - assertThatCode(() -> JwtDecoders.fromOidcIssuerLocation(this.issuer)) + prepareConfigurationResponseOidc("malformed"); + assertThatCode(() -> JwtDecoders.fromIssuerLocation(this.issuer)) .isInstanceOf(RuntimeException.class); } @Test - public void issuerWhenOauth2ResponseIsMalformedThenThrowsRuntimeException() { - prepareConfigurationResponseForOauth2("", "malformed"); + public void issuerWhenOAuth2ResponseIsMalformedThenThrowsRuntimeException() { + prepareConfigurationResponseOAuth2("malformed"); assertThatCode(() -> JwtDecoders.fromIssuerLocation(this.issuer)) .isInstanceOf(RuntimeException.class); } @Test public void issuerWhenRespondingIssuerMismatchesRequestedIssuerThenThrowsIllegalStateException() { - prepareConfigurationResponse(); - assertThatCode(() -> JwtDecoders.fromOidcIssuerLocation(this.issuer + "/wrong")) + prepareConfigurationResponse(String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer + "/wrong", this.issuer)); + assertThatCode(() -> JwtDecoders.fromOidcIssuerLocation(this.issuer)) .isInstanceOf(IllegalStateException.class); } @Test public void issuerWhenOidcFallbackRespondingIssuerMismatchesRequestedIssuerThenThrowsIllegalStateException() { - prepareConfigurationResponseForOidcFallback("", null); - assertThatCode(() -> JwtDecoders.fromIssuerLocation(this.issuer + "/wrong")) + prepareConfigurationResponseOidc(String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer + "/wrong", this.issuer)); + assertThatCode(() -> JwtDecoders.fromIssuerLocation(this.issuer)) .isInstanceOf(IllegalStateException.class); } @Test - public void issuerWhenOauth2RespondingIssuerMismatchesRequestedIssuerThenThrowsIllegalStateException() { - prepareConfigurationResponseForOauth2("", null); - assertThatCode(() -> JwtDecoders.fromIssuerLocation(this.issuer + "/wrong")) + public void issuerWhenOAuth2RespondingIssuerMismatchesRequestedIssuerThenThrowsIllegalStateException() { + prepareConfigurationResponseOAuth2(String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer + "/wrong", this.issuer)); + assertThatCode(() -> JwtDecoders.fromIssuerLocation(this.issuer)) .isInstanceOf(IllegalStateException.class); } @@ -208,7 +210,6 @@ public class JwtDecodersTests { throws Exception { this.server.shutdown(); - assertThatCode(() -> JwtDecoders.fromOidcIssuerLocation("https://issuer")) .isInstanceOf(IllegalArgumentException.class); } @@ -218,7 +219,6 @@ public class JwtDecodersTests { throws Exception { this.server.shutdown(); - assertThatCode(() -> JwtDecoders.fromIssuerLocation("https://issuer")) .isInstanceOf(IllegalArgumentException.class); } @@ -229,75 +229,69 @@ public class JwtDecodersTests { } private void prepareConfigurationResponse(String body) { - MockResponse mockResponse = new MockResponse() - .setBody(body) - .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); - this.server.enqueue(mockResponse); + this.server.enqueue(response(body)); + this.server.enqueue(response(JWK_SET)); } - /** - * 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); + private void prepareConfigurationResponseOidc() { + String body = String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer, this.issuer); + prepareConfigurationResponseOidc(body); + } - final Dispatcher dispatcher = new Dispatcher() { + private void prepareConfigurationResponseOidc(String body) { + Map responses = new HashMap<>(); + responses.put(oidc(), response(body)); + responses.put(jwks(), response(JWK_SET)); + prepareConfigurationResponses(responses); + } + + private void prepareConfigurationResponseOAuth2() { + String body = String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer, this.issuer); + prepareConfigurationResponseOAuth2(body); + } + + private void prepareConfigurationResponseOAuth2(String body) { + Map responses = new HashMap<>(); + responses.put(oauth(), response(body)); + responses.put(jwks(), response(JWK_SET)); + prepareConfigurationResponses(responses); + } + + private void prepareConfigurationResponses(Map responses) { + 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); + public MockResponse dispatch(RecordedRequest request) { + return Optional.of(request).map(RecordedRequest::getRequestUrl).map(HttpUrl::toString) + .map(responses::get) + .orElse(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(); } + + private String oidc() { + URI uri = URI.create(this.issuer); + return UriComponentsBuilder.fromUri(uri) + .replacePath(uri.getPath() + OIDC_METADATA_PATH).toUriString(); + } + + private String oauth() { + URI uri = URI.create(this.issuer); + return UriComponentsBuilder.fromUri(uri) + .replacePath(OAUTH_METADATA_PATH + uri.getPath()).toUriString(); + } + + private String jwks() { + return this.issuer + "/.well-known/jwks.json"; + } + + private MockResponse response(String body) { + return new MockResponse() + .setBody(body) + .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + } } diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecodersTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecodersTests.java index ff0bc6d970..d4d34cf356 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecodersTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecodersTests.java @@ -15,14 +15,23 @@ */ package org.springframework.security.oauth2.jwt; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import okhttp3.HttpUrl; +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; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.web.util.UriComponentsBuilder; import static org.assertj.core.api.Assertions.assertThatCode; @@ -62,6 +71,9 @@ public class ReactiveJwtDecodersTests { private static final String JWK_SET = "{\"keys\":[{\"p\":\"49neceJFs8R6n7WamRGy45F5Tv0YM-R2ODK3eSBUSLOSH2tAqjEVKOkLE5fiNA3ygqq15NcKRadB2pTVf-Yb5ZIBuKzko8bzYIkIqYhSh_FAdEEr0vHF5fq_yWSvc6swsOJGqvBEtuqtJY027u-G2gAQasCQdhyejer68zsTn8M\",\"kty\":\"RSA\",\"q\":\"tWR-ysspjZ73B6p2vVRVyHwP3KQWL5KEQcdgcmMOE_P_cPs98vZJfLhxobXVmvzuEWBpRSiqiuyKlQnpstKt94Cy77iO8m8ISfF3C9VyLWXi9HUGAJb99irWABFl3sNDff5K2ODQ8CmuXLYM25OwN3ikbrhEJozlXg_NJFSGD4E\",\"d\":\"FkZHYZlw5KSoqQ1i2RA2kCUygSUOf1OqMt3uomtXuUmqKBm_bY7PCOhmwbvbn4xZYEeHuTR8Xix-0KpHe3NKyWrtRjkq1T_un49_1LLVUhJ0dL-9_x0xRquVjhl_XrsRXaGMEHs8G9pLTvXQ1uST585gxIfmCe0sxPZLvwoic-bXf64UZ9BGRV3lFexWJQqCZp2S21HfoU7wiz6kfLRNi-K4xiVNB1gswm_8o5lRuY7zB9bRARQ3TS2G4eW7p5sxT3CgsGiQD3_wPugU8iDplqAjgJ5ofNJXZezoj0t6JMB_qOpbrmAM1EnomIPebSLW7Ky9SugEd6KMdL5lW6AuAQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"qi\":\"wdkFu_tV2V1l_PWUUimG516Zvhqk2SWDw1F7uNDD-Lvrv_WNRIJVzuffZ8WYiPy8VvYQPJUrT2EXL8P0ocqwlaSTuXctrORcbjwgxDQDLsiZE0C23HYzgi0cofbScsJdhcBg7d07LAf7cdJWG0YVl1FkMCsxUlZ2wTwHfKWf-v4\",\"dp\":\"uwnPxqC-IxG4r33-SIT02kZC1IqC4aY7PWq0nePiDEQMQWpjjNH50rlq9EyLzbtdRdIouo-jyQXB01K15-XXJJ60dwrGLYNVqfsTd0eGqD1scYJGHUWG9IDgCsxyEnuG3s0AwbW2UolWVSsU2xMZGb9PurIUZECeD1XDZwMp2s0\",\"dq\":\"hra786AunB8TF35h8PpROzPoE9VJJMuLrc6Esm8eZXMwopf0yhxfN2FEAvUoTpLJu93-UH6DKenCgi16gnQ0_zt1qNNIVoRfg4rw_rjmsxCYHTVL3-RDeC8X_7TsEySxW0EgFTHh-nr6I6CQrAJjPM88T35KHtdFATZ7BCBB8AE\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}"; private static final String ISSUER_MISMATCH = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczpcL1wvd3Jvbmdpc3N1ZXIiLCJleHAiOjQ2ODcyNTYwNDl9.Ax8LMI6rhB9Pv_CE3kFi1JPuLj9gZycifWrLeDpkObWEEVAsIls9zAhNFyJlG-Oo7up6_mDhZgeRfyKnpSF5GhKJtXJDCzwg0ZDVUE6rS0QadSxsMMGbl7c4y0lG_7TfLX2iWeNJukJj_oSW9KzW4FsBp1BoocWjrreesqQU3fZHbikH-c_Fs2TsAIpHnxflyEzfOFWpJ8D4DtzHXqfvieMwpy42xsPZK3LR84zlasf0Ne1tC_hLHvyHRdAXwn0CMoKxc7-8j0r9Mq8kAzUsPn9If7bMLqGkxUcTPdk5x7opAUajDZx95SXHLmtztNtBa2S6EfPJXuPKG6tM5Wq5Ug"; + private static final String OIDC_METADATA_PATH = "/.well-known/openid-configuration"; + private static final String OAUTH_METADATA_PATH = "/.well-known/oauth-authorization-server"; + private MockWebServer server; private String issuer; private String jwkSetUri; @@ -71,7 +83,8 @@ public class ReactiveJwtDecodersTests { this.server = new MockWebServer(); this.server.start(); this.issuer = createIssuerFromServer(); - this.jwkSetUri = this.issuer + "/.well-known/jwks.json"; + this.jwkSetUri = this.issuer + ".well-known/jwks.json"; + this.issuer += "path"; } @After @@ -81,8 +94,7 @@ public class ReactiveJwtDecodersTests { @Test public void issuerWhenResponseIsTypicalThenReturnedDecoderValidatesIssuer() { - prepareOpenIdConfigurationResponse(); - this.server.enqueue(new MockResponse().setBody(JWK_SET)); + prepareConfigurationResponse(); ReactiveJwtDecoder decoder = ReactiveJwtDecoders.fromOidcIssuerLocation(this.issuer); @@ -91,27 +103,91 @@ public class ReactiveJwtDecodersTests { .hasMessageContaining("This iss claim is not equal to the configured issuer"); } + @Test + public void issuerWhenOidcFallbackResponseIsTypicalThenReturnedDecoderValidatesIssuer() { + prepareConfigurationResponseOidc(); + + ReactiveJwtDecoder decoder = ReactiveJwtDecoders.fromIssuerLocation(this.issuer); + + assertThatCode(() -> decoder.decode(ISSUER_MISMATCH).block()) + .isInstanceOf(JwtValidationException.class) + .hasMessageContaining("This iss claim is not equal to the configured issuer"); + } + + @Test + public void issuerWhenOAuth2ResponseIsTypicalThenReturnedDecoderValidatesIssuer() { + prepareConfigurationResponseOAuth2(); + + ReactiveJwtDecoder decoder = ReactiveJwtDecoders.fromIssuerLocation(this.issuer); + + assertThatCode(() -> decoder.decode(ISSUER_MISMATCH).block()) + .isInstanceOf(JwtValidationException.class) + .hasMessageContaining("This iss claim is not equal to the configured issuer"); + } + @Test public void issuerWhenResponseIsNonCompliantThenThrowsRuntimeException() { - prepareOpenIdConfigurationResponse("{ \"missing_required_keys\" : \"and_values\" }"); + prepareConfigurationResponse("{ \"missing_required_keys\" : \"and_values\" }"); assertThatCode(() -> ReactiveJwtDecoders.fromOidcIssuerLocation(this.issuer)) .isInstanceOf(RuntimeException.class); } + @Test + public void issuerWhenOidcFallbackResponseIsNonCompliantThenThrowsRuntimeException() { + prepareConfigurationResponseOidc("{ \"missing_required_keys\" : \"and_values\" }"); + assertThatCode(() -> ReactiveJwtDecoders.fromIssuerLocation(this.issuer)) + .isInstanceOf(RuntimeException.class); + } + + @Test + public void issuerWhenOAuth2ResponseIsNonCompliantThenThrowsRuntimeException() { + prepareConfigurationResponseOAuth2("{ \"missing_required_keys\" : \"and_values\" }"); + assertThatCode(() -> ReactiveJwtDecoders.fromIssuerLocation(this.issuer)) + .isInstanceOf(RuntimeException.class); + } + @Test public void issuerWhenResponseIsMalformedThenThrowsRuntimeException() { - prepareOpenIdConfigurationResponse("malformed"); + prepareConfigurationResponse("malformed"); assertThatCode(() -> ReactiveJwtDecoders.fromOidcIssuerLocation(this.issuer)) .isInstanceOf(RuntimeException.class); } @Test - public void issuerWhenRespondingIssuerMismatchesRequestedIssuerThenThrowsIllegalStateException() { - prepareOpenIdConfigurationResponse(); + public void issuerWhenOidcFallbackResponseIsMalformedThenThrowsRuntimeException() { + prepareConfigurationResponseOidc("malformed"); + assertThatCode(() -> ReactiveJwtDecoders.fromIssuerLocation(this.issuer)) + .isInstanceOf(RuntimeException.class); + } - assertThatCode(() -> ReactiveJwtDecoders.fromOidcIssuerLocation(this.issuer + "/wrong")) + @Test + public void issuerWhenOAuth2ResponseIsMalformedThenThrowsRuntimeException() { + prepareConfigurationResponseOAuth2("malformed"); + assertThatCode(() -> ReactiveJwtDecoders.fromIssuerLocation(this.issuer)) + .isInstanceOf(RuntimeException.class); + } + + @Test + public void issuerWhenRespondingIssuerMismatchesRequestedIssuerThenThrowsIllegalStateException() { + prepareConfigurationResponse(String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer + "/wrong", this.issuer)); + + assertThatCode(() -> ReactiveJwtDecoders.fromOidcIssuerLocation(this.issuer)) + .isInstanceOf(IllegalStateException.class); + } + + @Test + public void issuerWhenOidcFallbackRespondingIssuerMismatchesRequestedIssuerThenThrowsIllegalStateException() { + prepareConfigurationResponseOidc(String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer + "/wrong", this.issuer)); + assertThatCode(() -> ReactiveJwtDecoders.fromIssuerLocation(this.issuer)) + .isInstanceOf(IllegalStateException.class); + } + + @Test + public void issuerWhenOAuth2RespondingIssuerMismatchesRequestedIssuerThenThrowsIllegalStateException() { + prepareConfigurationResponseOAuth2(String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer + "/wrong", this.issuer)); + assertThatCode(() -> ReactiveJwtDecoders.fromIssuerLocation(this.issuer)) .isInstanceOf(IllegalStateException.class); } @@ -125,19 +201,84 @@ public class ReactiveJwtDecodersTests { .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(() -> ReactiveJwtDecoders.fromIssuerLocation("https://issuer")) + .isInstanceOf(IllegalArgumentException.class); } - private void prepareOpenIdConfigurationResponse(String body) { - MockResponse mockResponse = new MockResponse() - .setBody(body) - .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); - this.server.enqueue(mockResponse); + private void prepareConfigurationResponse() { + String body = String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer, this.issuer); + prepareConfigurationResponse(body); + } + + private void prepareConfigurationResponse(String body) { + this.server.enqueue(response(body)); + this.server.enqueue(response(JWK_SET)); + } + + private void prepareConfigurationResponseOidc() { + String body = String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer, this.issuer); + prepareConfigurationResponseOidc(body); + } + + private void prepareConfigurationResponseOidc(String body) { + Map responses = new HashMap<>(); + responses.put(oidc(), response(body)); + responses.put(jwks(), response(JWK_SET)); + prepareConfigurationResponses(responses); + } + + private void prepareConfigurationResponseOAuth2() { + String body = String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer, this.issuer); + prepareConfigurationResponseOAuth2(body); + } + + private void prepareConfigurationResponseOAuth2(String body) { + Map responses = new HashMap<>(); + responses.put(oauth(), response(body)); + responses.put(jwks(), response(JWK_SET)); + prepareConfigurationResponses(responses); + } + + private void prepareConfigurationResponses(Map responses) { + Dispatcher dispatcher = new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + return Optional.of(request).map(RecordedRequest::getRequestUrl).map(HttpUrl::toString) + .map(responses::get) + .orElse(new MockResponse().setResponseCode(404)); + } + }; + this.server.setDispatcher(dispatcher); } private String createIssuerFromServer() { return this.server.url("").toString(); } + + private String oidc() { + URI uri = URI.create(this.issuer); + return UriComponentsBuilder.fromUri(uri) + .replacePath(uri.getPath() + OIDC_METADATA_PATH).toUriString(); + } + + private String oauth() { + URI uri = URI.create(this.issuer); + return UriComponentsBuilder.fromUri(uri) + .replacePath(OAUTH_METADATA_PATH + uri.getPath()).toUriString(); + } + + private String jwks() { + return this.issuer + "/.well-known/jwks.json"; + } + + private MockResponse response(String body) { + return new MockResponse() + .setBody(body) + .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + } }