Pārlūkot izejas kodu

NimbusJwtDecoder Builder

A Builder to simply common construction patterns for NimbusJwtDecoder

Issue: gh-6010
Josh Cummings 6 gadi atpakaļ
vecāks
revīzija
d28e32b000

+ 4 - 1
config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java

@@ -30,6 +30,7 @@ import org.springframework.security.config.http.SessionCreationPolicy;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
 import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport;
 import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
 import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
@@ -43,6 +44,8 @@ import org.springframework.security.web.access.AccessDeniedHandler;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.util.Assert;
 
+import static org.springframework.security.oauth2.jwt.JwtProcessors.withJwkSetUri;
+
 /**
  *
  * An {@link AbstractHttpConfigurer} for OAuth 2.0 Resource Server Support.
@@ -218,7 +221,7 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
 		}
 
 		public JwtConfigurer jwkSetUri(String uri) {
-			this.decoder = new NimbusJwtDecoderJwkSupport(uri);
+			this.decoder = new NimbusJwtDecoder(withJwkSetUri(uri).build());
 			return this;
 		}
 

+ 7 - 7
config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java

@@ -76,7 +76,7 @@ import org.springframework.security.oauth2.jwt.JwtClaimNames;
 import org.springframework.security.oauth2.jwt.JwtDecoder;
 import org.springframework.security.oauth2.jwt.JwtException;
 import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
-import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport;
+import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
 import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
 import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
 import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
@@ -109,6 +109,7 @@ import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
+import static org.springframework.security.oauth2.jwt.JwtProcessors.withJwkSetUri;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@@ -737,7 +738,7 @@ public class OAuth2ResourceServerConfigurerTests {
 		jwtConfigurer.decoder(decoder);
 		jwtConfigurer.jwkSetUri(JWK_SET_URI);
 
-		assertThat(jwtConfigurer.getJwtDecoder()).isInstanceOf(NimbusJwtDecoderJwkSupport.class);
+		assertThat(jwtConfigurer.getJwtDecoder()).isInstanceOf(NimbusJwtDecoder.class);
 
 	}
 
@@ -770,7 +771,7 @@ public class OAuth2ResourceServerConfigurerTests {
 		jwtConfigurer.jwkSetUri(JWK_SET_URI);
 
 		assertThat(jwtConfigurer.getJwtDecoder()).isNotEqualTo(decoder);
-		assertThat(jwtConfigurer.getJwtDecoder()).isInstanceOf(NimbusJwtDecoderJwkSupport.class);
+		assertThat(jwtConfigurer.getJwtDecoder()).isInstanceOf(NimbusJwtDecoder.class);
 	}
 
 	@Test
@@ -1460,8 +1461,7 @@ public class OAuth2ResourceServerConfigurerTests {
 
 		@Override
 		protected void configure(HttpSecurity http) throws Exception {
-			NimbusJwtDecoderJwkSupport jwtDecoder =
-					new NimbusJwtDecoderJwkSupport(this.uri);
+			NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(withJwkSetUri(this.uri).build());
 			jwtDecoder.setJwtValidator(this.jwtValidator);
 
 			// @formatter:off
@@ -1488,7 +1488,7 @@ public class OAuth2ResourceServerConfigurerTests {
 			JwtTimestampValidator jwtValidator = new JwtTimestampValidator(Duration.ofHours(1));
 			jwtValidator.setClock(nearlyAnHourFromTokenExpiry);
 
-			NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(this.uri);
+			NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(withJwkSetUri(this.uri).build());
 			jwtDecoder.setJwtValidator(jwtValidator);
 
 			// @formatter:off
@@ -1511,7 +1511,7 @@ public class OAuth2ResourceServerConfigurerTests {
 			JwtTimestampValidator jwtValidator = new JwtTimestampValidator(Duration.ofHours(1));
 			jwtValidator.setClock(justOverOneHourAfterExpiry);
 
-			NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(this.uri);
+			NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(withJwkSetUri(this.uri).build());
 			jwtDecoder.setJwtValidator(jwtValidator);
 
 			// @formatter:off

+ 5 - 2
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java

@@ -43,10 +43,12 @@ import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames
 import org.springframework.security.oauth2.core.oidc.user.OidcUser;
 import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.security.oauth2.jwt.JwtDecoder;
-import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport;
+import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
+import static org.springframework.security.oauth2.jwt.JwtProcessors.withJwkSetUri;
+
 /**
  * An implementation of an {@link AuthenticationProvider}
  * for the OpenID Connect Core 1.0 Authorization Code Grant Flow.
@@ -209,7 +211,8 @@ public class OidcAuthorizationCodeAuthenticationProvider implements Authenticati
 				);
 				throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
 			}
-			jwtDecoder = new NimbusJwtDecoderJwkSupport(clientRegistration.getProviderDetails().getJwkSetUri());
+			String jwkSetUri = clientRegistration.getProviderDetails().getJwkSetUri();
+			jwtDecoder = new NimbusJwtDecoder(withJwkSetUri(jwkSetUri).build());
 			this.jwtDecoders.put(clientRegistration.getRegistrationId(), jwtDecoder);
 		}
 		return jwtDecoder;

+ 0 - 1
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoder.java

@@ -44,5 +44,4 @@ public interface JwtDecoder {
 	 * @throws JwtException if an error occurs while attempting to decode the JWT
 	 */
 	Jwt decode(String token) throws JwtException;
