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); }