Add NimbusReactiveJwtDecoder RSAPublicKey Support
Fixes: gh-5460
This commit is contained in:
parent
d32aa3c6d6
commit
8ef4a5ba92
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
|
|
|
@ -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);
|
||||
}
|
Loading…
Reference in New Issue