Add NimbusReactiveJwtDecoder RSAPublicKey Support

Fixes: gh-5460
This commit is contained in:
Rob Winch 2018-06-25 20:42:42 -05:00
parent d32aa3c6d6
commit 8ef4a5ba92
6 changed files with 130 additions and 21 deletions

View File

@ -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;

View File

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

View File

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

View File

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

View File

@ -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))

View File

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