Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nimbus reactive decoder accept Nimbus JWTProcessor #6367

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,37 +15,22 @@
*/
package org.springframework.security.oauth2.jwt;

import java.security.interfaces.RSAPublicKey;
import java.time.Instant;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

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;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.JWTParser;
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import com.nimbusds.jwt.proc.JWTProcessor;
import reactor.core.publisher.Mono;

import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
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.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* An implementation of a {@link ReactiveJwtDecoder} that "decodes" a
Expand All @@ -64,56 +49,23 @@
* @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 NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
private final JWTProcessor<JWKContext> jwtProcessor;

private final ReactiveJWKSource reactiveJwkSource;

private final JWKSelectorFactory jwkSelectorFactory;
public final class NimbusReactiveJwtDecoder<T extends SecurityContext> implements ReactiveJwtDecoder {
private final ReactiveJWTProcessor jwtProcessor;

private OAuth2TokenValidator<Jwt> jwtValidator = JwtValidators.createDefault();
private Converter<Map<String, Object>, Map<String, Object>> claimSetConverter = MappedJwtClaimSetConverter
.withDefaults(Collections.emptyMap());

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);
jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {});

this.jwtProcessor = jwtProcessor;
this.reactiveJwkSource = new ReactiveJWKSourceAdapter(jwkSource);
this.jwkSelectorFactory = new JWKSelectorFactory(algorithm);
public NimbusReactiveJwtDecoder(String jwksUri){
this(new ReactiveJWKSJWTProcessor(jwksUri));
}

/**
* Constructs a {@code NimbusJwtDecoderJwkSupport} using the provided parameters.
*
* @param jwkSetUrl the JSON Web Key (JWK) Set {@code URL}
*/
public NimbusReactiveJwtDecoder(String jwkSetUrl) {
Assert.hasText(jwkSetUrl, "jwkSetUrl cannot be empty");
String jwsAlgorithm = JwsAlgorithms.RS256;
JWSAlgorithm algorithm = JWSAlgorithm.parse(jwsAlgorithm);
JWKSource jwkSource = new JWKContextJWKSource();
JWSKeySelector<JWKContext> jwsKeySelector =
new JWSVerificationKeySelector<>(algorithm, jwkSource);

DefaultJWTProcessor<JWKContext> jwtProcessor = new DefaultJWTProcessor<>();
jwtProcessor.setJWSKeySelector(jwsKeySelector);
jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {});
this.jwtProcessor = jwtProcessor;

this.reactiveJwkSource = new ReactiveRemoteJWKSource(jwkSetUrl);

this.jwkSelectorFactory = new JWKSelectorFactory(algorithm);
public NimbusReactiveJwtDecoder(RSAPublicKey publicKey){
this(new ReactivePublicKeyJWTProcessor(publicKey));
}

public NimbusReactiveJwtDecoder(ReactiveJWTProcessor jwtProcessor){
this.jwtProcessor=jwtProcessor;
}

/**
Expand Down Expand Up @@ -155,11 +107,8 @@ private JWT parse(String token) {

private Mono<Jwt> decode(SignedJWT parsedToken) {
try {
JWKSelector selector = this.jwkSelectorFactory
.createSelector(parsedToken.getHeader());
return this.reactiveJwkSource.get(selector)
.onErrorMap(e -> new IllegalStateException("Could not obtain the keys", e))
.map(jwkList -> createClaimsSet(parsedToken, jwkList))
return jwtProcessor.process(parsedToken)
.onErrorMap(e -> !(e instanceof IllegalStateException) && !(e instanceof JwtException), e -> new IllegalStateException("Could not obtain the keys", e))
.map(set -> createJwt(parsedToken, set))
.map(this::validateJwt)
.onErrorMap(e -> !(e instanceof IllegalStateException) && !(e instanceof JwtException), e -> new JwtException("An error occurred while attempting to decode the Jwt: ", e));
Expand All @@ -168,15 +117,6 @@ private Mono<Jwt> decode(SignedJWT parsedToken) {
}
}

private JWTClaimsSet createClaimsSet(JWT parsedToken, List<JWK> jwkList) {
try {
return this.jwtProcessor.process(parsedToken, new JWKContext(jwkList));
}
catch (BadJOSEException | JOSEException e) {
throw new JwtException("Failed to validate the token", e);
}
}

private Jwt createJwt(JWT parsedJwt, JWTClaimsSet jwtClaimsSet) {
Map<String, Object> headers = new LinkedHashMap<>(parsedJwt.getHeader().toJSONObject());
Map<String, Object> claims = this.claimSetConverter.convert(jwtClaimsSet.getClaims());
Expand All @@ -196,9 +136,4 @@ private Jwt validateJwt(Jwt jwt) {

return jwt;
}

private static RSAKey rsaKey(RSAPublicKey publicKey) {
return new RSAKey.Builder(publicKey)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* 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.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.JWKSelector;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.BadJOSEException;
import com.nimbusds.jose.proc.JWSKeySelector;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import com.nimbusds.jwt.proc.JWTProcessor;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

/**
* A simple implementation of {@link ReactiveJWTProcessor}.
* This implementation is mainly a wrapper around a simple {@link com.nimbusds.jwt.proc.JWTProcessor}
* but with a reactive initialization to get public key from a JWKS endpoint.
*/
public class ReactiveJWKSJWTProcessor implements ReactiveJWTProcessor {
private final JWTProcessor<JWKContext> jwtProcessor;
private final JWKSelectorFactory jwkSelectorFactory;
private final ReactiveRemoteJWKSource reactiveJwkSource;

public ReactiveJWKSJWTProcessor(String jwkSetUrl) {
this(jwkSetUrl, WebClient.create(), JWSAlgorithm.parse(JwsAlgorithms.RS256));
}

public ReactiveJWKSJWTProcessor(String jwkSetUrl, WebClient webClient) {
this(jwkSetUrl, webClient, JWSAlgorithm.parse(JwsAlgorithms.RS256));
}

public ReactiveJWKSJWTProcessor(String jwkSetUrl, JWSAlgorithm algorithm) {
this(jwkSetUrl, WebClient.create(), algorithm);
}

public ReactiveJWKSJWTProcessor(String jwkSetUrl, WebClient webClient, JWSAlgorithm algorithm) {
Assert.hasText(jwkSetUrl, "jwkSetUrl cannot be empty");

JWKSource<JWKContext> jwkSource = new JWKContextJWKSource();
JWSKeySelector<JWKContext> jwsKeySelector = new JWSVerificationKeySelector<>(algorithm, jwkSource);

DefaultJWTProcessor<JWKContext> jwtProcessor = new DefaultJWTProcessor<>();
jwtProcessor.setJWSKeySelector(jwsKeySelector);
jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {
});

reactiveJwkSource = new ReactiveRemoteJWKSource(webClient, jwkSetUrl);

jwkSelectorFactory = new JWKSelectorFactory(algorithm);

this.jwtProcessor = jwtProcessor;
}

