From 5c2a5927045d1d7b1f48d7d6f9a99cf3844a9479 Mon Sep 17 00:00:00 2001 From: Petr Aubrecht Date: Wed, 4 Aug 2021 20:02:55 +0200 Subject: [PATCH] FISH-1520 verify if JWT is encrypted as required Make the private and public key locations customizable via MP properties. Introduce JWT specific exception to unify error processing. Improve error messages. Remove unused MockTokenParser. --- .../JWTAuthenticationMechanism.java | 16 +- .../JWTProcessingException.java} | 39 ++--- .../eesecurity/JwtPrivateKeyStore.java | 26 ++- .../jwtauth/eesecurity/JwtPublicKeyStore.java | 35 ++-- .../eesecurity/SignedJWTIdentityStore.java | 46 +++-- .../jwtauth/jwt/JwtTokenParser.java | 158 ++++++++++-------- 6 files changed, 171 insertions(+), 149 deletions(-) rename appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/{tck/MockTokenParser.java => eesecurity/JWTProcessingException.java} (69%) diff --git a/appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/eesecurity/JWTAuthenticationMechanism.java b/appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/eesecurity/JWTAuthenticationMechanism.java index 4b42d48398e..956c20d3253 100644 --- a/appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/eesecurity/JWTAuthenticationMechanism.java +++ b/appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/eesecurity/JWTAuthenticationMechanism.java @@ -76,14 +76,14 @@ public JWTAuthenticationMechanism() { // https://download.eclipse.org/microprofile/microprofile-jwt-auth-1.2/microprofile-jwt-auth-spec-1.2.html#_jwt_and_http_headers Optional properties = SignedJWTIdentityStore.readVendorProperties(); Config config = ConfigProvider.getConfig(); - configJwtTokenHeader = readConfig(Names.TOKEN_HEADER, properties, config, CONFIG_TOKEN_HEADER_AUTHORIZATION); + configJwtTokenHeader = SignedJWTIdentityStore.readConfig(Names.TOKEN_HEADER, properties, config, CONFIG_TOKEN_HEADER_AUTHORIZATION); if ((!CONFIG_TOKEN_HEADER_AUTHORIZATION.equals(configJwtTokenHeader)) && (!CONFIG_TOKEN_HEADER_COOKIE.equals(configJwtTokenHeader))) { throw new DeploymentException("Configuration " + Names.TOKEN_HEADER + " must be either " + CONFIG_TOKEN_HEADER_AUTHORIZATION + " or " + CONFIG_TOKEN_HEADER_COOKIE + ", but is " + configJwtTokenHeader); } - configJwtTokenCookie = readConfig(Names.TOKEN_COOKIE, properties, config, "Bearer"); + configJwtTokenCookie = SignedJWTIdentityStore.readConfig(Names.TOKEN_COOKIE, properties, config, "Bearer"); } @Override @@ -139,16 +139,4 @@ private SignedJWTCredential createSignedJWTCredential(String token) { return null; } - - /** - * Read configuration from Vendor or server or return default value. - */ - private String readConfig(String key, Optional properties, Config config, String defaultValue) { - Optional valueOpt = properties.map(props -> props.getProperty(key)); - if (!valueOpt.isPresent()) { - valueOpt = config.getOptionalValue(key, String.class); - } - return valueOpt.orElse(defaultValue); - } - } diff --git a/appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/tck/MockTokenParser.java b/appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/eesecurity/JWTProcessingException.java similarity index 69% rename from appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/tck/MockTokenParser.java rename to appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/eesecurity/JWTProcessingException.java index 988fa869c10..830a85296ee 100644 --- a/appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/tck/MockTokenParser.java +++ b/appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/eesecurity/JWTProcessingException.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright (c) 2017-2019 Payara Foundation and/or its affiliates. All rights reserved. + * Copyright (c) [2021] Payara Foundation and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development @@ -37,33 +37,26 @@ * only if the new code is made subject to such option by the copyright * holder. */ -package fish.payara.microprofile.jwtauth.tck; - -import java.security.PublicKey; -import org.eclipse.microprofile.jwt.JsonWebToken; -import fish.payara.microprofile.jwtauth.jwt.JwtTokenParser; +package fish.payara.microprofile.jwtauth.eesecurity; /** - * - * * This implements the artefact mandated by the MP-JWT TCK for offline - * (outside container) testing - * of the token parser. - * - * @author Arjan Tijms - * + * Exception during processing of JWT. */ -public class MockTokenParser { +public class JWTProcessingException extends Exception { + + public JWTProcessingException() { + } - private final JwtTokenParser jwtTokenParser = new JwtTokenParser(); + public JWTProcessingException(String message) { + super(message); + } - public JsonWebToken parse(String bearerToken, String issuer, PublicKey signedBy) throws Exception { - try { - jwtTokenParser.parse(bearerToken); - return jwtTokenParser.verify(issuer, signedBy); - } catch (Exception e) { - throw new IllegalStateException("", e); - } + public JWTProcessingException(String message, Throwable cause) { + super(message, cause); } -} + public JWTProcessingException(Throwable cause) { + super(cause); + } +} diff --git a/appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/eesecurity/JwtPrivateKeyStore.java b/appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/eesecurity/JwtPrivateKeyStore.java index 0af38d2cfc5..5dde7187b75 100644 --- a/appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/eesecurity/JwtPrivateKeyStore.java +++ b/appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/eesecurity/JwtPrivateKeyStore.java @@ -40,12 +40,6 @@ package fish.payara.microprofile.jwtauth.eesecurity; -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.ConfigProvider; - -import javax.enterprise.inject.spi.DeploymentException; -import javax.json.JsonArray; -import javax.json.JsonObject; import java.math.BigInteger; import java.security.KeyFactory; import java.security.PrivateKey; @@ -55,14 +49,19 @@ import java.util.Base64; import java.util.Optional; import java.util.function.Supplier; - +import javax.enterprise.inject.spi.DeploymentException; +import javax.json.JsonArray; +import javax.json.JsonObject; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; import static org.eclipse.microprofile.jwt.config.Names.DECRYPTOR_KEY_LOCATION; -class JwtPrivateKeyStore { +public class JwtPrivateKeyStore { private final Config config; private final Supplier> cacheSupplier; private final Duration defaultCacheTTL; + private String keyLocation = "/privateKey.pem"; /** * @param defaultCacheTTL Private key cache TTL @@ -73,8 +72,17 @@ public JwtPrivateKeyStore(Duration defaultCacheTTL) { this.cacheSupplier = new KeyLoadingCache(this::readRawPrivateKey)::get; } + /** + * @param defaultCacheTTL Private key cache TTL + * @param keyLocation location of the private key + */ + public JwtPrivateKeyStore(Duration defaultCacheTTL, Optional keyLocation) { + this(defaultCacheTTL); + this.keyLocation = keyLocation.orElse(this.keyLocation); + } + private CacheableString readRawPrivateKey() { - CacheableString privateKey = JwtKeyStoreUtils.readKeyFromLocation("/privateKey.pem", defaultCacheTTL); + CacheableString privateKey = JwtKeyStoreUtils.readKeyFromLocation(keyLocation, defaultCacheTTL); if (!privateKey.isPresent()) { privateKey = JwtKeyStoreUtils.readMPKeyFromLocation(config, DECRYPTOR_KEY_LOCATION, defaultCacheTTL); diff --git a/appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/eesecurity/JwtPublicKeyStore.java b/appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/eesecurity/JwtPublicKeyStore.java index 263f7c1245f..0d9bae48701 100644 --- a/appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/eesecurity/JwtPublicKeyStore.java +++ b/appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/eesecurity/JwtPublicKeyStore.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright (c) [2017-2020] Payara Foundation and/or its affiliates. All rights reserved. + * Copyright (c) [2017-2021] Payara Foundation and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development @@ -39,18 +39,6 @@ */ package fish.payara.microprofile.jwtauth.eesecurity; -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.ConfigProvider; - -import javax.enterprise.inject.spi.DeploymentException; -import javax.json.Json; -import javax.json.JsonArray; -import javax.json.JsonObject; -import javax.json.JsonReader; -import javax.json.JsonValue; -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.io.StringReader; import java.math.BigInteger; import java.security.AlgorithmParameters; import java.security.KeyFactory; @@ -64,15 +52,18 @@ import java.security.spec.X509EncodedKeySpec; import java.time.Duration; import java.util.Base64; -import java.util.Objects; import java.util.Optional; import java.util.function.Supplier; import java.util.logging.Logger; - +import javax.enterprise.inject.spi.DeploymentException; +import javax.json.JsonArray; +import javax.json.JsonObject; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; import static org.eclipse.microprofile.jwt.config.Names.VERIFIER_PUBLIC_KEY; import static org.eclipse.microprofile.jwt.config.Names.VERIFIER_PUBLIC_KEY_LOCATION; -class JwtPublicKeyStore { +public class JwtPublicKeyStore { private static final Logger LOGGER = Logger.getLogger(JwtPublicKeyStore.class.getName()); private static final String RSA_ALGORITHM = "RSA"; @@ -82,6 +73,7 @@ class JwtPublicKeyStore { private final Config config; private final Supplier> cacheSupplier; private final Duration defaultCacheTTL; + private String keyLocation = "/publicKey.pem"; /** * @param defaultCacheTTL Public key cache TTL @@ -92,6 +84,15 @@ public JwtPublicKeyStore(Duration defaultCacheTTL) { this.cacheSupplier = new KeyLoadingCache(this::readRawPublicKey)::get; } + /** + * @param defaultCacheTTL Public key cache TTL + * @param keyLocation location of the public key + */ + public JwtPublicKeyStore(Duration defaultCacheTTL, Optional keyLocation) { + this(defaultCacheTTL); + this.keyLocation = keyLocation.orElse(this.keyLocation); + } + /** * * @param keyID The JWT key ID or null if no key ID was provided @@ -105,7 +106,7 @@ public PublicKey getPublicKey(String keyID) { } private CacheableString readRawPublicKey() { - CacheableString publicKey = JwtKeyStoreUtils.readKeyFromLocation("/publicKey.pem", defaultCacheTTL); + CacheableString publicKey = JwtKeyStoreUtils.readKeyFromLocation(keyLocation, defaultCacheTTL); if (!publicKey.isPresent()) { publicKey = readMPEmbeddedPublicKey(); diff --git a/appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/eesecurity/SignedJWTIdentityStore.java b/appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/eesecurity/SignedJWTIdentityStore.java index 22ea48cc0b7..1ef079d61ed 100644 --- a/appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/eesecurity/SignedJWTIdentityStore.java +++ b/appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/eesecurity/SignedJWTIdentityStore.java @@ -44,7 +44,6 @@ import java.io.IOException; import static java.lang.Thread.currentThread; import java.net.URL; -import java.security.PublicKey; import java.time.Duration; import java.util.Arrays; import java.util.Collection; @@ -83,6 +82,8 @@ public class SignedJWTIdentityStore implements IdentityStore { private final JwtPublicKeyStore publicKeyStore; private final JwtPrivateKeyStore privateKeyStore; + private final boolean isEncryptionRequired; + public SignedJWTIdentityStore() { config = ConfigProvider.getConfig(); @@ -99,29 +100,23 @@ public SignedJWTIdentityStore() { enabledNamespace = readEnabledNamespace(properties); customNamespace = readCustomNamespace(properties); disableTypeVerification = readDisableTypeVerification(properties); - publicKeyStore = new JwtPublicKeyStore(readPublicKeyCacheTTL(properties)); - privateKeyStore = new JwtPrivateKeyStore(readPublicKeyCacheTTL(properties)); + Optional publicKeyLocation = readConfigOptional(Names.VERIFIER_PUBLIC_KEY_LOCATION, properties, config); //mp.jwt.verifyAndParseEncryptedJWT.publickey.location + Optional publicKey = readConfigOptional(Names.VERIFIER_PUBLIC_KEY, properties, config); //mp.jwt.verifyAndParseEncryptedJWT.publickey + Optional decryptKeyLocation = readConfigOptional(Names.DECRYPTOR_KEY_LOCATION, properties, config); //mp.jwt.decrypt.key.location + publicKeyStore = new JwtPublicKeyStore(readPublicKeyCacheTTL(properties), publicKeyLocation); + privateKeyStore = new JwtPrivateKeyStore(readPublicKeyCacheTTL(properties), decryptKeyLocation); + + // Signing is required by default, it doesn't parse if not signed + isEncryptionRequired = decryptKeyLocation.isPresent(); } public CredentialValidationResult validate(SignedJWTCredential signedJWTCredential) { final JwtTokenParser jwtTokenParser = new JwtTokenParser(enabledNamespace, customNamespace, disableTypeVerification); try { - jwtTokenParser.parse(signedJWTCredential.getSignedJWT()); - String keyID = jwtTokenParser.getKeyID(); - - PublicKey publicKey = publicKeyStore.getPublicKey(keyID); - JsonWebTokenImpl jsonWebToken = null; - try { - jsonWebToken = jwtTokenParser.verify(acceptedIssuer, publicKey); - } catch (IllegalStateException illegalStateException) { - if (illegalStateException.getMessage().equals("No parsed SignedJWT.")) { - jsonWebToken = jwtTokenParser.verify(acceptedIssuer, publicKey, privateKeyStore.getPrivateKey(keyID)); - } else { - throw illegalStateException; - } - } + JsonWebTokenImpl jsonWebToken = jwtTokenParser.parse(signedJWTCredential.getSignedJWT(), + isEncryptionRequired, publicKeyStore, acceptedIssuer, privateKeyStore); - // verify audience + // verifyAndParseEncryptedJWT audience final Set recipientsOfThisJWT = jsonWebToken.getAudience(); // find if any recipient is in the allowed audience Boolean recipientInAudience = allowedAudience @@ -188,4 +183,19 @@ private Optional readAudience(Optional properties) { return properties.isPresent() ? Optional.ofNullable(properties.get().getProperty(Names.AUDIENCES)) : Optional.empty(); } + /** + * Read configuration from Vendor or server or return default value. + */ + public static String readConfig(String key, Optional properties, Config config, String defaultValue) { + Optional valueOpt = readConfigOptional(key, properties, config); + return valueOpt.orElse(defaultValue); + } + + public static Optional readConfigOptional(String key, Optional properties, Config config) { + Optional valueOpt = properties.map(props -> props.getProperty(key)); + if (!valueOpt.isPresent()) { + valueOpt = config.getOptionalValue(key, String.class); + } + return valueOpt; + } } diff --git a/appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/jwt/JwtTokenParser.java b/appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/jwt/JwtTokenParser.java index 2bf69046542..cb2a5493b5a 100644 --- a/appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/jwt/JwtTokenParser.java +++ b/appserver/payara-appserver-modules/microprofile/jwt-auth/src/main/java/fish/payara/microprofile/jwtauth/jwt/JwtTokenParser.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright (c) [2017-2020] Payara Foundation and/or its affiliates. All rights reserved. + * Copyright (c) [2017-2021] Payara Foundation and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development @@ -39,38 +39,39 @@ */ package fish.payara.microprofile.jwtauth.jwt; +import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JOSEObjectType; -import com.nimbusds.jose.JWEHeader; -import com.nimbusds.jose.JWSHeader; +import static com.nimbusds.jose.JWEAlgorithm.RSA_OAEP; +import com.nimbusds.jose.JWSAlgorithm; +import static com.nimbusds.jose.JWSAlgorithm.ES256; +import static com.nimbusds.jose.JWSAlgorithm.RS256; import com.nimbusds.jose.crypto.ECDSAVerifier; import com.nimbusds.jose.crypto.RSADecrypter; import com.nimbusds.jose.crypto.RSASSAVerifier; import com.nimbusds.jwt.EncryptedJWT; import com.nimbusds.jwt.SignedJWT; -import org.eclipse.microprofile.jwt.Claims; - -import javax.json.Json; -import javax.json.JsonNumber; -import javax.json.JsonReader; -import javax.json.JsonString; -import javax.json.JsonValue; +import fish.payara.microprofile.jwtauth.eesecurity.JWTProcessingException; +import fish.payara.microprofile.jwtauth.eesecurity.JwtPrivateKeyStore; +import fish.payara.microprofile.jwtauth.eesecurity.JwtPublicKeyStore; import java.io.StringReader; import java.security.PrivateKey; import java.security.PublicKey; import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPublicKey; import java.text.ParseException; +import static java.util.Arrays.asList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; - -import static com.nimbusds.jose.JWEAlgorithm.RSA_OAEP; -import static com.nimbusds.jose.JWSAlgorithm.ES256; -import static com.nimbusds.jose.JWSAlgorithm.RS256; -import static java.util.Arrays.asList; +import javax.json.Json; import static javax.json.Json.createObjectBuilder; +import javax.json.JsonNumber; +import javax.json.JsonReader; +import javax.json.JsonString; +import javax.json.JsonValue; +import org.eclipse.microprofile.jwt.Claims; import static org.eclipse.microprofile.jwt.Claims.exp; import static org.eclipse.microprofile.jwt.Claims.iat; import static org.eclipse.microprofile.jwt.Claims.iss; @@ -110,33 +111,59 @@ public JwtTokenParser() { this(Optional.empty(), Optional.empty(), Optional.empty()); } - public void parse(String bearerToken) throws Exception { - rawToken = bearerToken; - + public JsonWebTokenImpl parse(String bearerToken, boolean encryptionRequired, JwtPublicKeyStore publicKeyStore, + String acceptedIssuer, JwtPrivateKeyStore privateKeyStore) throws JWTProcessingException { + JsonWebTokenImpl jsonWebToken; try { - signedJWT = SignedJWT.parse(rawToken); + rawToken = bearerToken; + String keyId; + + int parts = rawToken.split("\\.", 5).length; // not interested in parts above 5 + if (parts == 3) { + // signed JWT has 3 parts + signedJWT = SignedJWT.parse(rawToken); - if (!checkIsJWT(signedJWT.getHeader())) { - throw new IllegalStateException("Not JWT"); + if (!checkIsSignedJWT(signedJWT)) { + throw new JWTProcessingException("Not signed JWT, typ must be 'JWT'"); + } + keyId = signedJWT.getHeader().getKeyID(); + } else { + // encrypted JWT has 5 parts + encryptedJWT = EncryptedJWT.parse(rawToken); + + if (!checkIsEncryptedJWT(encryptedJWT)) { + throw new JWTProcessingException("Not encrypted JWT, cty must be 'JWT'"); + } + keyId = encryptedJWT.getHeader().getKeyID(); } - } catch (ParseException parseException) { - encryptedJWT = EncryptedJWT.parse(rawToken); - if (!checkIsJWT(encryptedJWT.getHeader())) { - throw new IllegalStateException("Not JWT"); + PublicKey publicKey = publicKeyStore.getPublicKey(keyId); + // first, parse the payload of the encrypting envelope, save signedJWT + if (encryptedJWT == null) { + if (encryptionRequired) { + // see JWT Auth 1.2, Requirements for accepting signed and encrypted tokens + throw new JWTProcessingException("JWT expected to be encrypted, mp.jwt.decrypt.key.location was defined!"); + } + jsonWebToken = verifyAndParseSignedJWT(acceptedIssuer, publicKey); + } else { + jsonWebToken = verifyAndParseEncryptedJWT(acceptedIssuer, publicKey, privateKeyStore.getPrivateKey(keyId)); } + } catch (JWTProcessingException | ParseException ex) { + throw new JWTProcessingException(ex); } + return jsonWebToken; } - public JsonWebTokenImpl verify(String issuer, PublicKey publicKey) throws Exception { + private JsonWebTokenImpl verifyAndParseSignedJWT(String issuer, PublicKey publicKey) throws JWTProcessingException { if (signedJWT == null) { throw new IllegalStateException("No parsed SignedJWT."); } + JWSAlgorithm signAlgorithmName = signedJWT.getHeader().getAlgorithm(); // 1.0 4.1 alg + MP-JWT 1.0 6.1 1 - if (!signedJWT.getHeader().getAlgorithm().equals(RS256) - && !signedJWT.getHeader().getAlgorithm().equals(ES256)) { - throw new IllegalStateException("Not RS256 or ES256"); + if (!signAlgorithmName.equals(RS256) + && !signAlgorithmName.equals(ES256)) { + throw new JWTProcessingException("Only RS256 or ES256 algorithms supported for JWT signing, used " + signAlgorithmName); } try (JsonReader reader = Json.createReader(new StringReader(signedJWT.getPayload().toString()))) { @@ -147,33 +174,37 @@ public JsonWebTokenImpl verify(String issuer, PublicKey publicKey) throws Except // MP-JWT 1.0 4.1 Minimum MP-JWT Required Claims if (!checkRequiredClaimsPresent(rawClaims)) { - throw new IllegalStateException("Not all required claims present"); + throw new JWTProcessingException("Not all required claims present"); } // MP-JWT 1.0 4.1 upn - has fallbacks String callerPrincipalName = getCallerPrincipalName(rawClaims); if (callerPrincipalName == null) { - throw new IllegalStateException("One of upn, preferred_username or sub is required to be non null"); + throw new JWTProcessingException("One of upn, preferred_username or sub is required to be non null"); } // MP-JWT 1.0 6.1 2 if (!checkIssuer(rawClaims, issuer)) { - throw new IllegalStateException("Bad issuer"); + throw new JWTProcessingException("Bad issuer"); } if (!checkNotExpired(rawClaims)) { - throw new IllegalStateException("Expired"); + throw new JWTProcessingException("JWT token expired"); } // MP-JWT 1.0 6.1 2 - if (signedJWT.getHeader().getAlgorithm().equals(RS256)) { - if (!signedJWT.verify(new RSASSAVerifier((RSAPublicKey) publicKey))) { - throw new IllegalStateException("Signature invalid"); - } - } else { - if (!signedJWT.verify(new ECDSAVerifier((ECPublicKey) publicKey))) { - throw new IllegalStateException("Signature invalid"); + try { + if (signAlgorithmName.equals(RS256)) { + if (!signedJWT.verify(new RSASSAVerifier((RSAPublicKey) publicKey))) { + throw new JWTProcessingException("Signature of the JWT token is invalid"); + } + } else { + if (!signedJWT.verify(new ECDSAVerifier((ECPublicKey) publicKey))) { + throw new JWTProcessingException("Signature of the JWT token is invalid"); + } } + } catch (JOSEException ex) { + throw new JWTProcessingException("Exception during JWT signature validation", ex); } rawClaims.put( @@ -184,38 +215,29 @@ public JsonWebTokenImpl verify(String issuer, PublicKey publicKey) throws Except } } - public JsonWebTokenImpl verify(String issuer, PublicKey publicKey, PrivateKey privateKey) throws Exception { - if (signedJWT == null) { - if (encryptedJWT == null) { - throw new IllegalStateException("No parsed SignedJWT or EncryptedJWT."); - } + private JsonWebTokenImpl verifyAndParseEncryptedJWT(String issuer, PublicKey publicKey, PrivateKey privateKey) throws JWTProcessingException { + if (encryptedJWT == null) { + throw new IllegalStateException("EncryptedJWT not parsed"); + } - if (!encryptedJWT.getHeader().getAlgorithm().getName().equals(RSA_OAEP.getName())) { - throw new IllegalStateException("Not RSA-OAEP"); - } + String algName = encryptedJWT.getHeader().getAlgorithm().getName(); + if (!RSA_OAEP.getName().equals(algName)) { + throw new JWTProcessingException("Only RSA-OAEP algorithm is supported for JWT encryption, used " + algName); + } + try { encryptedJWT.decrypt(new RSADecrypter(privateKey)); - - signedJWT = encryptedJWT.getPayload().toSignedJWT(); - - if (signedJWT == null) { - throw new IllegalStateException("No parsed SignedJWT."); - } + } catch (JOSEException ex) { + throw new JWTProcessingException("Exception during decrypting JWT token", ex); } - return verify(issuer, publicKey); - } + signedJWT = encryptedJWT.getPayload().toSignedJWT(); - public String getKeyID() { - if (signedJWT == null && encryptedJWT == null) { - throw new IllegalStateException("No parsed SignedJWT or EncryptedJWT."); + if (signedJWT == null) { + throw new JWTProcessingException("Unable to parse signed JWT."); } - if (signedJWT != null) { - return signedJWT.getHeader().getKeyID(); - } else { - return encryptedJWT.getHeader().getKeyID(); - } + return verifyAndParseSignedJWT(issuer, publicKey); } private Map handleNamespacedClaims(Map currentClaims) { @@ -271,15 +293,15 @@ private boolean checkIssuer(Map presentedClaims, String accep return acceptedIssuer.equals(issuer); } - private boolean checkIsJWT(JWSHeader header) { - return disableTypeVerification || Optional.ofNullable(header.getType()) + private boolean checkIsSignedJWT(SignedJWT jwt) { + return disableTypeVerification || Optional.ofNullable(jwt.getHeader().getType()) .map(JOSEObjectType::toString) .orElse("") .equals("JWT"); } - private boolean checkIsJWT(JWEHeader header) { - return disableTypeVerification || Optional.ofNullable(header.getContentType()) + private boolean checkIsEncryptedJWT(EncryptedJWT jwt) { + return disableTypeVerification || Optional.ofNullable(jwt.getHeader().getContentType()) .orElse("") .equals("JWT"); }