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/NimbusJwkReactiveJwtDecoder.java new file mode 100644 index 0000000000..1f68843ede --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwkReactiveJwtDecoder.java @@ -0,0 +1,157 @@ +/* + * 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.JWK; +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.JWTParser; +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 reactor.core.publisher.Mono; + +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * An implementation of a {@link JwtDecoder} that "decodes" a + * JSON Web Token (JWT) and additionally verifies it's digital signature if the JWT is a + * JSON Web Signature (JWS). The public key used for verification is obtained from the + * JSON Web Key (JWK) Set {@code URL} supplied via the constructor. + * + *

+ * NOTE: This implementation uses the Nimbus JOSE + JWT SDK internally. + * + * @author Rob Winch + * @since 5.1 + * @see JwtDecoder + * @see JSON Web Token (JWT) + * @see JSON Web Signature (JWS) + * @see JSON Web Key (JWK) + * @see Nimbus JOSE + JWT SDK + */ +public final class NimbusJwkReactiveJwtDecoder implements ReactiveJwtDecoder { + private final JWTProcessor jwtProcessor; + + private final ReactiveRemoteJWKSource 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); + } + + /** + * 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) { + Assert.hasText(jwkSetUrl, "jwkSetUrl cannot be empty"); + Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty"); + + JWSAlgorithm algorithm = JWSAlgorithm.parse(jwsAlgorithm); + JWKSource jwkSource = new JWKContextJWKSource(); + JWSKeySelector jwsKeySelector = + new JWSVerificationKeySelector<>(algorithm, jwkSource); + + DefaultJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); + jwtProcessor.setJWSKeySelector(jwsKeySelector); + this.jwtProcessor = jwtProcessor; + + this.reactiveJwkSource = new ReactiveRemoteJWKSource(jwkSetUrl); + + this.jwkSelectorFactory = new JWKSelectorFactory(algorithm); + + } + + @Override + public Mono decode(String token) throws JwtException { + JWT jwt = parse(token); + if (jwt instanceof SignedJWT) { + return this.decode((SignedJWT) jwt); + } + return Mono.empty(); + } + + private JWT parse(String token) { + try { + return JWTParser.parse(token); + } catch (Exception ex) { + throw new JwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex); + } + } + + private Mono decode(SignedJWT parsedToken) { + try { + JWKSelector selector = this.jwkSelectorFactory + .createSelector(parsedToken.getHeader()); + return this.reactiveJwkSource.get(selector) + .map(jwkList -> createJwkSet(parsedToken, jwkList)) + .map(set -> createJwt(parsedToken, set)); + } catch (Exception ex) { + throw new JwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex); + } + } + + private JWTClaimsSet createJwkSet(JWT parsedToken, List jwkList) { + try { + return this.jwtProcessor.process(parsedToken, new JWKContext(jwkList)); + } + catch (BadJOSEException e) { + throw new RuntimeException(e); + } + catch (JOSEException e) { + throw new RuntimeException(e); + } + } + + private Jwt createJwt(JWT parsedJwt, JWTClaimsSet jwtClaimsSet) { + Instant expiresAt = null; + if (jwtClaimsSet.getExpirationTime() != null) { + expiresAt = jwtClaimsSet.getExpirationTime().toInstant(); + } + Instant issuedAt = null; + if (jwtClaimsSet.getIssueTime() != null) { + issuedAt = jwtClaimsSet.getIssueTime().toInstant(); + } else if (expiresAt != null) { + // Default to expiresAt - 1 second + issuedAt = Instant.from(expiresAt).minusSeconds(1); + } + + Map headers = new LinkedHashMap<>(parsedJwt.getHeader().toJSONObject()); + + return new Jwt(parsedJwt.getParsedString(), issuedAt, expiresAt, headers, jwtClaimsSet.getClaims()); + } +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoder.java new file mode 100644 index 0000000000..8ecf5d3e63 --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoder.java @@ -0,0 +1,50 @@ +/* + * 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 reactor.core.publisher.Mono; + +/** + * Implementations of this interface are responsible for "decoding" + * a JSON Web Token (JWT) from it's compact claims representation format to a {@link Jwt}. + * + *

+ * JWTs may be represented using the JWS Compact Serialization format for a + * JSON Web Signature (JWS) structure or JWE Compact Serialization format for a + * JSON Web Encryption (JWE) structure. Therefore, implementors are responsible + * for verifying a JWS and/or decrypting a JWE. + * + * @author Rob Winch + * @since 5.1 + * @see Jwt + * @see JSON Web Token (JWT) + * @see JSON Web Signature (JWS) + * @see JSON Web Encryption (JWE) + * @see JWS Compact Serialization + * @see JWE Compact Serialization + */ +public interface ReactiveJwtDecoder { + + /** + * Decodes the JWT from it's compact claims representation format and returns a {@link Jwt}. + * + * @param token the JWT value + * @return a {@link Jwt} + * @throws JwtException if an error occurs while attempting to decode the JWT + */ + Mono decode(String token) throws JwtException; + +}