-
 }

+ 6 - 4
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoders.java

@@ -15,14 +15,16 @@
  */
 package org.springframework.security.oauth2.jwt;
 
+import java.net.URI;
+import java.util.Map;
+
 import org.springframework.core.ParameterizedTypeReference;
 import org.springframework.http.RequestEntity;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
 import org.springframework.web.client.RestTemplate;
 import org.springframework.web.util.UriComponentsBuilder;
 
-import java.net.URI;
-import java.util.Map;
+import static org.springframework.security.oauth2.jwt.JwtProcessors.withJwkSetUri;
 
 /**
  * Allows creating a {@link JwtDecoder} from an
@@ -58,8 +60,8 @@ public final class JwtDecoders {
 		OAuth2TokenValidator<Jwt> jwtValidator =
 				JwtValidators.createDefaultWithIssuer(oidcIssuerLocation);
 
-		NimbusJwtDecoderJwkSupport jwtDecoder =
-				new NimbusJwtDecoderJwkSupport(openidConfiguration.get("jwks_uri").toString());
+		NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(
+				withJwkSetUri(openidConfiguration.get("jwks_uri").toString()).build());
 		jwtDecoder.setJwtValidator(jwtValidator);
 
 		return jwtDecoder;

+ 162 - 0
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtProcessors.java

@@ -0,0 +1,162 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.jwt;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collections;
+
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.jwk.source.JWKSource;
+import com.nimbusds.jose.jwk.source.RemoteJWKSet;
+import com.nimbusds.jose.proc.JWSKeySelector;
+import com.nimbusds.jose.proc.JWSVerificationKeySelector;
+import com.nimbusds.jose.proc.SecurityContext;
+import com.nimbusds.jose.util.Resource;
+import com.nimbusds.jose.util.ResourceRetriever;
+import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
+import com.nimbusds.jwt.proc.DefaultJWTProcessor;
+import com.nimbusds.jwt.proc.JWTProcessor;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.http.RequestEntity;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.Assert;
+import org.springframework.web.client.RestOperations;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * A collection of builders for creating Nimbus {@link JWTProcessor} instances.
+ *
+ * @author Josh Cummings
+ * @since 5.2
+ * @see NimbusJwtDecoder
+ */
+public final class JwtProcessors {
+
+	/**
+	 * Use the given
+	 * <a href="https://tools.ietf.org/html/rfc7517#section-5">JWK Set</a> uri.
+	 *
+	 * @param jwkSetUri the JWK Set uri to use
+	 * @return a {@link JwtProcessors} for further configurations
+	 */
+	public static JwkSetUriJwtProcessorBuilder withJwkSetUri(String jwkSetUri) {
+		return new JwkSetUriJwtProcessorBuilder(jwkSetUri);
+	}
+
+	/**
+	 * A builder for creating Nimbus {@link JWTProcessor} instances based on a
+	 * <a target="_blank" href="https://tools.ietf.org/html/rfc7517#section-5">JWK Set</a> uri.
+	 */
+	public static final class JwkSetUriJwtProcessorBuilder {
+		private String jwkSetUri;
+		private JWSAlgorithm jwsAlgorithm = JWSAlgorithm.RS256;
+		private RestOperations restOperations = new RestTemplate();
+
+		private JwkSetUriJwtProcessorBuilder(String jwkSetUri) {
+			Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty");
+			this.jwkSetUri = jwkSetUri;
+		}
+
+		/**
+		 * Use the given signing
+		 * <a href="https://tools.ietf.org/html/rfc7515#section-4.1.1" target="_blank">algorithm</a>.
+		 *
+		 * @param jwsAlgorithm the algorithm to use
+		 * @return a {@link JwtProcessors} for further configurations
+		 */
+		public JwkSetUriJwtProcessorBuilder jwsAlgorithm(String jwsAlgorithm) {
+			Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty");
+			this.jwsAlgorithm = JWSAlgorithm.parse(jwsAlgorithm);
+			return this;
+		}
+
+		/**
+		 * Use the given {@link RestOperations} to coordinate with the authorization servers indicated in the
+		 * <a href="https://tools.ietf.org/html/rfc7517#section-5">JWK Set</a> uri
+		 * as well as the
+		 * <a href="http://openid.net/specs/openid-connect-core-1_0.html#IssuerIdentifier">Issuer</a>.
+		 *
+		 * @param restOperations
+		 * @return
+		 */
+		public JwkSetUriJwtProcessorBuilder restOperations(RestOperations restOperations) {
+			Assert.notNull(restOperations, "restOperations cannot be null");
+			this.restOperations = restOperations;
+			return this;
+		}
+
+		/**
+		 * Build the configured {@link JwtDecoder}.
+		 *
+		 * @return the configured {@link JwtDecoder}
+		 */
+		public JWTProcessor<SecurityContext> build() {
+			ResourceRetriever jwkSetRetriever = new RestOperationsResourceRetriever(this.restOperations);
+			JWKSource<SecurityContext> jwkSource = new RemoteJWKSet<>(toURL(this.jwkSetUri), jwkSetRetriever);
+			JWSKeySelector<SecurityContext> jwsKeySelector =
+					new JWSVerificationKeySelector<>(this.jwsAlgorithm, jwkSource);
+			ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
+			jwtProcessor.setJWSKeySelector(jwsKeySelector);
+
+			// Spring Security validates the claim set independent from Nimbus
+			jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { });
+
+			return jwtProcessor;
+		}
+
+		private static URL toURL(String url) {
+			try {
+				return new URL(url);
+			} catch (MalformedURLException ex) {
+				throw new IllegalArgumentException("Invalid JWK Set URL \"" + url + "\" : " + ex.getMessage(), ex);
+			}
+		}
+
+		private static class RestOperationsResourceRetriever implements ResourceRetriever {
+			private final RestOperations restOperations;
+
+			RestOperationsResourceRetriever(RestOperations restOperations) {
+				Assert.notNull(restOperations, "restOperations cannot be null");
+				this.restOperations = restOperations;
+			}
+
+			@Override
+			public Resource retrieveResource(URL url) throws IOException {
+				HttpHeaders headers = new HttpHeaders();
+				headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));
+
+				ResponseEntity<String> response;
+				try {
+					RequestEntity<Void> request = new RequestEntity<>(headers, HttpMethod.GET, url.toURI());
+					response = this.restOperations.exchange(request, String.class);
+				} catch (Exception ex) {
+					throw new IOException(ex);
+				}
+
+				if (response.getStatusCodeValue() != 200) {
+					throw new IOException(response.toString());
+				}
+
+				return new Resource(response.getBody(), "UTF-8");
+			}
+		}
+	}
+}