public Mono<JWTClaimsSet> process(SignedJWT jwt) {
return Mono.defer(() -> {
try {
JWKSelector select = jwkSelectorFactory.createSelector(jwt.getHeader());
return reactiveJwkSource
.get(select)
.map(JWKContext::new)
.map(context -> createClaimsSet(jwt, context));
} catch (RuntimeException ex) {
return Mono.error(new JwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex));
}
});
}

private JWTClaimsSet createClaimsSet(JWT parsedToken, JWKContext context) {
try {
return this.jwtProcessor.process(parsedToken, context);
} catch (BadJOSEException | JOSEException e) {
throw new JwtException("Failed to validate the token", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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.jwt.EncryptedJWT;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import reactor.core.publisher.Mono;

/**
* Interface for parsing and processing JWT token.
* It is somehow a reactive version of {@link com.nimbusds.jwt.proc.JWTProcessor}.
*/
public interface ReactiveJWTProcessor {
default Mono<JWTClaimsSet> process(SignedJWT jwt) {
throw new JwtException("Signed JWT not supported");
}

default Mono<JWTClaimsSet> process(EncryptedJWT jwt) {
throw new JwtException("Encrypted JWT not supported");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
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;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import com.nimbusds.jwt.proc.JWTProcessor;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
import reactor.core.publisher.Mono;

import java.security.interfaces.RSAPublicKey;

/**
* A simple implementation of {@link ReactiveJWTProcessor}.
* This implementation is mainly a wrapper around a simple {@link com.nimbusds.jwt.proc.JWTProcessor}.
* JWT will be validated against a provided public key.
*/
public class ReactivePublicKeyJWTProcessor implements ReactiveJWTProcessor {
private final JWTProcessor<SecurityContext> jwtProcessor;

public ReactivePublicKeyJWTProcessor(RSAPublicKey publicKey) {
this(publicKey, JWSAlgorithm.parse(JwsAlgorithms.RS256));
}

public ReactivePublicKeyJWTProcessor(RSAPublicKey publicKey, JWSAlgorithm algorithm) {
RSAKey rsaKey = new RSAKey.Builder(publicKey).build();
JWKSet jwkSet = new JWKSet(rsaKey);
JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(jwkSet);
JWSKeySelector<SecurityContext> jwsKeySelector = new JWSVerificationKeySelector<>(algorithm, jwkSource);

DefaultJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
jwtProcessor.setJWSKeySelector(jwsKeySelector);
jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {
});

this.jwtProcessor = jwtProcessor;
}

public Mono<JWTClaimsSet> process(SignedJWT jwt) {
return Mono.defer(() -> {
try {
return Mono.just(jwtProcessor.process(jwt, null));
} catch (BadJOSEException | JOSEException e) {
return Mono.error(e);
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,12 @@ class ReactiveRemoteJWKSource implements ReactiveJWKSource {
*/
private final AtomicReference<Mono<JWKSet>> cachedJWKSet = new AtomicReference<>(Mono.empty());

private WebClient webClient = WebClient.create();
private final WebClient webClient;

private final String jwkSetURL;

ReactiveRemoteJWKSource(String jwkSetURL) {
ReactiveRemoteJWKSource(WebClient webClient, String jwkSetURL) {
this.webClient = webClient;
this.jwkSetURL = jwkSetURL;
}

Expand Down
Loading