Browse Source

Add NimbusReactiveJwtDecoder RSAPublicKey Support

Fixes: gh-5460
Rob Winch 7 years ago
parent
commit
8ef4a5ba92

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

@@ -38,7 +38,7 @@ import org.springframework.security.oauth2.core.oidc.OidcIdToken;
 import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
 import org.springframework.security.oauth2.core.oidc.user.OidcUser;
 import org.springframework.security.oauth2.core.user.OAuth2User;
-import org.springframework.security.oauth2.jwt.NimbusJwkReactiveJwtDecoder;
+import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
 import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
@@ -220,7 +220,7 @@ public class OidcReactiveAuthenticationManager implements
 					);
 					throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
 				}
-				jwtDecoder = new NimbusJwkReactiveJwtDecoder(clientRegistration.getProviderDetails().getJwkSetUri());
+				jwtDecoder = new NimbusReactiveJwtDecoder(clientRegistration.getProviderDetails().getJwkSetUri());
 				this.jwtDecoders.put(clientRegistration.getRegistrationId(), jwtDecoder);
 			}
 			return jwtDecoder;

+ 27 - 13
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoder.java → oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java

@@ -19,6 +19,9 @@ import com.nimbusds.jose.JOSEException;
 import com.nimbusds.jose.JWSAlgorithm;
 import com.nimbusds.jose.jwk.JWK;
 import com.nimbusds.jose.jwk.JWKSelector;
+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.proc.BadJOSEException;
 import com.nimbusds.jose.proc.JWSKeySelector;
@@ -33,6 +36,7 @@ import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
 import org.springframework.util.Assert;
 import reactor.core.publisher.Mono;
 
+import java.security.interfaces.RSAPublicKey;
 import java.time.Instant;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -55,32 +59,37 @@ import java.util.Map;
  * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7517">JSON Web Key (JWK)</a>
  * @see <a target="_blank" href="https://connect2id.com/products/nimbus-jose-jwt">Nimbus JOSE + JWT SDK</a>
  */