+ 7 - 0
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java

@@ -38,8 +38,15 @@ import org.springframework.util.Assert;
 /**
  * A low-level Nimbus implementation of {@link JwtDecoder} which takes a raw Nimbus configuration.
  *
+ * It's simple to produce an instance of {@code JWTProcessor} using {@link JwtProcessors}:
+ * <pre>
+ * 	JWTProcessor&lt;SecurityContext&gt; jwtProcessor = JwtProcessors.fromJwkSetUri(uri).build();
+ * 	NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(jwtProcessor);
+ * </pre>
+ *
  * @author Josh Cummings
  * @since 5.2
+ * @see JwtProcessors
  */
 public final class NimbusJwtDecoder implements JwtDecoder {
 	private static final String DECODING_ERROR_MESSAGE_TEMPLATE =

+ 19 - 62
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java

@@ -15,34 +15,16 @@
  */
 package org.springframework.security.oauth2.jwt;
 
-import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URL;
 import java.util.Collections;
 import java.util.Map;
 
-import com.nimbusds.jose.JWSAlgorithm;
-import com.nimbusds.jose.jwk.source.JWKSource;
-import com.nimbusds.jose.jwk.source.RemoteJWKSet;
-import com.nimbusds.jose.proc.JWSKeySelector;
-import com.nimbusds.jose.proc.JWSVerificationKeySelector;
-import com.nimbusds.jose.proc.SecurityContext;
-import com.nimbusds.jose.util.Resource;
-import com.nimbusds.jose.util.ResourceRetriever;
-import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
-import com.nimbusds.jwt.proc.DefaultJWTProcessor;
-
 import org.springframework.core.convert.converter.Converter;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.MediaType;
-import org.springframework.http.RequestEntity;
-import org.springframework.http.ResponseEntity;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
 import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
 import org.springframework.util.Assert;
 import org.springframework.web.client.RestOperations;
-import org.springframework.web.client.RestTemplate;
+
+import static org.springframework.security.oauth2.jwt.JwtProcessors.withJwkSetUri;
 
 /**
  * An implementation of a {@link JwtDecoder} that "decodes" a
@@ -53,7 +35,7 @@ import org.springframework.web.client.RestTemplate;
  * <p>
  * <b>NOTE:</b> This implementation uses the Nimbus JOSE + JWT SDK internally.
  *
- * @deprecated Use {@link NimbusJwtDecoder} instead
+ * @deprecated Use {@link NimbusJwtDecoder} or {@link JwtDecoders} instead
  *
  * @author Joe Grandja
  * @author Josh Cummings
@@ -67,8 +49,10 @@ import org.springframework.web.client.RestTemplate;
  */
 @Deprecated
 public final class NimbusJwtDecoderJwkSupport implements JwtDecoder {
-	private final JWSAlgorithm jwsAlgorithm;
-	private final RestOperationsResourceRetriever jwkSetRetriever = new RestOperationsResourceRetriever();
+	private JwtProcessors.JwkSetUriJwtProcessorBuilder jwtProcessorBuilder;
+	private OAuth2TokenValidator<Jwt> jwtValidator = JwtValidators.createDefault();
+	private Converter<Map<String, Object>, Map<String, Object>> claimSetConverter =
+			MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap());
 
 	private NimbusJwtDecoder delegate;
 
@@ -90,22 +74,16 @@ public final class NimbusJwtDecoderJwkSupport implements JwtDecoder {
 	public NimbusJwtDecoderJwkSupport(String jwkSetUrl, String jwsAlgorithm) {
 		Assert.hasText(jwkSetUrl, "jwkSetUrl cannot be empty");
 		Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty");
-		JWKSource jwkSource;
-		try {
-			jwkSource = new RemoteJWKSet(new URL(jwkSetUrl), this.jwkSetRetriever);
-		} catch (MalformedURLException ex) {
-			throw new IllegalArgumentException("Invalid JWK Set URL \"" + jwkSetUrl + "\" : " + ex.getMessage(), ex);
-		}
-		this.jwsAlgorithm = JWSAlgorithm.parse(jwsAlgorithm);
-		JWSKeySelector<SecurityContext> jwsKeySelector =
-			new JWSVerificationKeySelector<>(this.jwsAlgorithm, jwkSource);
-		ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
-		jwtProcessor.setJWSKeySelector(jwsKeySelector);
 
-		// Spring Security validates the claim set independent from Nimbus
-		jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {});
+		this.jwtProcessorBuilder = withJwkSetUri(jwkSetUrl).jwsAlgorithm(jwsAlgorithm);
+		this.delegate = makeDelegate();
+	}
 
-		this.delegate = new NimbusJwtDecoder(jwtProcessor);
+	private NimbusJwtDecoder makeDelegate() {
+		NimbusJwtDecoder delegate = new NimbusJwtDecoder(this.jwtProcessorBuilder.build());
+		delegate.setClaimSetConverter(this.claimSetConverter);
+		delegate.setJwtValidator(this.jwtValidator);
+		return delegate;
 	}
 
 	@Override
@@ -120,6 +98,7 @@ public final class NimbusJwtDecoderJwkSupport implements JwtDecoder {
 	 */
 	public void setJwtValidator(OAuth2TokenValidator<Jwt> jwtValidator) {
 		Assert.notNull(jwtValidator, "jwtValidator cannot be null");
+		this.jwtValidator = jwtValidator;
 		this.delegate.setJwtValidator(jwtValidator);
 	}
 
@@ -130,6 +109,7 @@ public final class NimbusJwtDecoderJwkSupport implements JwtDecoder {
 	 */
 	public final void setClaimSetConverter(Converter<Map<String, Object>, Map<String, Object>> claimSetConverter) {
 		Assert.notNull(claimSetConverter, "claimSetConverter cannot be null");
+		this.claimSetConverter = claimSetConverter;
 		this.delegate.setClaimSetConverter(claimSetConverter);
 	}
 
@@ -141,30 +121,7 @@ public final class NimbusJwtDecoderJwkSupport implements JwtDecoder {
 	 */
 	public final void setRestOperations(RestOperations restOperations) {
 		Assert.notNull(restOperations, "restOperations cannot be null");
-		this.jwkSetRetriever.restOperations = restOperations;
-	}
-
-	private static class RestOperationsResourceRetriever implements ResourceRetriever {
-		private RestOperations restOperations = new RestTemplate();
-
-		@Override
-		public Resource retrieveResource(URL url) throws IOException {
-			HttpHeaders headers = new HttpHeaders();
-			headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));
-
-			ResponseEntity<String> response;
-			try {
-				RequestEntity<Void> request = new RequestEntity<>(headers, HttpMethod.GET, url.toURI());
-				response = this.restOperations.exchange(request, String.class);
-			} catch (Exception ex) {
-				throw new IOException(ex);
-			}
-
-			if (response.getStatusCodeValue() != 200) {
-				throw new IOException(response.toString());
-			}
-
-			return new Resource(response.getBody(), "UTF-8");
-		}
+		this.jwtProcessorBuilder = this.jwtProcessorBuilder.restOperations(restOperations);
+		this.delegate = makeDelegate();
 	}
 }

+ 83 - 0
oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtProcessorsTest.java

@@ -0,0 +1,83 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.oauth2.jwt;
+
+import com.nimbusds.jose.proc.SecurityContext;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.proc.JWTProcessor;
+import org.junit.Test;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.RequestEntity;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.RestOperations;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.springframework.security.oauth2.jwt.JwtProcessors.withJwkSetUri;
+
+/**
+ * Tests for {@link JwtProcessors}
+ */
+public class JwtProcessorsTest {
+	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 SIGNED_JWT = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJzY3AiOlsibWVzc2FnZTpyZWFkIl0sImV4cCI6NDY4Mzg5Nzc3Nn0.LtMVtIiRIwSyc3aX35Zl0JVwLTcQZAB3dyBOMHNaHCKUljwMrf20a_gT79LfhjDzE_fUVUmFiAO32W1vFnYpZSVaMDUgeIOIOpxfoe9shj_uYenAwIS-_UxqGVIJiJoXNZh_MK80ShNpvsQwamxWEEOAMBtpWNiVYNDMdfgho9n3o5_Z7Gjy8RLBo1tbDREbO9kTFwGIxm_EYpezmRCRq4w1DdS6UDW321hkwMxPnCMSWOvp-hRpmgY2yjzLgPJ6Aucmg9TJ8jloAP1DjJoF1gRR7NTAk8LOGkSjTzVYDYMbCF51YdpojhItSk80YzXiEsv1mTz4oMM49jXBmfXFMA";
+	private static final String JWK_SET_URI = "http://issuer/.well-known/jwks.json";
+
+	@Test
+	public void withJwkSetUriWhenNullOrEmptyThenThrowsException() {
+		assertThatCode(() -> withJwkSetUri(null)).isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void jwsAlgorithmWhenNullOrEmptyThenThrowsException() {
+		JwtProcessors.JwkSetUriJwtProcessorBuilder builder = withJwkSetUri(JWK_SET_URI);
+		assertThatCode(() -> builder.jwsAlgorithm(null)).isInstanceOf(IllegalArgumentException.class);
+		assertThatCode(() -> builder.jwsAlgorithm("")).isInstanceOf(IllegalArgumentException.class);
+		assertThatCode(() -> builder.jwsAlgorithm("RS4096")).doesNotThrowAnyException();
+	}
+
+	@Test
+	public void restOperationsWhenNullThenThrowsException() {
+		JwtProcessors.JwkSetUriJwtProcessorBuilder builder = withJwkSetUri(JWK_SET_URI);
+		assertThatCode(() -> builder.restOperations(null)).isInstanceOf(IllegalArgumentException.class);
+	}
+
+	// gh-5603
+	@Test
+	public void processWhenSignedThenOk() throws Exception {
+		RestOperations restOperations = mockJwkSetResponse(JWK_SET);
+		JWTProcessor<SecurityContext> processor =
+				withJwkSetUri(JWK_SET_URI).restOperations(restOperations).build();
+		assertThat(processor.process(SIGNED_JWT, null))
+				.extracting(JWTClaimsSet::getExpirationTime)
+				.isNotNull();
+		verify(restOperations).exchange(any(RequestEntity.class), eq(String.class));
+	}
+
+	private static RestOperations mockJwkSetResponse(String response) {
+		RestOperations restOperations = mock(RestOperations.class);
+		when(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
+				.thenReturn(new ResponseEntity<>(response, HttpStatus.OK));
+		return restOperations;
+	}
+}

+ 19 - 23
oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java

@@ -17,44 +17,40 @@ package org.springframework.security.oauth2.jwt;
 
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Map;
 
-import com.nimbusds.jose.JWSAlgorithm;
-import com.nimbusds.jose.JWSHeader;
-import com.nimbusds.jwt.JWT;
-import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.JWTParser;
-import com.nimbusds.jwt.SignedJWT;
-import com.nimbusds.jwt.proc.DefaultJWTProcessor;
 import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
 import org.assertj.core.api.Assertions;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mockito;
 import org.powermock.core.classloader.annotations.PowerMockIgnore;
 import org.powermock.core.classloader.annotations.PrepareForTest;
 import org.powermock.modules.junit4.PowerMockRunner;
 
 import org.springframework.core.convert.converter.Converter;
+import org.springframework.http.HttpStatus;
 import org.springframework.http.RequestEntity;
+import org.springframework.http.ResponseEntity;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
 import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
 import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
+import org.springframework.web.client.RestOperations;
 import org.springframework.web.client.RestTemplate;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode;
 import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
-import static org.powermock.api.mockito.PowerMockito.mockStatic;
 import static org.powermock.api.mockito.PowerMockito.spy;
 import static org.powermock.api.mockito.PowerMockito.when;
-import static org.powermock.api.mockito.PowerMockito.whenNew;
 
 /**
  * Tests for {@link NimbusJwtDecoderJwkSupport}.
@@ -111,21 +107,14 @@ public class NimbusJwtDecoderJwkSupportTests {
 	// gh-5168
 	@Test
 	public void decodeWhenExpClaimNullThenDoesNotThrowException() throws Exception {
-		SignedJWT jwt = mock(SignedJWT.class);
-		JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.parse(JWS_ALGORITHM)).build();
-		when(jwt.getHeader()).thenReturn(header);
-
-		mockStatic(JWTParser.class);
-		when(JWTParser.parse(anyString())).thenReturn(jwt);
-
-		DefaultJWTProcessor jwtProcessor = mock(DefaultJWTProcessor.class);
-		whenNew(DefaultJWTProcessor.class).withAnyArguments().thenReturn(jwtProcessor);
-
-		JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().audience("resource1").build();
-		when(jwtProcessor.process(any(JWT.class), eq(null))).thenReturn(jwtClaimsSet);
-
 		NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(JWK_SET_URL);
-		assertThatCode(() -> jwtDecoder.decode("encoded-jwt")).doesNotThrowAnyException();
+		jwtDecoder.setRestOperations(mockJwkSetResponse(JWK_SET));
+		jwtDecoder.setClaimSetConverter(map -> {
+			Map<String, Object> claims = new HashMap<>(map);
+			claims.remove(JwtClaimNames.EXP);
+			return claims;
+		});
+		assertThatCode(() -> jwtDecoder.decode(SIGNED_JWT)).doesNotThrowAnyException();
 	}
 
 	// gh-5457
@@ -256,4 +245,11 @@ public class NimbusJwtDecoderJwkSupportTests {
 		assertThatCode(() -> jwtDecoder.setClaimSetConverter(null))
 				.isInstanceOf(IllegalArgumentException.class);
 	}
+
+	private static RestOperations mockJwkSetResponse(String response) {
+		RestOperations restOperations = mock(RestOperations.class);
+		Mockito.when(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
+				.thenReturn(new ResponseEntity<>(response, HttpStatus.OK));
+		return restOperations;
+	}
 }

+ 32 - 37
oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java

@@ -16,43 +16,40 @@
 
 package org.springframework.security.oauth2.jwt;
 
-import java.net.URL;
 import java.text.ParseException;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Map;
 
 import com.nimbusds.jose.JOSEException;
-import com.nimbusds.jose.JWSAlgorithm;
-import com.nimbusds.jose.jwk.source.JWKSource;
-import com.nimbusds.jose.jwk.source.RemoteJWKSet;
 import com.nimbusds.jose.proc.BadJOSEException;
-import com.nimbusds.jose.proc.JWSKeySelector;
-import com.nimbusds.jose.proc.JWSVerificationKeySelector;
 import com.nimbusds.jose.proc.SecurityContext;
-import com.nimbusds.jose.util.DefaultResourceRetriever;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.jwt.proc.BadJWTException;
-import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
 import com.nimbusds.jwt.proc.DefaultJWTProcessor;
 import com.nimbusds.jwt.proc.JWTProcessor;
-import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
 import org.assertj.core.api.Assertions;
 import org.junit.Test;
 
 import org.springframework.core.convert.converter.Converter;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.RequestEntity;
+import org.springframework.http.ResponseEntity;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
 import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
+import org.springframework.web.client.RestOperations;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode;
 import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
+import static org.springframework.security.oauth2.jwt.JwtProcessors.withJwkSetUri;
 
 /**
  * Tests for {@link NimbusJwtDecoder}
@@ -101,6 +98,12 @@ public class NimbusJwtDecoderTests {
 				.doesNotThrowAnyException();
 	}
 
+	@Test
+	public void decodeWhenIatClaimNullThenDoesNotThrowException() {
+		assertThatCode(() -> this.jwtDecoder.decode(SIGNED_JWT))
+				.doesNotThrowAnyException();
+	}
+
 	// gh-5457
 	@Test
 	public void decodeWhenPlainJwtThenExceptionDoesNotMentionClass() {
@@ -159,33 +162,26 @@ public class NimbusJwtDecoderTests {
 	}
 
 	@Test
-	public void decodeWhenSignedThenOk() throws Exception {
-		try ( MockWebServer server = new MockWebServer() ) {
-			server.enqueue(new MockResponse().setBody(JWK_SET));
-			String jwkSetUri = server.url("/.well-known/jwks.json").toString();
-			NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(withJwkSetUri(jwkSetUri));
-			Jwt jwt = jwtDecoder.decode(SIGNED_JWT);
-			assertThat(jwt.containsClaim(JwtClaimNames.EXP)).isNotNull();
-		}
+	public void decodeWhenSignedThenOk() {
+		NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(withSigning(JWK_SET));
+		Jwt jwt = jwtDecoder.decode(SIGNED_JWT);
+		assertThat(jwt.containsClaim(JwtClaimNames.EXP)).isNotNull();
 	}
 
 	@Test
-	public void decodeWhenJwkResponseIsMalformedThenReturnsStockException() throws Exception {
-		try ( MockWebServer server = new MockWebServer() ) {
-			server.enqueue(new MockResponse().setBody(MALFORMED_JWK_SET));
-			String jwkSetUri = server.url("/.well-known/jwks.json").toString();
-			NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(withJwkSetUri(jwkSetUri));
-			assertThatCode(() -> jwtDecoder.decode(SIGNED_JWT))
-					.isInstanceOf(JwtException.class)
-					.hasMessage("An error occurred while attempting to decode the Jwt: Malformed Jwk set");
-		}
+	public void decodeWhenJwkResponseIsMalformedThenReturnsStockException() {
+		NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(withSigning(MALFORMED_JWK_SET));
+		assertThatCode(() -> jwtDecoder.decode(SIGNED_JWT))
+				.isInstanceOf(JwtException.class)
+				.hasMessage("An error occurred while attempting to decode the Jwt: Malformed Jwk set");
 	}
 
 	@Test
 	public void decodeWhenJwkEndpointIsUnresponsiveThenReturnsJwtException() throws Exception {
 		try ( MockWebServer server = new MockWebServer() ) {
 			String jwkSetUri = server.url("/.well-known/jwks.json").toString();
-			NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(withJwkSetUri(jwkSetUri));
+			NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(
+					withJwkSetUri(jwkSetUri).build());
 
 			server.shutdown();
 			assertThatCode(() -> jwtDecoder.decode(SIGNED_JWT))
@@ -194,18 +190,17 @@ public class NimbusJwtDecoderTests {
 		}
 	}
 
-	private JWTProcessor<SecurityContext> withoutSigning() {
-		return new MockJwtProcessor();
+	private static JWTProcessor<SecurityContext> withSigning(String jwkResponse) {
+		RestOperations restOperations = mock(RestOperations.class);
+		when(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
+				.thenReturn(new ResponseEntity<>(jwkResponse, HttpStatus.OK));
+		return withJwkSetUri("http://issuer/.well-known/jwks.json")
+				.restOperations(restOperations)
+				.build();
 	}
 
-	private JWTProcessor<SecurityContext> withJwkSetUri(String jwkSetUri) throws Exception {
-		JWKSource jwkSource =
-				new RemoteJWKSet(new URL(jwkSetUri), new DefaultResourceRetriever(5000, 5000));
-		JWSKeySelector<SecurityContext> jwsKeySelector =
-				new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, jwkSource);
-		ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
-		jwtProcessor.setJWSKeySelector(jwsKeySelector);
-		return jwtProcessor;
+	private static JWTProcessor<SecurityContext> withoutSigning() {
+		return new MockJwtProcessor();
 	}
 
 	private static class MockJwtProcessor extends DefaultJWTProcessor<SecurityContext> {