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
This commit is contained in:
Josh Cummings 2019-06-10 10:18:21 -06:00
parent f5b7706942
commit 1739ef8d3c
No known key found for this signature in database
GPG Key ID: 49EF60DD7FF83443
6 changed files with 540 additions and 465 deletions

View File

@ -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
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig">OpenID Provider Configuration</a>
@ -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<Map<String, Object>> typeReference =
new ParameterizedTypeReference<Map<String, Object>>() {};
/**
* Creates a {@link ClientRegistration.Builder} using the provided
@ -63,12 +67,6 @@ 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
@ -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<ProviderType, String> configuration = getIssuerConfiguration(issuer, OIDC_METADATA_PATH);
OIDCProviderMetadata metadata = parse(configuration.get(ProviderType.OIDCV1), OIDCProviderMetadata::parse);
Assert.hasText(issuer, "issuer cannot be empty");
Map<String, Object> configuration = getConfiguration(issuer, oidc(URI.create(issuer)));
OIDCProviderMetadata metadata = parse(configuration, 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
* Creates a {@link ClientRegistration.Builder} using the provided
* <a href="https://openid.net/specs/openid-connect-core-1_0.html#IssuerIdentifier">Issuer</a> 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}:
*
* <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.
* {@code host/.well-known/openid-configuration/path}, as defined in
* <a href="https://tools.ietf.org/html/rfc8414#section-5">RFC 8414's Compatibility Notes</a>.
* </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>
* {@code issuer/.well-known/openid-configuration}, as defined in
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest">
* OpenID Provider Configuration</a>.
* </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>.
* {@code host/.well-known/oauth-authorization-server/path}, as defined in
* <a href="https://tools.ietf.org/html/rfc8414#section-3.1">Authorization Server Metadata Request</a>.
* </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.
* Note that the second endpoint is the equivalent of calling
* {@link ClientRegistrations#fromOidcIssuerLocation(String)}.
*
* <p>
* Example usage:
@ -136,22 +133,63 @@ public final class ClientRegistrations {
* </pre>
*
* @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<ProviderType, String> configuration = getIssuerConfiguration(issuer, OIDC_METADATA_PATH, OAUTH2_METADATA_PATH);
Assert.hasText(issuer, "issuer cannot be empty");
URI uri = URI.create(issuer);
Map<String, Object> 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<String, Object> getConfiguration(String issuer, URI... uris) {
String errorMessage = "Unable to resolve Configuration with the provided Issuer of \"" + issuer + "\"";
for (URI uri : uris) {
try {
RequestEntity<Void> 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> T parse(Map<String, Object> body,
ThrowingFunction<JSONObject, T, ParseException> parser) {
try {
return parser.apply(new JSONObject(body));
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
private interface ThrowingFunction<S, T, E extends Throwable> {
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 <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(uri, String.class);
} 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>/.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)) {
@ -317,18 +249,6 @@ public final class ClientRegistrations {
}
}
private static <T> T parse(String body, ThrowingFunction<String, T, ParseException> parser) {
try {
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() {}
}

View File

@ -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/":

View File

@ -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
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig">OpenID Provider Configuration</a> 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<Map<String, Object>> typeReference =
new ParameterizedTypeReference<Map<String, Object>>() {};
/**
* 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<String, Object> configuration = getIssuerConfiguration(oidcIssuerLocation, OIDC_METADATA_PATH);
Assert.hasText(oidcIssuerLocation, "oidcIssuerLocation cannot be empty");
Map<String, Object> 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
* <a href="https://openid.net/specs/openid-connect-core-1_0.html#IssuerIdentifier">Issuer</a> 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}:
*
* <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>
* <li>
* {@code host/.well-known/openid-configuration/path}, as defined in
* <a href="https://tools.ietf.org/html/rfc8414#section-5">RFC 8414's Compatibility Notes</a>.
* </li>
* <li>
* {@code issuer/.well-known/openid-configuration}, as defined in
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest">
* OpenID Provider Configuration</a>.
* </li>
* <li>
* {@code host/.well-known/oauth-authorization-server/path}, as defined in
* <a href="https://tools.ietf.org/html/rfc8414#section-3.1">Authorization Server Metadata Request</a>.
* </li>
* </ol>
*
* Note that the second endpoint is the equivalent of calling
* {@link JwtDecoders#fromOidcIssuerLocation(String)}
*
* @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
* @return a {@link JwtDecoder} that was initialized by one of the described endpoints
*/
public static JwtDecoder fromIssuerLocation(String issuer) {
Map<String, Object> configuration = getIssuerConfiguration(issuer, OIDC_METADATA_PATH, OAUTH2_METADATA_PATH);
Assert.hasText(issuer, "issuer cannot be empty");
URI uri = URI.create(issuer);
Map<String, Object> 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<String, Object> getConfiguration(String issuer, URI... uris) {
String errorMessage = "Unable to resolve the Configuration with the provided Issuer of " +
"\"" + issuer + "\"";
for (URI uri : uris) {
try {
RequestEntity<Void> request = RequestEntity.get(uri).build();
ResponseEntity<Map<String, Object>> 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
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">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 <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 {
RequestEntity<Void> 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
* <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()
};
}
}
private JwtDecoders() {}
}