-public final class NimbusJwkReactiveJwtDecoder implements ReactiveJwtDecoder {
+public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 	private final JWTProcessor<JWKContext> jwtProcessor;
 
-	private final ReactiveRemoteJWKSource reactiveJwkSource;
+	private final ReactiveJWKSource reactiveJwkSource;
 
 	private final JWKSelectorFactory jwkSelectorFactory;
 
-	/**
-	 * Constructs a {@code NimbusJwtDecoderJwkSupport} using the provided parameters.
-	 *
-	 * @param jwkSetUrl the JSON Web Key (JWK) Set {@code URL}
-	 */
-	public NimbusJwkReactiveJwtDecoder(String jwkSetUrl) {
-		this(jwkSetUrl, JwsAlgorithms.RS256);
+	public NimbusReactiveJwtDecoder(RSAPublicKey publicKey) {
+		JWSAlgorithm algorithm = JWSAlgorithm.parse(JwsAlgorithms.RS256);
+
+		RSAKey rsaKey = rsaKey(publicKey);
+		JWKSet jwkSet = new JWKSet(rsaKey);
+		JWKSource jwkSource = new ImmutableJWKSet<>(jwkSet);
+		JWSKeySelector<JWKContext> jwsKeySelector =
+				new JWSVerificationKeySelector<>(algorithm, jwkSource);
+		DefaultJWTProcessor jwtProcessor = new DefaultJWTProcessor<>();
+		jwtProcessor.setJWSKeySelector(jwsKeySelector);
+
+		this.jwtProcessor = jwtProcessor;
+		this.reactiveJwkSource = new ReactiveJWKSourceAdapter(jwkSource);
+		this.jwkSelectorFactory = new JWKSelectorFactory(algorithm);
 	}
 
 	/**
 	 * Constructs a {@code NimbusJwtDecoderJwkSupport} using the provided parameters.
 	 *
 	 * @param jwkSetUrl the JSON Web Key (JWK) Set {@code URL}
-	 * @param jwsAlgorithm the JSON Web Algorithm (JWA) used for verifying the digital signatures
 	 */
-	public NimbusJwkReactiveJwtDecoder(String jwkSetUrl, String jwsAlgorithm) {
+	public NimbusReactiveJwtDecoder(String jwkSetUrl) {
 		Assert.hasText(jwkSetUrl, "jwkSetUrl cannot be empty");
-		Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty");
-
+		String jwsAlgorithm = JwsAlgorithms.RS256;
 		JWSAlgorithm algorithm = JWSAlgorithm.parse(jwsAlgorithm);
 		JWKSource jwkSource = new JWKContextJWKSource();
 		JWSKeySelector<JWKContext> jwsKeySelector =
@@ -152,4 +161,9 @@ public final class NimbusJwkReactiveJwtDecoder implements ReactiveJwtDecoder {
 
 		return new Jwt(parsedJwt.getParsedString(), issuedAt, expiresAt, headers, jwtClaimsSet.getClaims());
 	}
+
+	private static RSAKey rsaKey(RSAPublicKey publicKey) {
+		return new RSAKey.Builder(publicKey)
+				.build();
+	}
 }

+ 32 - 0
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJWKSource.java

@@ -0,0 +1,32 @@
+/*
+ * 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.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKSelector;
+import reactor.core.publisher.Mono;
+
+import java.util.List;
+
+/**
+ * A reactive version of {@link com.nimbusds.jose.jwk.source.JWKSource}
+ * @author Rob Winch
+ * @since 5.1
+ */
+interface ReactiveJWKSource {
+	Mono<List<JWK>> get(JWKSelector jwkSelector);
+}

+ 47 - 0
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJWKSourceAdapter.java

@@ -0,0 +1,47 @@
+/*
+ * 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.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKSelector;
+import com.nimbusds.jose.jwk.source.JWKSource;
+import com.nimbusds.jose.proc.SecurityContext;
+import reactor.core.publisher.Mono;
+
+import java.util.List;
+
+/**
+ * Adapts a {@link JWKSource} to a {@link ReactiveJWKSource} which must be non-blocking.
+ * @author Rob Winch
+ * @since 5.1
+ */
+class ReactiveJWKSourceAdapter implements ReactiveJWKSource {
+	private final JWKSource<SecurityContext> source;
+
+	/**
+	 * Creates a new instance
+	 * @param source
+	 */
+	ReactiveJWKSourceAdapter(JWKSource<SecurityContext> source) {
+		this.source = source;
+	}
+
+	@Override
+	public Mono<List<JWK>> get(JWKSelector jwkSelector) {
+		return Mono.fromCallable(() -> this.source.get(jwkSelector, null));
+	}
+}

+ 2 - 2
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSource.java

@@ -34,7 +34,7 @@ import java.util.concurrent.atomic.AtomicReference;
  * @author Rob Winch
  * @since 5.1
  */
-class ReactiveRemoteJWKSource {
+class ReactiveRemoteJWKSource implements ReactiveJWKSource {
 	/**
 	 * The cached JWK set.
 	 */
@@ -48,7 +48,7 @@ class ReactiveRemoteJWKSource {
 		this.jwkSetURL = jwkSetURL;
 	}
 
-	Mono<List<JWK>> get(JWKSelector jwkSelector) {
+	public Mono<List<JWK>> get(JWKSelector jwkSelector) {
 		return this.cachedJWKSet.get()
 				.switchIfEmpty(getJWKSet())
 				.flatMap(jwkSet -> get(jwkSelector, jwkSet))

+ 20 - 4
oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoderTests.java → oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java

@@ -22,6 +22,10 @@ import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
+import java.security.KeyFactory;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
 import java.util.Date;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -31,7 +35,7 @@ import static org.assertj.core.api.Assertions.assertThatCode;
  * @author Rob Winch
  * @since 5.1
  */
-public class NimbusJwkReactiveJwtDecoderTests {
+public class NimbusReactiveJwtDecoderTests {
 
 	private String expired = "eyJraWQiOiJrZXktaWQtMSIsImFsZyI6IlJTMjU2In0.eyJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6MTUyOTkzNzYzMX0.Dt5jFOKkB8zAmjciwvlGkj4LNStXWH0HNIfr8YYajIthBIpVgY5Hg_JL8GBmUFzKDgyusT0q60OOg8_Pdi4Lu-VTWyYutLSlNUNayMlyBaVEWfyZJnh2_OwMZr1vRys6HF-o1qZldhwcfvczHg61LwPa1ISoqaAltDTzBu9cGISz2iBUCuR0x71QhbuRNyJdjsyS96NqiM_TspyiOSxmlNch2oAef1MssOQ23CrKilIvEDsz_zk5H94q7rH0giWGdEHCENESsTJS0zvzH6r2xIWjd5WnihFpCPkwznEayxaEhrdvJqT_ceyXCIfY4m3vujPQHNDG0UshpwvDuEbPUg";
 
@@ -51,14 +55,14 @@ public class NimbusJwkReactiveJwtDecoderTests {
 		+ "}";
 
 	private MockWebServer server;
-	private NimbusJwkReactiveJwtDecoder decoder;
+	private NimbusReactiveJwtDecoder decoder;
 
 	@Before
 	public void setup() throws Exception {
 		this.server = new MockWebServer();
 		this.server.start();
 		this.server.enqueue(new MockResponse().setBody(jwkSet));
-		this.decoder = new NimbusJwkReactiveJwtDecoder(this.server.url("/certs").toString());
+		this.decoder = new NimbusReactiveJwtDecoder(this.server.url("/certs").toString());
 	}
 
 	@After
@@ -73,6 +77,18 @@ public class NimbusJwkReactiveJwtDecoderTests {
 		assertThat(jwt.getClaims().get("scope")).isEqualTo("message:read");
 	}
 
+	@Test
+	public void decodeWhenRSAPublicKeyThenSuccess() throws Exception {
+		byte[] bytes = Base64.getDecoder().decode("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqL48v1clgFw+Evm145pmh8nRYiNt72Gupsshn7Qs8dxEydCRp1DPOV/PahPk1y2nvldBNIhfNL13JOAiJ6BTiF+2ICuICAhDArLMnTH61oL1Hepq8W1xpa9gxsnL1P51thvfmiiT4RTW57koy4xIWmIp8ZXXfYgdH2uHJ9R0CQBuYKe7nEOObjxCFWC8S30huOfW2cYtv0iB23h6w5z2fDLjddX6v/FXM7ktcokgpm3/XmvT/+bL6/GGwz9k6kJOyMTubecr+WT//le8ikY66zlplYXRQh6roFfFCL21Pt8xN5zrk+0AMZUnmi8F2S2ztSBmAVJ7H71ELXsURBVZpwIDAQAB");
+		RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA")
+				.generatePublic(new X509EncodedKeySpec(bytes));
+		this.decoder = new NimbusReactiveJwtDecoder(publicKey);
+		String noKeyId = "eyJhbGciOiJSUzI1NiJ9.eyJzY29wZSI6IiIsImV4cCI6OTIyMzM3MjAwNjA5NjM3NX0.hNVuHSUkxdLZrDfqdmKcOi0ggmNaDuB4ZPxPtJl1gwBiXzIGN6Hwl24O2BfBZiHFKUTQDs4_RvzD71mEG3DvUrcKmdYWqIB1l8KNmxQLUDG-cAPIpJmRJgCh50tf8OhOE_Cb9E1HcsOUb47kT9iz-VayNBcmo6BmyZLdEGhsdGBrc3Mkz2dd_0PF38I2Hf_cuSjn9gBjFGtiPEXJvob3PEjVTSx_zvodT8D9p3An1R3YBZf5JSd1cQisrXgDX2k1Jmf7UKKWzgfyCgnEtRWWbsUdPqo3rSEY9GDC1iSQXsFTTC1FT_JJDkwzGf011fsU5O_Ko28TARibmKTCxAKNRQ";
+
+		assertThatCode(() -> this.decoder.decode(noKeyId).block())
+			.doesNotThrowAnyException();
+	}
+
 	@Test
 	public void decodeWhenIssuedAtThenSuccess() {
 		String withIssuedAt = "eyJraWQiOiJrZXktaWQtMSIsImFsZyI6IlJTMjU2In0.eyJzY29wZSI6IiIsImV4cCI6OTIyMzM3MjAwNjA5NjM3NSwiaWF0IjoxNTI5OTQyNDQ4fQ.LBzAJO-FR-uJDHST61oX4kimuQjz6QMJPW_mvEXRB6A-fMQWpfTQ089eboipAqsb33XnwWth9ELju9HMWLk0FjlWVVzwObh9FcoKelmPNR8mZIlFG-pAYGgSwi8HufyLabXHntFavBiFtqwp_z9clSOFK1RxWvt3lywEbGgtCKve0BXOjfKWiH1qe4QKGixH-NFxidvz8Qd5WbJwyb9tChC6ZKoKPv7Jp-N5KpxkY-O2iUtINvn4xOSactUsvKHgF8ZzZjvJGzG57r606OZXaNtoElQzjAPU5xDGg5liuEJzfBhvqiWCLRmSuZ33qwp3aoBnFgEw0B85gsNe3ggABg";
@@ -96,7 +112,7 @@ public class NimbusJwkReactiveJwtDecoderTests {
 
 	@Test
 	public void decodeWhenInvalidJwkSetUrlThenFail() {
-		this.decoder = new NimbusJwkReactiveJwtDecoder("http://localhost:1280/certs");
+		this.decoder = new NimbusReactiveJwtDecoder("http://localhost:1280/certs");
 		assertThatCode(() -> this.decoder.decode(this.messageReadToken).block())
 				.isInstanceOf(JwtException.class);
 	}