|
@@ -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.
|
|
|
+ *
|
|
|
+ * <p>
|
|
|
+ * <b>NOTE:</b> This implementation uses the Nimbus JOSE + JWT SDK internally.
|
|
|
+ *
|
|
|
+ * @author Rob Winch
|
|
|
+ * @since 5.1
|
|
|
+ * @see JwtDecoder
|
|
|
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token (JWT)</a>
|
|
|
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515">JSON Web Signature (JWS)</a>
|
|
|
+ * @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 {
|
|
|
+ private final JWTProcessor<JWKContext> 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<JWKContext> jwsKeySelector =
|
|
|
+ new JWSVerificationKeySelector<>(algorithm, jwkSource);
|
|
|
+
|
|
|
+ DefaultJWTProcessor<JWKContext> jwtProcessor = new DefaultJWTProcessor<>();
|
|
|
+ jwtProcessor.setJWSKeySelector(jwsKeySelector);
|
|
|
+ this.jwtProcessor = jwtProcessor;
|
|
|
+
|
|
|
+ this.reactiveJwkSource = new ReactiveRemoteJWKSource(jwkSetUrl);
|
|
|
+
|
|
|
+ this.jwkSelectorFactory = new JWKSelectorFactory(algorithm);
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Mono<Jwt> 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<Jwt> 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<JWK> 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<String, Object> headers = new LinkedHashMap<>(parsedJwt.getHeader().toJSONObject());
|
|
|
+
|
|
|
+ return new Jwt(parsedJwt.getParsedString(), issuedAt, expiresAt, headers, jwtClaimsSet.getClaims());
|
|
|
+ }
|
|
|
+}
|