View File

@ -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<Map<String, Object>> typeReference =
new ParameterizedTypeReference<Map<String, Object>>() {};
/**
* 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<String, Object> openidConfiguration = getOpenidConfiguration(oidcIssuerLocation);
Assert.hasText(oidcIssuerLocation, "oidcIssuerLocation cannot be empty");
Map<String, Object> configuration = getConfiguration(oidcIssuerLocation, oidc(URI.create(oidcIssuerLocation)));
return withProviderConfiguration(configuration, oidcIssuerLocation);
}
/**
* Creates a {@link ReactiveJwtDecoder} using the provided
* <a href="https://openid.net/specs/openid-connect-core-1_0.html#IssuerIdentifier">Issuer</a> 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}:
*
* <ol>
* <li>
* {@code host/.well-known/openid-configuration/path}, as defined in
* <a href="https://tools.ietf.org/html/rfc8414#section-5">RFC 8414's Compatibility Notes</a>.
* </li>
* <li>
* {@code issuer/.well-known/openid-configuration}, as defined in
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest">
* OpenID Provider Configuration</a>.
* </li>
* <li>
* {@code host/.well-known/oauth-authorization-server/path}, as defined in
* <a href="https://tools.ietf.org/html/rfc8414#section-3.1">Authorization Server Metadata Request</a>.
* </li>
* </ol>
*
* 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<String, Object> 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<String, Object> getConfiguration(String issuer, URI... uris) {
String errorMessage = "Unable to resolve the Configuration with the provided Issuer of " +
"\"" + issuer + "\"";
for (URI uri : uris) {
try {
RequestEntity<Void> request = RequestEntity.get(uri).build();
ResponseEntity<Map<String, Object>> 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<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);
NimbusReactiveJwtDecoder jwtDecoder =
new NimbusReactiveJwtDecoder(openidConfiguration.get("jwks_uri").toString());
OAuth2TokenValidator<Jwt> jwtValidator = JwtValidators.createDefaultWithIssuer(issuer);
NimbusReactiveJwtDecoder 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>>() {};
RestTemplate rest = new RestTemplate();
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);
}
}
private ReactiveJwtDecoders() {}
}

View File

@ -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<String, MockResponse> 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<String, MockResponse> responses = new HashMap<>();
responses.put(oauth(), response(body));
responses.put(jwks(), response(JWK_SET));
prepareConfigurationResponses(responses);
}
private void prepareConfigurationResponses(Map<String, MockResponse> 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);
}
}

View File

@ -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<String, MockResponse> 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<String, MockResponse> responses = new HashMap<>();
responses.put(oauth(), response(body));
responses.put(jwks(), response(JWK_SET));
prepareConfigurationResponses(responses);
}
private void prepareConfigurationResponses(Map<String, MockResponse> 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);
}
}