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:
parent
f5b7706942
commit
1739ef8d3c
|
@ -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() {}
|
||||
|
||||
}
|
||||
|
|
|
@ -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/":
|
||||
|
|
|
@ -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() {}
|
||||
}
|
||||
|
|
|
@ -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() {}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue