Răsfoiți Sursa

Internalize Nimbus JwtDecoder Builder

Issue: gh-6010
Josh Cummings 6 ani în urmă
părinte
comite
9478abebd2

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

@@ -45,7 +45,7 @@ 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;
+import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSetUri;
 
 /**
  *
@@ -246,7 +246,7 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
 		}
 
 		public JwtConfigurer jwkSetUri(String uri) {
-			this.decoder = new NimbusJwtDecoder(withJwkSetUri(uri).build());
+			this.decoder = withJwkSetUri(uri).build();
 			return this;
 		}
 

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

@@ -33,8 +33,6 @@ import java.util.Map;
 import java.util.stream.Collectors;
 import javax.annotation.PreDestroy;
 
-import com.nimbusds.jose.proc.SecurityContext;
-import com.nimbusds.jwt.proc.JWTProcessor;
 import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
 import org.hamcrest.core.AllOf;
@@ -85,7 +83,6 @@ import org.springframework.security.oauth2.jwt.Jwt;
 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.JwtProcessors;
 import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
 import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
 import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
@@ -122,7 +119,8 @@ 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.withPublicKey;
+import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSetUri;
+import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withPublicKey;
 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;
@@ -1626,7 +1624,7 @@ public class OAuth2ResourceServerConfigurerTests {
 		JwtDecoder decoder() throws Exception {
 			RSAPublicKey publicKey = (RSAPublicKey)
 					KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(this.spec));
-			return new NimbusJwtDecoder(withPublicKey(publicKey).build());
+			return withPublicKey(publicKey).build();
 		}
 	}
 
@@ -1739,10 +1737,8 @@ public class OAuth2ResourceServerConfigurerTests {
 
 		@Bean
 		NimbusJwtDecoder jwtDecoder() {
-			JWTProcessor<SecurityContext> jwtProcessor =
-					JwtProcessors.withJwkSetUri("https://example.org/.well-known/jwks.json")
-							.restOperations(this.rest).build();
-			return new NimbusJwtDecoder(jwtProcessor);
+			return withJwkSetUri("https://example.org/.well-known/jwks.json")
+					.restOperations(this.rest).build();
 		}
 	}
 

+ 6 - 6
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactory.java

@@ -15,6 +15,10 @@
  */
 package org.springframework.security.oauth2.client.oidc.authentication;
 
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
 import org.springframework.security.oauth2.client.registration.ClientRegistration;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2Error;
@@ -27,11 +31,7 @@ import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.Function;
-
-import static org.springframework.security.oauth2.jwt.JwtProcessors.withJwkSetUri;
+import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSetUri;
 
 /**
  * A {@link JwtDecoderFactory factory} that provides a {@link JwtDecoder}
@@ -65,7 +65,7 @@ public final class OidcIdTokenDecoderFactory implements JwtDecoderFactory<Client
 				throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
 			}
 			String jwkSetUri = clientRegistration.getProviderDetails().getJwkSetUri();
-			NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(withJwkSetUri(jwkSetUri).build());
+			NimbusJwtDecoder jwtDecoder = withJwkSetUri(jwkSetUri).build();
 			OAuth2TokenValidator<Jwt> jwtValidator = this.jwtValidatorFactory.apply(clientRegistration);
 			jwtDecoder.setJwtValidator(jwtValidator);
 			return jwtDecoder;

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

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2019 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -24,7 +24,7 @@ import org.springframework.security.oauth2.core.OAuth2TokenValidator;
 import org.springframework.web.client.RestTemplate;
 import org.springframework.web.util.UriComponentsBuilder;
 
-import static org.springframework.security.oauth2.jwt.JwtProcessors.withJwkSetUri;
+import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSetUri;
 
 /**
  * Allows creating a {@link JwtDecoder} from an
@@ -60,8 +60,7 @@ public final class JwtDecoders {
 		OAuth2TokenValidator<Jwt> jwtValidator =
 				JwtValidators.createDefaultWithIssuer(oidcIssuerLocation);
 
-		NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(
-				withJwkSetUri(openidConfiguration.get("jwks_uri").toString()).build());
+		NimbusJwtDecoder jwtDecoder = withJwkSetUri(openidConfiguration.get("jwks_uri").toString()).build();
 		jwtDecoder.setJwtValidator(jwtValidator);
 
 		return jwtDecoder;

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

@@ -1,238 +0,0 @@
-/*
- * 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
- *
- *      https://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.security.interfaces.RSAPublicKey;
-import java.util.Collections;
-
-import com.nimbusds.jose.JWSAlgorithm;
-import com.nimbusds.jose.jwk.JWKSet;
-import com.nimbusds.jose.jwk.RSAKey;
-import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
-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.security.oauth2.jose.jws.JwsAlgorithms;
-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);
-	}
-
-	/**
-	 * Use the given public key to validate JWTs
-	 *
-	 * @param key the public key to use
-	 * @return a {@link PublicKeyJwtProcessorBuilder} for further configurations
-	 */
-	public static PublicKeyJwtProcessorBuilder withPublicKey(RSAPublicKey key) {
-		return new PublicKeyJwtProcessorBuilder(key);
-	}
-
-	/**
-	 * 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");
-			}
-		}
-	}
-
-	/**
-	 * A builder for creating Nimbus {@link JWTProcessor} instances based on a
-	 * public key.
-	 */
-	public static final class PublicKeyJwtProcessorBuilder {
-		private JWSAlgorithm jwsAlgorithm;
-		private RSAKey key;
-
-		private PublicKeyJwtProcessorBuilder(RSAPublicKey key) {
-			Assert.notNull(key, "key cannot be null");
-			this.jwsAlgorithm = JWSAlgorithm.parse(JwsAlgorithms.RS256);
-			this.key = rsaKey(key);
-		}
-
-		private static RSAKey rsaKey(RSAPublicKey publicKey) {
-			return new RSAKey.Builder(publicKey)
-					.build();
-		}
-
-		/**
-		 * Use the given signing
-		 * <a href="https://tools.ietf.org/html/rfc7515#section-4.1.1" target="_blank">algorithm</a>.
-		 *
-		 * The value should be one of
-		 * <a href="https://tools.ietf.org/html/rfc7518#section-3.3" target="_blank">RS256, RS384, or RS512</a>.
-		 *
-		 * @param jwsAlgorithm the algorithm to use
-		 * @return a {@link JwtProcessors} for further configurations
-		 */
-		public PublicKeyJwtProcessorBuilder jwsAlgorithm(String jwsAlgorithm) {
-			Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty");
-			this.jwsAlgorithm = JWSAlgorithm.parse(jwsAlgorithm);
-			return this;
-		}
-
-		/**
-		 * Build the configured {@link JWTProcessor}.
-		 *mzRC
-		 * @return the configured {@link JWTProcessor}
-		 */
-		public JWTProcessor<SecurityContext> build() {
-			if (!JWSAlgorithm.Family.RSA.contains(this.jwsAlgorithm)) {
-				throw new IllegalStateException("The provided key is of type RSA; " +
-						"however the signature algorithm is of some other type: " +
-						this.jwsAlgorithm + ". Please indicate one of RS256, RS384, or RS512.");
-			}
-
-			JWKSet jwkSet = new JWKSet(this.key);
-			JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(jwkSet);
-			JWSKeySelector<SecurityContext> jwsKeySelector =
-					new JWSVerificationKeySelector<>(this.jwsAlgorithm, jwkSource);
-			DefaultJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
-			jwtProcessor.setJWSKeySelector(jwsKeySelector);
-
-			// Spring Security validates the claim set independent from Nimbus
-			jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { });
-
-			return jwtProcessor;
-		}
-	}
-}

+ 222 - 8
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2019 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,37 +16,54 @@
 
 package org.springframework.security.oauth2.jwt;
 
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.interfaces.RSAPublicKey;
 import java.text.ParseException;
 import java.time.Instant;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Map;
 
+import com.nimbusds.jose.JWSAlgorithm;
 import com.nimbusds.jose.RemoteKeySourceException;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
+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.JWT;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.JWTParser;
 import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
+import com.nimbusds.jwt.proc.DefaultJWTProcessor;
 import com.nimbusds.jwt.proc.JWTProcessor;
 
 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.core.OAuth2TokenValidatorResult;
+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;
 
 /**
  * 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 =
@@ -154,4 +171,201 @@ public final class NimbusJwtDecoder implements JwtDecoder {
 
 		return jwt;
 	}
+
+	/**
+	 * 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 JwkSetUriJwtDecoderBuilder} for further configurations
+	 *
+	 * @since 5.2
+	 */
+	public static JwkSetUriJwtDecoderBuilder withJwkSetUri(String jwkSetUri) {
+		return new JwkSetUriJwtDecoderBuilder(jwkSetUri);
+	}
+
+	/**
+	 * Use the given public key to validate JWTs
+	 *
+	 * @param key the public key to use
+	 * @return a {@link PublicKeyJwtDecoderBuilder} for further configurations
+	 *
+	 * @since 5.2
+	 */
+	public static PublicKeyJwtDecoderBuilder withPublicKey(RSAPublicKey key) {
+		return new PublicKeyJwtDecoderBuilder(key);
+	}
+
+	/**
+	 * A builder for creating {@link NimbusJwtDecoder} instances based on a
+	 * <a target="_blank" href="https://tools.ietf.org/html/rfc7517#section-5">JWK Set</a> uri.
+	 *
+	 * @since 5.2
+	 */
+	public static final class JwkSetUriJwtDecoderBuilder {
+		private String jwkSetUri;
+		private JWSAlgorithm jwsAlgorithm = JWSAlgorithm.RS256;
+		private RestOperations restOperations = new RestTemplate();
+
+		private JwkSetUriJwtDecoderBuilder(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 JwkSetUriJwtDecoderBuilder} for further configurations
+		 */
+		public JwkSetUriJwtDecoderBuilder 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 JwkSetUriJwtDecoderBuilder restOperations(RestOperations restOperations) {
+			Assert.notNull(restOperations, "restOperations cannot be null");
+			this.restOperations = restOperations;
+			return this;
+		}
+
+		JWTProcessor<SecurityContext> processor() {
+			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;
+		}
+
+		/**
+		 * Build the configured {@link NimbusJwtDecoder}.
+		 *
+		 * @return the configured {@link NimbusJwtDecoder}
+		 */
+		public NimbusJwtDecoder build() {
+			return new NimbusJwtDecoder(processor());
+		}
+
+		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");
+			}
+		}
+	}
+
+	/**
+	 * A builder for creating {@link NimbusJwtDecoder} instances based on a
+	 * public key.
+	 *
+	 * @since 5.2
+	 */
+	public static final class PublicKeyJwtDecoderBuilder {
+		private JWSAlgorithm jwsAlgorithm;
+		private RSAKey key;
+
+		private PublicKeyJwtDecoderBuilder(RSAPublicKey key) {
+			Assert.notNull(key, "key cannot be null");
+			this.jwsAlgorithm = JWSAlgorithm.parse(JwsAlgorithms.RS256);
+			this.key = rsaKey(key);
+		}
+
+		private static RSAKey rsaKey(RSAPublicKey publicKey) {
+			return new RSAKey.Builder(publicKey)
+					.build();
+		}
+
+		/**
+		 * Use the given signing
+		 * <a href="https://tools.ietf.org/html/rfc7515#section-4.1.1" target="_blank">algorithm</a>.
+		 *
+		 * The value should be one of
+		 * <a href="https://tools.ietf.org/html/rfc7518#section-3.3" target="_blank">RS256, RS384, or RS512</a>.
+		 *
+		 * @param jwsAlgorithm the algorithm to use
+		 * @return a {@link PublicKeyJwtDecoderBuilder} for further configurations
+		 */
+		public PublicKeyJwtDecoderBuilder jwsAlgorithm(String jwsAlgorithm) {
+			Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty");
+			this.jwsAlgorithm = JWSAlgorithm.parse(jwsAlgorithm);
+			return this;
+		}
+
+		JWTProcessor<SecurityContext> processor() {
+			if (!JWSAlgorithm.Family.RSA.contains(this.jwsAlgorithm)) {
+				throw new IllegalStateException("The provided key is of type RSA; " +
+						"however the signature algorithm is of some other type: " +
+						this.jwsAlgorithm + ". Please indicate one of RS256, RS384, or RS512.");
+			}
+
+			JWKSet jwkSet = new JWKSet(this.key);
+			JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(jwkSet);
+			JWSKeySelector<SecurityContext> jwsKeySelector =
+					new JWSVerificationKeySelector<>(this.jwsAlgorithm, jwkSource);
+			DefaultJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
+			jwtProcessor.setJWSKeySelector(jwsKeySelector);
+
+			// Spring Security validates the claim set independent from Nimbus
+			jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { });
+
+			return jwtProcessor;
+		}
+
+		/**
+		 * Build the configured {@link NimbusJwtDecoder}.
+		 *
+		 * @return the configured {@link NimbusJwtDecoder}
+		 */
+		public NimbusJwtDecoder build() {
+			return new NimbusJwtDecoder(processor());
+		}
+	}
 }

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

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2019 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -24,7 +24,7 @@ import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
 import org.springframework.util.Assert;
 import org.springframework.web.client.RestOperations;
 
-import static org.springframework.security.oauth2.jwt.JwtProcessors.withJwkSetUri;
+import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSetUri;
 
 /**
  * An implementation of a {@link JwtDecoder} that "decodes" a
@@ -49,7 +49,7 @@ import static org.springframework.security.oauth2.jwt.JwtProcessors.withJwkSetUr
  */
 @Deprecated
 public final class NimbusJwtDecoderJwkSupport implements JwtDecoder {
-	private JwtProcessors.JwkSetUriJwtProcessorBuilder jwtProcessorBuilder;
+	private NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder jwtDecoderBuilder;
 	private OAuth2TokenValidator<Jwt> jwtValidator = JwtValidators.createDefault();
 	private Converter<Map<String, Object>, Map<String, Object>> claimSetConverter =
 			MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap());
@@ -75,12 +75,12 @@ public final class NimbusJwtDecoderJwkSupport implements JwtDecoder {
 		Assert.hasText(jwkSetUrl, "jwkSetUrl cannot be empty");
 		Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty");
 
-		this.jwtProcessorBuilder = withJwkSetUri(jwkSetUrl).jwsAlgorithm(jwsAlgorithm);
+		this.jwtDecoderBuilder = withJwkSetUri(jwkSetUrl).jwsAlgorithm(jwsAlgorithm);
 		this.delegate = makeDelegate();
 	}
 
 	private NimbusJwtDecoder makeDelegate() {
-		NimbusJwtDecoder delegate = new NimbusJwtDecoder(this.jwtProcessorBuilder.build());
+		NimbusJwtDecoder delegate = this.jwtDecoderBuilder.build();
 		delegate.setClaimSetConverter(this.claimSetConverter);
 		delegate.setJwtValidator(this.jwtValidator);
 		return delegate;
@@ -121,7 +121,7 @@ public final class NimbusJwtDecoderJwkSupport implements JwtDecoder {
 	 */
 	public final void setRestOperations(RestOperations restOperations) {
 		Assert.notNull(restOperations, "restOperations cannot be null");
-		this.jwtProcessorBuilder = this.jwtProcessorBuilder.restOperations(restOperations);
+		this.jwtDecoderBuilder = this.jwtDecoderBuilder.restOperations(restOperations);
 		this.delegate = makeDelegate();
 	}
 }

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

@@ -1,149 +0,0 @@
-/*
- * 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
- *
- *      https://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.security.KeyFactory;
-import java.security.NoSuchAlgorithmException;
-import java.security.interfaces.RSAPublicKey;
-import java.security.spec.EncodedKeySpec;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.X509EncodedKeySpec;
-import java.util.Base64;
-
-import com.nimbusds.jose.proc.BadJOSEException;
-import com.nimbusds.jose.proc.SecurityContext;
-import com.nimbusds.jwt.JWTClaimsSet;
-import com.nimbusds.jwt.proc.JWTProcessor;
-import org.junit.BeforeClass;
-import org.junit.Test;
-
-import org.springframework.http.HttpStatus;
-import org.springframework.http.RequestEntity;
-import org.springframework.http.ResponseEntity;
-import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
-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.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.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\":\"5VhDhCcOp9D3krJi2W2uF-LPovIKqtU59Jdt_gF6iwPw_0Bgo8UlbFRv3HdnVYUExLdkvHOoA9bmbkV0w_6TUwLP65PKPu6P8JfeWsIuGi3wY91_CsfahxGmVZxlZookBUIMMnylvM9fGR6-daNgkl31CKqDpkJt9XF35yjVpbM\",\"kty\":\"RSA\",\"q\":\"v3yrgOhimojmLVsLBdynmw_pfAlZPw2eXVzoJ514xP94UZJQRY_NOUjYV0O9Vqict_Qv42sUa-uurY8n_0Btslt--iJsMyTHYMIKjbyeFAqAGFuXYbQPnorEOkuZhT1NIBZhlLLuKSD8DVCtsEv2EVgBTwyzFJ6QbXLqVpNUvZs\",\"d\":\"ZyjCopsw7TszSV4qMIyunb0PaGfHbQ_0LJcAxhNwQsf3MYR6j0J9k1GVxq2SjpRylgKJg8CKjySaU4frewH7MEaNLNdfR2_XMFSKW3KFggdNRtW1TFwjcHfpBLTvB3MEaTx56Sohn0eXqd_Wa2EAfRiLjllwOeqwXqgdSXdvKfkkmV2DBZ2h100wLJB87Y5kvlGvbNDs0KgTFaPWRZkCQz3CGhGPDyTJVwzgJvIxbHgzl9DnW6FlgrP_DZmyfGbJ833FZSiBczTQGDWT7euR3h491fKPCHTXjdULtU1578NldRAo8SOXH4ThXXA_kwKafIGlKx5LZPNwMWgNuVvE6Q\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"qi\":\"eaGTNLhJf1K82YqB6VKrYWz1hxsKnjRBg-V-kuWJXvW7HQsLFKx56kXy_ximz_IQDZOO3F-rW_7Saz3RvWuFt_Yq7sRcLCMtpiDRbZ-nGDgHxQHedtLoalLLPmJkMMsZwZzXf9l6LO6a8r30lrC_C-kPY5K7lz97ZToKeper7c8\",\"dp\":\"QQJ4-O_dTqKEWvfn3zwg2jJ3qvezIGOarwNxsUuYAenXGXOVMTcD-aYhozvRdcNj66MUkfqyyIvU-7MCe0AhYKluaJeW_6m98XQLGmzqho85EgXKKjMmdZ0CKkhP0fYcacUkEfeVP2UEzukREeWCzVqGx7MV6D3yT12foE3J6dM\",\"dq\":\"PsH2V5ZSEsHBZqYLE83ApMJvTHan6FFnUMQNVkZ2-WGdJmbkphe-NAMa3GbYHBnA201NkKRcmg4xPrLHchHEogr4r7QucAiiy6Rs3w0tZfYXC2ShVaU05Uoni8-RLijsKRMMwjZudc5YrWh-tGQA7qhALY9E9gIN5cEe6mb5A_c\",\"n\":\"q4yKxb6SNePdDmQi9xFCrP6QvHosErQzryknQTTTffs0t3cy3Er3lIceuhZ7yQNSCDfPFqG8GoyoKhuChRiA5D-J2ab7bqTa1QJKfnCyERoscftgN2fXPHjHoiKbpGV2tMVw8mXl__tePOAiKbMJaBUnlAvJgkk1rVm08dSwpLC1sr2M19euf9jwnRGkMRZuhp9iCPgECRke5T8Ixpv0uQjSmGHnWUKTFlbj8sM83suROR1Ue64JSGScANc5vk3huJ_J97qTC-K2oKj6L8d9O8dpc4obijEOJwpydNvTYDgbiivYeSB00KS9jlBkQ5B2QqLvLVEygDl3dp59nGx6YQ\"}]}";
-	private static final String JWK_SET_URI = "http://issuer/.well-known/jwks.json";
-	private static final String RS512_SIGNED_JWT = "eyJhbGciOiJSUzUxMiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJleHAiOjE5NzQzMjYxMTl9.LKAx-60EBfD7jC1jb1eKcjO4uLvf3ssISV-8tN-qp7gAjSvKvj4YA9-V2mIb6jcS1X_xGmNy6EIimZXpWaBR3nJmeu-jpe85u4WaW2Ztr8ecAi-dTO7ZozwdtljKuBKKvj4u1nF70zyCNl15AozSG0W1ASrjUuWrJtfyDG6WoZ8VfNMuhtU-xUYUFvscmeZKUYQcJ1KS-oV5tHeF8aNiwQoiPC_9KXCOZtNEJFdq6-uzFdHxvOP2yex5Gbmg5hXonauIFXG2ZPPGdXzm-5xkhBpgM8U7A_6wb3So8wBvLYYm2245QUump63AJRAy8tQpwt4n9MvQxQgS3z9R-NK92A";
-	private static final String RS256_SIGNED_JWT = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJleHAiOjE5NzQzMjYzMzl9.CT-H2OWEqmSs1NWmnta5ealLFvM8OlbQTjGhfRcKLNxrTrzsOkqBJl-AN3k16BQU7mS32o744TiiZ29NcDlxPsr1MqTlN86-dobPiuNIDLp3A1bOVdXMcVFuMYkrNv0yW0tGS9OjEqsCCuZDkZ1by6AhsHLbGwRY-6AQdcRouZygGpOQu1hNun5j8q5DpSTY4AXKARIFlF-O3OpVbPJ0ebr3Ki-i3U9p_55H0e4-wx2bqcApWlqgofl1I8NKWacbhZgn81iibup2W7E0CzCzh71u1Mcy3xk1sYePx-dwcxJnHmxJReBBWjJZEAeCrkbnn_OCuo2fA-EQyNJtlN5F2w";
-	private static final String VERIFY_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq4yKxb6SNePdDmQi9xFCrP6QvHosErQzryknQTTTffs0t3cy3Er3lIceuhZ7yQNSCDfPFqG8GoyoKhuChRiA5D+J2ab7bqTa1QJKfnCyERoscftgN2fXPHjHoiKbpGV2tMVw8mXl//tePOAiKbMJaBUnlAvJgkk1rVm08dSwpLC1sr2M19euf9jwnRGkMRZuhp9iCPgECRke5T8Ixpv0uQjSmGHnWUKTFlbj8sM83suROR1Ue64JSGScANc5vk3huJ/J97qTC+K2oKj6L8d9O8dpc4obijEOJwpydNvTYDgbiivYeSB00KS9jlBkQ5B2QqLvLVEygDl3dp59nGx6YQIDAQAB";
-
-	private static KeyFactory kf;
-
-	@BeforeClass
-	public static void keyFactory() throws NoSuchAlgorithmException {
-		kf = KeyFactory.getInstance("RSA");
-	}
-
-	@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(RS256_SIGNED_JWT, null))
-				.extracting(JWTClaimsSet::getExpirationTime)
-				.isNotNull();
-		verify(restOperations).exchange(any(RequestEntity.class), eq(String.class));
-	}
-
-	@Test
-	public void withPublicKeyWhenNullThenThrowsException() {
-		assertThatThrownBy(() -> JwtProcessors.withPublicKey(null))
-				.isInstanceOf(IllegalArgumentException.class);
-	}
-
-	@Test
-	public void buildWhenSignatureAlgorithmMismatchesKeyTypeThenThrowsException() {
-		assertThatCode(() -> JwtProcessors.withPublicKey(key())
-				.jwsAlgorithm(JwsAlgorithms.ES256)
-				.build())
-				.isInstanceOf(IllegalStateException.class);
-	}
-
-	@Test
-	public void processWhenUsingPublicKeyThenSuccessfullyDecodes() throws Exception {
-		JWTProcessor<SecurityContext> processor = JwtProcessors.withPublicKey(key()).build();
-		assertThat(processor.process(RS256_SIGNED_JWT, null))
-				.extracting(JWTClaimsSet::getSubject)
-				.isEqualTo("test-subject");
-	}
-
-	@Test
-	public void processWhenUsingPublicKeyWithRs512ThenSuccessfullyDecodes() throws Exception {
-		JWTProcessor<SecurityContext> processor = JwtProcessors
-				.withPublicKey(key()).jwsAlgorithm(JwsAlgorithms.RS512).build();
-		assertThat(processor.process(RS512_SIGNED_JWT, null))
-				.extracting(JWTClaimsSet::getSubject)
-				.isEqualTo("test-subject");
-	}
-
-	@Test
-	public void processWhenSignatureMismatchesAlgorithmThenThrowsException() throws Exception {
-		JWTProcessor<SecurityContext> processor = JwtProcessors
-				.withPublicKey(key()).jwsAlgorithm(JwsAlgorithms.RS512).build();
-		assertThatCode(() -> processor.process(RS256_SIGNED_JWT, null))
-				.isInstanceOf(BadJOSEException.class);
-	}
-
-	private RSAPublicKey key() throws InvalidKeySpecException {
-		byte[] decoded = Base64.getDecoder().decode(VERIFY_KEY.getBytes());
-		EncodedKeySpec spec = new X509EncodedKeySpec(decoded);
-		return (RSAPublicKey) kf.generatePublic(spec);
-	}
-
-	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;
-	}
-}

+ 88 - 5
oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2019 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,8 +16,15 @@
 
 package org.springframework.security.oauth2.jwt;
 
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.EncodedKeySpec;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
 import java.text.ParseException;
 import java.util.Arrays;
+import java.util.Base64;
 import java.util.Collections;
 import java.util.Map;
 
@@ -31,6 +38,7 @@ import com.nimbusds.jwt.proc.DefaultJWTProcessor;
 import com.nimbusds.jwt.proc.JWTProcessor;
 import okhttp3.mockwebserver.MockWebServer;
 import org.assertj.core.api.Assertions;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import org.springframework.core.convert.converter.Converter;
@@ -40,6 +48,7 @@ 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 static org.assertj.core.api.Assertions.assertThat;
@@ -49,7 +58,8 @@ 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;
+import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSetUri;
+import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withPublicKey;
 
 /**
  * Tests for {@link NimbusJwtDecoder}
@@ -65,8 +75,20 @@ public class NimbusJwtDecoderTests {
 	private static final String UNSIGNED_JWT = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJleHAiOi0yMDMzMjI0OTcsImp0aSI6IjEyMyIsInR5cCI6IkpXVCJ9.";
 	private static final String EMPTY_EXP_CLAIM_JWT = "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJhdWRpZW5jZSJ9.D1eT0jpBEpuh74p-YT-uF81Z7rkVqIpUtJ5hWWFiVShZ9s8NIntK4Q1GlvlziiySSaVYaXtpTmDB3c8r-Z5Mj4ibihiueCSq7jaPD3sA8IMQKL-L6Uol8MSD_lSFE2n3fVBTxFeaejBKfZsDxnhzgpy8g7PncR47w8NHs-7tKO4qw7G_SV3hkNpDNoqZTfMImxyWEebgKM2pJAhN4das2CO1KAjYMfEByLcgYncE8fzdYPJhMFo2XRRSQABoeUBuKSAwIntBaOGvcb-qII_Hefc5U0cmpNItG75F2XfX803plKI4FFpAxJsbPKWSQmhs6bZOrhx0x74pY5LS3ghmJw";
 
+	private static final String JWK_SET_URI = "http://issuer/.well-known/jwks.json";
+	private static final String RS512_SIGNED_JWT = "eyJhbGciOiJSUzUxMiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJleHAiOjE5NzQzMjYxMTl9.LKAx-60EBfD7jC1jb1eKcjO4uLvf3ssISV-8tN-qp7gAjSvKvj4YA9-V2mIb6jcS1X_xGmNy6EIimZXpWaBR3nJmeu-jpe85u4WaW2Ztr8ecAi-dTO7ZozwdtljKuBKKvj4u1nF70zyCNl15AozSG0W1ASrjUuWrJtfyDG6WoZ8VfNMuhtU-xUYUFvscmeZKUYQcJ1KS-oV5tHeF8aNiwQoiPC_9KXCOZtNEJFdq6-uzFdHxvOP2yex5Gbmg5hXonauIFXG2ZPPGdXzm-5xkhBpgM8U7A_6wb3So8wBvLYYm2245QUump63AJRAy8tQpwt4n9MvQxQgS3z9R-NK92A";
+	private static final String RS256_SIGNED_JWT = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJleHAiOjE5NzQzMjYzMzl9.CT-H2OWEqmSs1NWmnta5ealLFvM8OlbQTjGhfRcKLNxrTrzsOkqBJl-AN3k16BQU7mS32o744TiiZ29NcDlxPsr1MqTlN86-dobPiuNIDLp3A1bOVdXMcVFuMYkrNv0yW0tGS9OjEqsCCuZDkZ1by6AhsHLbGwRY-6AQdcRouZygGpOQu1hNun5j8q5DpSTY4AXKARIFlF-O3OpVbPJ0ebr3Ki-i3U9p_55H0e4-wx2bqcApWlqgofl1I8NKWacbhZgn81iibup2W7E0CzCzh71u1Mcy3xk1sYePx-dwcxJnHmxJReBBWjJZEAeCrkbnn_OCuo2fA-EQyNJtlN5F2w";
+	private static final String VERIFY_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq4yKxb6SNePdDmQi9xFCrP6QvHosErQzryknQTTTffs0t3cy3Er3lIceuhZ7yQNSCDfPFqG8GoyoKhuChRiA5D+J2ab7bqTa1QJKfnCyERoscftgN2fXPHjHoiKbpGV2tMVw8mXl//tePOAiKbMJaBUnlAvJgkk1rVm08dSwpLC1sr2M19euf9jwnRGkMRZuhp9iCPgECRke5T8Ixpv0uQjSmGHnWUKTFlbj8sM83suROR1Ue64JSGScANc5vk3huJ/J97qTC+K2oKj6L8d9O8dpc4obijEOJwpydNvTYDgbiivYeSB00KS9jlBkQ5B2QqLvLVEygDl3dp59nGx6YQIDAQAB";
+
+	private static KeyFactory kf;
+
 	NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(withoutSigning());
 
+	@BeforeClass
+	public static void keyFactory() throws NoSuchAlgorithmException {
+		kf = KeyFactory.getInstance("RSA");
+	}
+
 	@Test
 	public void constructorWhenJwtProcessorIsNullThenThrowIllegalArgumentException() {
 		assertThatThrownBy(() -> new NimbusJwtDecoder(null))
@@ -180,8 +202,7 @@ public class NimbusJwtDecoderTests {
 	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).build());
+			NimbusJwtDecoder jwtDecoder = withJwkSetUri(jwkSetUri).build();
 
 			server.shutdown();
 			assertThatCode(() -> jwtDecoder.decode(SIGNED_JWT))
@@ -190,13 +211,75 @@ public class NimbusJwtDecoderTests {
 		}
 	}
 
+	@Test
+	public void withJwkSetUriWhenNullOrEmptyThenThrowsException() {
+		Assertions.assertThatCode(() -> withJwkSetUri(null)).isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void jwsAlgorithmWhenNullOrEmptyThenThrowsException() {
+		NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder builder = withJwkSetUri(JWK_SET_URI);
+		Assertions.assertThatCode(() -> builder.jwsAlgorithm(null)).isInstanceOf(IllegalArgumentException.class);
+		Assertions.assertThatCode(() -> builder.jwsAlgorithm("")).isInstanceOf(IllegalArgumentException.class);
+		Assertions.assertThatCode(() -> builder.jwsAlgorithm("RS4096")).doesNotThrowAnyException();
+	}
+
+	@Test
+	public void restOperationsWhenNullThenThrowsException() {
+		NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder builder = withJwkSetUri(JWK_SET_URI);
+		Assertions.assertThatCode(() -> builder.restOperations(null)).isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void withPublicKeyWhenNullThenThrowsException() {
+		assertThatThrownBy(() -> withPublicKey(null))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void buildWhenSignatureAlgorithmMismatchesKeyTypeThenThrowsException() {
+		Assertions.assertThatCode(() -> withPublicKey(key())
+				.jwsAlgorithm(JwsAlgorithms.ES256)
+				.build())
+				.isInstanceOf(IllegalStateException.class);
+	}
+
+	@Test
+	public void decodeWhenUsingPublicKeyThenSuccessfullyDecodes() throws Exception {
+		NimbusJwtDecoder decoder = withPublicKey(key()).build();
+		assertThat(decoder.decode(RS256_SIGNED_JWT))
+				.extracting(Jwt::getSubject)
+				.isEqualTo("test-subject");
+	}
+
+	@Test
+	public void decodeWhenUsingPublicKeyWithRs512ThenSuccessfullyDecodes() throws Exception {
+		NimbusJwtDecoder decoder = withPublicKey(key()).jwsAlgorithm(JwsAlgorithms.RS512).build();
+		assertThat(decoder.decode(RS512_SIGNED_JWT))
+				.extracting(Jwt::getSubject)
+				.isEqualTo("test-subject");
+	}
+
+	@Test
+	public void decodeWhenSignatureMismatchesAlgorithmThenThrowsException() throws Exception {
+		NimbusJwtDecoder decoder = withPublicKey(key()).jwsAlgorithm(JwsAlgorithms.RS512).build();
+		Assertions.assertThatCode(() -> decoder.decode(RS256_SIGNED_JWT))
+				.isInstanceOf(JwtException.class);
+	}
+
+	private RSAPublicKey key() throws InvalidKeySpecException {
+		byte[] decoded = Base64.getDecoder().decode(VERIFY_KEY.getBytes());
+		EncodedKeySpec spec = new X509EncodedKeySpec(decoded);
+		return (RSAPublicKey) kf.generatePublic(spec);
+	}
+
 	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();
+				.processor();
 	}
 
 	private static JWTProcessor<SecurityContext> withoutSigning() {

+ 2 - 3
samples/boot/oauth2resourceserver-static/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2019 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -25,7 +25,6 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 import org.springframework.security.oauth2.jwt.JwtDecoder;
-import org.springframework.security.oauth2.jwt.JwtProcessors;
 import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
 
 /**
@@ -50,7 +49,7 @@ public class OAuth2ResourceServerSecurityConfiguration extends WebSecurityConfig
 
 	@Bean
 	JwtDecoder jwtDecoder() throws Exception {
-		return new NimbusJwtDecoder(JwtProcessors.withPublicKey(key()).build());
+		return NimbusJwtDecoder.withPublicKey(key()).build();
 	}
 
 	private RSAPublicKey key() throws Exception {