diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManager.java
index dfb30ca3c0..c7cd9f8959 100644
--- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManager.java
+++ b/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;
diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java
similarity index 81%
rename from oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoder.java
rename to oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java
index 7a16daf188..be428ab0e3 100644
--- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoder.java
+++ b/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 JSON Web Key (JWK)
* @see Nimbus JOSE + JWT SDK
*/
-public final class NimbusJwkReactiveJwtDecoder implements ReactiveJwtDecoder {
+public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
private final JWTProcessor 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 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 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();
+ }
}
diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJWKSource.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJWKSource.java
new file mode 100644
index 0000000000..32b84b88bc
--- /dev/null
+++ b/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> get(JWKSelector jwkSelector);
+}
diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJWKSourceAdapter.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJWKSourceAdapter.java
new file mode 100644
index 0000000000..309ae578cd
--- /dev/null
+++ b/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 source;
+
+ /**
+ * Creates a new instance
+ * @param source
+ */
+ ReactiveJWKSourceAdapter(JWKSource source) {
+ this.source = source;
+ }
+
+ @Override
+ public Mono> get(JWKSelector jwkSelector) {
+ return Mono.fromCallable(() -> this.source.get(jwkSelector, null));
+ }
+}
diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSource.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSource.java
index c26a3a9f4b..0079c8b863 100644
--- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSource.java
+++ b/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> get(JWKSelector jwkSelector) {
+ public Mono> get(JWKSelector jwkSelector) {
return this.cachedJWKSet.get()
.switchIfEmpty(getJWKSet())
.flatMap(jwkSet -> get(jwkSelector, jwkSet))
diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoderTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java
similarity index 75%
rename from oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoderTests.java
rename to oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java
index ef38f1917f..3b93935c59 100644
--- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoderTests.java
+++ b/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);
}