From d15287863e7b5e19025e8c4378fe58da946b87e9 Mon Sep 17 00:00:00 2001 From: Adam Bien Date: Tue, 3 Oct 2023 22:52:14 +0200 Subject: [PATCH] AWS Configuration Validation and Logging (#724) * broken AWS public key location detected * ending slash is not considered as error * public key location logged * dedicated logger and message for AwsAlbKeyResolver introduced * algorithm, header and key configuration validation refactoring --- .../AwsAlbKeyConfigurationValidator.java | 74 +++++++++++++++++++ .../jwt/auth/principal/AwsAlbKeyResolver.java | 7 +- .../principal/AwsAlbKeyResolverLogging.java | 32 ++++++++ .../principal/AwsAlbKeyResolverMessages.java | 15 ++++ .../jwt/auth/principal/PrincipalLogging.java | 1 + .../AwsAlbKeyConfigurationValidatorTest.java | 36 +++++++++ .../auth/principal/AwsAlbKeyResolverTest.java | 1 + 7 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/AwsAlbKeyConfigurationValidator.java create mode 100644 implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/AwsAlbKeyResolverLogging.java create mode 100644 implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/AwsAlbKeyResolverMessages.java create mode 100644 implementation/jwt-auth/src/test/java/io/smallrye/jwt/auth/principal/AwsAlbKeyConfigurationValidatorTest.java diff --git a/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/AwsAlbKeyConfigurationValidator.java b/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/AwsAlbKeyConfigurationValidator.java new file mode 100644 index 00000000..c1f72c61 --- /dev/null +++ b/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/AwsAlbKeyConfigurationValidator.java @@ -0,0 +1,74 @@ +package io.smallrye.jwt.auth.principal; + +import java.net.URI; + +import org.jose4j.lang.UnresolvableKeyException; + +import io.smallrye.jwt.algorithm.SignatureAlgorithm; + +interface AwsAlbKeyConfigurationValidator { + + public static void validateKeyConfiguration(JWTAuthContextInfo authContextInfo) throws UnresolvableKeyException { + // public key location check + var publicKeyLocation = authContextInfo.getPublicKeyLocation(); + if (publicKeyLocation == null) { + throw PrincipalMessages.msg.nullKeyLocation(); + } + if (containsSubPath(publicKeyLocation)) { + throw AwsAlbKeyResolverMessages.msg.subPathNotAllowed(); + } + } + + public static void validatePublicKeyAlgorithmConfiguration(JWTAuthContextInfo authContextInfo) { + var publicKeyAlgorithm = authContextInfo.getSignatureAlgorithm(); + if (publicKeyAlgorithm == null) { + AwsAlbKeyResolverLogging.log.publicKeyAlgorithmNotSet(); + } + if (!publicKeyAlgorithm.getAlgorithm().equals(SignatureAlgorithm.ES256.getAlgorithm())) { + AwsAlbKeyResolverLogging.log.publicKeyAlgorithmNotSetToES256(); + } + } + + /** + * verifies the entry: mp.jwt.token.header=X-Amzn-Oidc-Data + * + * @param authContextInfo + */ + public static void validateTokenHeaderConfiguration(JWTAuthContextInfo authContextInfo) { + var tokenHeader = authContextInfo.getTokenHeader(); + if (tokenHeader == null || !"X-Amzn-Oidc-Data".equals(tokenHeader)) { + AwsAlbKeyResolverLogging.log.invalidAWSTokenHeader(); + } + + } + + /** + * Remove ending slash from uri e.g. https://localhost:8080/ -> + * https://localhost:8080 + * + * @param uri public key location + * @return uri without ending slash + */ + static String removeEndingSlash(String uri) { + if (!uri.endsWith("/") || uri.length() == 1) { + return uri; + } + var length = uri.length(); + return uri.substring(0, length - 1); + } + + /** + * Check if public key location contains sub path e.g. + * https://localhost:8080/subpath + * Fails fast to prevent runtime errors + * + * @param publicKeyLocation to check + * @return true if public key location contains sub path which is invalid + */ + static boolean containsSubPath(String publicKeyLocation) { + var locationWithoutSlash = removeEndingSlash(publicKeyLocation); + var uri = URI.create(locationWithoutSlash); + return uri.getPath().contains("/"); + } + +} diff --git a/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/AwsAlbKeyResolver.java b/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/AwsAlbKeyResolver.java index a8f80eda..3242b3a5 100644 --- a/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/AwsAlbKeyResolver.java +++ b/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/AwsAlbKeyResolver.java @@ -30,9 +30,9 @@ public class AwsAlbKeyResolver implements VerificationKeyResolver { private AtomicInteger size = new AtomicInteger(); public AwsAlbKeyResolver(JWTAuthContextInfo authContextInfo) throws UnresolvableKeyException { - if (authContextInfo.getPublicKeyLocation() == null) { - throw PrincipalMessages.msg.nullKeyLocation(); - } + AwsAlbKeyConfigurationValidator.validateKeyConfiguration(authContextInfo); + AwsAlbKeyConfigurationValidator.validatePublicKeyAlgorithmConfiguration(authContextInfo); + AwsAlbKeyConfigurationValidator.validateTokenHeaderConfiguration(authContextInfo); this.authContextInfo = authContextInfo; this.cacheTimeToLive = Duration.ofMinutes(authContextInfo.getKeyCacheTimeToLive()).toMillis(); } @@ -57,6 +57,7 @@ public Key resolveKey(JsonWebSignature jws, List nestingContex protected Key retrieveKey(String kid) throws UnresolvableKeyException { String keyLocation = authContextInfo.getPublicKeyLocation() + "/" + kid; + AwsAlbKeyResolverLogging.log.publicKeyPath(keyLocation); SimpleResponse simpleResponse = null; try { simpleResponse = getHttpGet().get(keyLocation); diff --git a/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/AwsAlbKeyResolverLogging.java b/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/AwsAlbKeyResolverLogging.java new file mode 100644 index 00000000..eeaf258b --- /dev/null +++ b/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/AwsAlbKeyResolverLogging.java @@ -0,0 +1,32 @@ +package io.smallrye.jwt.auth.principal; + +import org.jboss.logging.BasicLogger; +import org.jboss.logging.Logger; +import org.jboss.logging.annotations.LogMessage; +import org.jboss.logging.annotations.Message; +import org.jboss.logging.annotations.MessageLogger; + +@MessageLogger(projectCode = "SRJWT", length = 5) +interface AwsAlbKeyResolverLogging extends BasicLogger { + AwsAlbKeyResolverLogging log = Logger.getMessageLogger(AwsAlbKeyResolverLogging.class, + AwsAlbKeyResolverLogging.class.getPackage().getName()); + + @LogMessage(level = Logger.Level.DEBUG) + @Message(id = 14000, value = "public key path: %s") + void publicKeyPath(String path); + + @LogMessage(level = Logger.Level.DEBUG) + @Message(id = 14001, value = "mp.jwt.verify.publickey.algorithm is not set." + + "Falling back to default algorithm: RS256 which is not compabible with AWS ALB." + + "Please set mp.jwt.verify.publickey.algorithm to ES256") + void publicKeyAlgorithmNotSet(); + + @LogMessage(level = Logger.Level.DEBUG) + @Message(id = 14002, value = "mp.jwt.verify.publickey.algorithm is not set to ES256") + void publicKeyAlgorithmNotSetToES256(); + + @LogMessage(level = Logger.Level.DEBUG) + @Message(id = 14003, value = "mp.jwt.token.header is not set to X-Amzn-Oidc-Data") + void invalidAWSTokenHeader(); + +} diff --git a/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/AwsAlbKeyResolverMessages.java b/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/AwsAlbKeyResolverMessages.java new file mode 100644 index 00000000..e325f6d7 --- /dev/null +++ b/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/AwsAlbKeyResolverMessages.java @@ -0,0 +1,15 @@ +package io.smallrye.jwt.auth.principal; + +import org.jboss.logging.Messages; +import org.jboss.logging.annotations.Message; +import org.jboss.logging.annotations.MessageBundle; +import org.jose4j.lang.UnresolvableKeyException; + +@MessageBundle(projectCode = "SRJWT", length = 5) +interface AwsAlbKeyResolverMessages { + AwsAlbKeyResolverMessages msg = Messages.getBundle(AwsAlbKeyResolverMessages.class); + + @Message(id = 15000, value = "Key is resolved from kid. Key location is not allowed. Provide only the path like: https://public-keys.auth.elb.[REGION].amazonaws.com") + UnresolvableKeyException subPathNotAllowed(); + +} diff --git a/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/PrincipalLogging.java b/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/PrincipalLogging.java index 99b2a6bc..d0c90dd7 100644 --- a/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/PrincipalLogging.java +++ b/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/PrincipalLogging.java @@ -192,4 +192,5 @@ interface PrincipalLogging extends BasicLogger { @LogMessage(level = Logger.Level.DEBUG) @Message(id = 8044, value = "Encrypted token headers must contain a content type header") void encryptedTokenMissingContentType(); + } diff --git a/implementation/jwt-auth/src/test/java/io/smallrye/jwt/auth/principal/AwsAlbKeyConfigurationValidatorTest.java b/implementation/jwt-auth/src/test/java/io/smallrye/jwt/auth/principal/AwsAlbKeyConfigurationValidatorTest.java new file mode 100644 index 00000000..132515df --- /dev/null +++ b/implementation/jwt-auth/src/test/java/io/smallrye/jwt/auth/principal/AwsAlbKeyConfigurationValidatorTest.java @@ -0,0 +1,36 @@ +package io.smallrye.jwt.auth.principal; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public class AwsAlbKeyConfigurationValidatorTest { + + @Test + void containsSubPath() { + assertTrue(AwsAlbKeyConfigurationValidator + .containsSubPath("https://public-keys.auth.elb.eu-central-1.amazonaws.com/keyid")); + assertTrue(AwsAlbKeyConfigurationValidator + .containsSubPath("https://public-keys.auth.elb.eu-central-1.amazonaws.com/index/keyid")); + assertTrue(AwsAlbKeyConfigurationValidator + .containsSubPath("https://public-keys.auth.elb.eu-central-1.amazonaws.com/index.html")); + assertFalse( + AwsAlbKeyConfigurationValidator.containsSubPath("https://public-keys.auth.elb.eu-central-1.amazonaws.com/")); + assertFalse(AwsAlbKeyConfigurationValidator.containsSubPath("https://public-keys.auth.elb.eu-central-1.amazonaws.com")); + } + + @Test + void removeEndingSlash() { + assertTrue(AwsAlbKeyConfigurationValidator.removeEndingSlash("key-location/keyid") + .equals("key-location/keyid")); + assertTrue(AwsAlbKeyConfigurationValidator.removeEndingSlash("key-location/index/keyid/") + .equals("key-location/index/keyid")); + assertTrue(AwsAlbKeyConfigurationValidator.removeEndingSlash("key-location/index.html/") + .equals("key-location/index.html")); + assertTrue(AwsAlbKeyConfigurationValidator.removeEndingSlash("key-location/") + .equals("key-location")); + assertTrue(AwsAlbKeyConfigurationValidator.removeEndingSlash("key-location") + .equals("key-location")); + } +} diff --git a/implementation/jwt-auth/src/test/java/io/smallrye/jwt/auth/principal/AwsAlbKeyResolverTest.java b/implementation/jwt-auth/src/test/java/io/smallrye/jwt/auth/principal/AwsAlbKeyResolverTest.java index 9b9e8bcf..8c29fed0 100644 --- a/implementation/jwt-auth/src/test/java/io/smallrye/jwt/auth/principal/AwsAlbKeyResolverTest.java +++ b/implementation/jwt-auth/src/test/java/io/smallrye/jwt/auth/principal/AwsAlbKeyResolverTest.java @@ -83,4 +83,5 @@ void loadAwsAlbVerificationKey() throws Exception { Key key2 = keyLocationResolver.resolveKey(signature, List.of()); assertTrue(key2 == key); } + }