From 83c3b3eb114c49c050f10cb07bb615a6388d8109 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Tue, 4 Jul 2023 13:08:58 +0100 Subject: [PATCH] Support AWS ALB token verification (#707) * Support AWS ALB token verification * Cache AWS ALB keys * Add properties to control the key cache --- doc/modules/ROOT/pages/configuration.adoc | 3 + .../java/io/smallrye/jwt/KeyProvider.java | 20 ++ .../AbstractKeyLocationResolver.java | 2 +- .../jwt/auth/principal/AwsAlbKeyResolver.java | 171 ++++++++++++++++++ .../auth/principal/DefaultJWTTokenParser.java | 11 +- .../auth/principal/JWTAuthContextInfo.java | 36 +++- .../jwt/auth/principal/PrincipalMessages.java | 6 + .../config/JWTAuthContextInfoProvider.java | 50 +++++ .../auth/principal/AwsAlbKeyResolverTest.java | 86 +++++++++ .../jwt/auth/principal/AwsAlbTokenTest.java | 36 ++++ 10 files changed, 416 insertions(+), 5 deletions(-) create mode 100644 implementation/jwt-auth/src/main/java/io/smallrye/jwt/KeyProvider.java create mode 100644 implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/AwsAlbKeyResolver.java create mode 100644 implementation/jwt-auth/src/test/java/io/smallrye/jwt/auth/principal/AwsAlbKeyResolverTest.java create mode 100644 testsuite/basic/src/test/java/io/smallrye/jwt/auth/principal/AwsAlbTokenTest.java diff --git a/doc/modules/ROOT/pages/configuration.adoc b/doc/modules/ROOT/pages/configuration.adoc index 1e39d4a7..56223303 100644 --- a/doc/modules/ROOT/pages/configuration.adoc +++ b/doc/modules/ROOT/pages/configuration.adoc @@ -42,9 +42,12 @@ SmallRye JWT supports many properties which can be used to customize the token p |smallrye.jwt.verify.key.location|NONE|Location of the verification key which can point to both public and secret keys. Secret keys can only be in the JWK format. Note that 'mp.jwt.verify.publickey.location' will be ignored if this property is set. |smallrye.jwt.verify.algorithm|`RS256`|Signature algorithm. Set it to `ES256` to support the Elliptic Curve signature algorithm. This property is deprecated, use `mp.jwt.verify.publickey.algorithm`. |smallrye.jwt.verify.key-format|`ANY`|Set this property to a specific key format such as `PEM_KEY`, `PEM_CERTIFICATE`, `JWK` or `JWK_BASE64URL` to optimize the way the verification key is loaded. +|smallrye.jwt.verify.key-provider|`DEFAULT`|By default, PEM, JWK or JWK key sets can be read from the local file system or fetched from URIs as required by MicroProfile JWT specification. Set this property to `AWS_ALB` to support an AWS Application Load Balancer verification key resolution. |smallrye.jwt.verify.relax-key-validation|false|Relax the validation of the verification keys, setting this property to `true` will allow public RSA keys with the length less than 2048 bit. |smallrye.jwt.verify.certificate-thumbprint|false|If this property is enabled then a signed token must contain either 'x5t' or 'x5t#S256' X509Certificate thumbprint headers. Verification keys can only be in JWK or PEM Certificate key formats in this case. JWK keys must have a 'x5c' (Base64-encoded X509Certificate) property set. |smallrye.jwt.token.header|`Authorization`|Set this property if another header such as `Cookie` is used to pass the token. This property is deprecated, use `mp.jwt.token.header`. +|smallrye.jwt.key-cache-size|`100`|Key cache size. Use this property, as well as `smallrye.jwt.key-cache-time-to-live`, to control the key cache when a key provider such as `AWS_ALB` is configured with `smallrye.jwt.verify.key-provider=AWS_ALB` for resolving the keys dynamically. +|smallrye.jwt.key-cache-time-to-live|`10`|Key cache entry time-to-live in minutes. Use this property, as well as `smallrye.jwt.key-cache-size`, to control the key cache when a key provider such as `AWS_ALB` is configured with `smallrye.jwt.verify.key-provider=AWS_ALB` for resolving the keys dynamically. |smallrye.jwt.token.cookie|none|Name of the cookie containing a token. This property will be effective only if `smallrye.jwt.token.header` is set to `Cookie`. This property is deprecated, use `mp.jwt.token.cookie`. |smallrye.jwt.always-check-authorization|false|Set this property to `true` for `Authorization` header be checked even if the `smallrye.jwt.token.header` is set to `Cookie` but no cookie with a `smallrye.jwt.token.cookie` name exists. |smallrye.jwt.token.schemes|`Bearer`|Comma-separated list containing an alternative single or multiple schemes, for example, `DPoP`. diff --git a/implementation/jwt-auth/src/main/java/io/smallrye/jwt/KeyProvider.java b/implementation/jwt-auth/src/main/java/io/smallrye/jwt/KeyProvider.java new file mode 100644 index 00000000..150949c5 --- /dev/null +++ b/implementation/jwt-auth/src/main/java/io/smallrye/jwt/KeyProvider.java @@ -0,0 +1,20 @@ +package io.smallrye.jwt; + +/** + * Well-known key providers. + */ +public enum KeyProvider { + /** + * AWS Application Load Balancer. + * + * Verification key in PEM format is fetched from the URI which is created by + * adding the current token `kid` (key identifier) header value to the AWS ALB URI. + */ + AWS_ALB, + + /** + * Verification key is resolved as required by the MP JWT specification: + * PEM or JWK key or JWK key set can be read from the local file system or fetched from URIs. + */ + DEFAULT +} diff --git a/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/AbstractKeyLocationResolver.java b/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/AbstractKeyLocationResolver.java index cdf136dd..59bc77ad 100644 --- a/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/AbstractKeyLocationResolver.java +++ b/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/AbstractKeyLocationResolver.java @@ -314,7 +314,7 @@ protected Key getSecretKeyFromJwk(JsonWebKey jwk) { return null; } - protected X509Certificate loadPEMCertificate(String content) { + protected static X509Certificate loadPEMCertificate(String content) { PrincipalLogging.log.checkKeyContentIsBase64EncodedPEMCertificate(); X509Certificate cert = null; try { 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 new file mode 100644 index 00000000..a8f80eda --- /dev/null +++ b/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/AwsAlbKeyResolver.java @@ -0,0 +1,171 @@ +package io.smallrye.jwt.auth.principal; + +import java.io.IOException; +import java.security.Key; +import java.time.Duration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.jose4j.http.Get; +import org.jose4j.http.SimpleGet; +import org.jose4j.http.SimpleResponse; +import org.jose4j.jwk.JsonWebKey; +import org.jose4j.jws.JsonWebSignature; +import org.jose4j.jwx.JsonWebStructure; +import org.jose4j.keys.resolvers.VerificationKeyResolver; +import org.jose4j.lang.UnresolvableKeyException; + +import io.smallrye.jwt.auth.principal.AbstractKeyLocationResolver.TrustAllHostnameVerifier; +import io.smallrye.jwt.auth.principal.AbstractKeyLocationResolver.TrustedHostsHostnameVerifier; +import io.smallrye.jwt.util.KeyUtils; +import io.smallrye.jwt.util.ResourceUtils; + +public class AwsAlbKeyResolver implements VerificationKeyResolver { + private JWTAuthContextInfo authContextInfo; + private long cacheTimeToLive; + private Map keys = new HashMap<>(); + private AtomicInteger size = new AtomicInteger(); + + public AwsAlbKeyResolver(JWTAuthContextInfo authContextInfo) throws UnresolvableKeyException { + if (authContextInfo.getPublicKeyLocation() == null) { + throw PrincipalMessages.msg.nullKeyLocation(); + } + this.authContextInfo = authContextInfo; + this.cacheTimeToLive = Duration.ofMinutes(authContextInfo.getKeyCacheTimeToLive()).toMillis(); + } + + @Override + public Key resolveKey(JsonWebSignature jws, List nestingContext) throws UnresolvableKeyException { + String kid = jws.getHeaders().getStringHeaderValue(JsonWebKey.KEY_ID_PARAMETER); + verifyKid(kid); + + CacheEntry entry = findValidCacheEntry(kid); + if (entry != null) { + return entry.key; + } else if (prepareSpaceForNewCacheEntry()) { + entry = new CacheEntry(retrieveKey(kid)); + keys.put(kid, entry); + return entry.key; + } else { + return retrieveKey(kid); + } + + } + + protected Key retrieveKey(String kid) throws UnresolvableKeyException { + String keyLocation = authContextInfo.getPublicKeyLocation() + "/" + kid; + SimpleResponse simpleResponse = null; + try { + simpleResponse = getHttpGet().get(keyLocation); + } catch (IOException ex) { + AbstractKeyLocationResolver.reportLoadKeyException(null, keyLocation, ex); + } + String keyContent = simpleResponse.getBody(); + try { + return KeyUtils.decodePublicKey(keyContent, authContextInfo.getSignatureAlgorithm()); + } catch (Exception e) { + AbstractKeyLocationResolver.reportUnresolvableKeyException(keyContent, keyLocation); + } + return null; + } + + protected SimpleGet getHttpGet() throws UnresolvableKeyException { + Get httpGet = new Get(); + if (authContextInfo.isTlsTrustAll()) { + httpGet.setHostnameVerifier(new TrustAllHostnameVerifier()); + } else if (authContextInfo.getTlsTrustedHosts() != null) { + httpGet.setHostnameVerifier(new TrustedHostsHostnameVerifier(authContextInfo.getTlsTrustedHosts())); + } + if (authContextInfo.getTlsCertificate() != null) { + httpGet.setTrustedCertificates( + AbstractKeyLocationResolver.loadPEMCertificate(authContextInfo.getTlsCertificate())); + } else if (authContextInfo.getTlsCertificatePath() != null) { + httpGet.setTrustedCertificates(AbstractKeyLocationResolver.loadPEMCertificate( + readKeyContent(authContextInfo.getTlsCertificatePath()))); + } + return httpGet; + } + + protected String readKeyContent(String keyLocation) throws UnresolvableKeyException { + try { + String content = ResourceUtils.readResource(keyLocation); + if (content == null) { + throw PrincipalMessages.msg.resourceNotFound(keyLocation); + } + return content; + } catch (IOException ex) { + AbstractKeyLocationResolver.reportLoadKeyException(null, keyLocation, ex); + return null; + } + } + + private void verifyKid(String kid) throws UnresolvableKeyException { + if (kid == null) { + throw PrincipalMessages.msg.nullKeyIdentifier(); + } + String expectedKid = authContextInfo.getTokenKeyId(); + if (expectedKid != null && !kid.equals(expectedKid)) { + PrincipalLogging.log.invalidTokenKidHeader(kid, expectedKid); + throw PrincipalMessages.msg.invalidTokenKid(); + } + } + + private void removeInvalidEntries() { + long now = now(); + for (Iterator> it = keys.entrySet().iterator(); it.hasNext();) { + Map.Entry next = it.next(); + if (isEntryExpired(next.getValue(), now)) { + it.remove(); + size.decrementAndGet(); + } + } + } + + private boolean prepareSpaceForNewCacheEntry() { + int currentSize; + do { + currentSize = size.get(); + if (currentSize == authContextInfo.getKeyCacheSize()) { + removeInvalidEntries(); + if (currentSize == authContextInfo.getKeyCacheSize()) { + return false; + } + } + } while (!size.compareAndSet(currentSize, currentSize + 1)); + return true; + } + + private CacheEntry findValidCacheEntry(String kid) { + CacheEntry entry = keys.get(kid); + if (entry != null) { + long now = now(); + if (isEntryExpired(entry, now)) { + // Entry has expired, remote introspection will be required + entry = null; + keys.remove(kid); + size.decrementAndGet(); + } + } + return entry; + } + + private boolean isEntryExpired(CacheEntry entry, long now) { + return entry.createdTime + cacheTimeToLive < now; + } + + private static long now() { + return System.currentTimeMillis(); + } + + private static class CacheEntry { + volatile Key key; + long createdTime = System.currentTimeMillis(); + + public CacheEntry(Key key) { + this.key = key; + } + } +} diff --git a/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/DefaultJWTTokenParser.java b/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/DefaultJWTTokenParser.java index 60d35778..05ae5689 100644 --- a/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/DefaultJWTTokenParser.java +++ b/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/DefaultJWTTokenParser.java @@ -36,6 +36,7 @@ import org.jose4j.lang.JoseException; import org.jose4j.lang.UnresolvableKeyException; +import io.smallrye.jwt.KeyProvider; import io.smallrye.jwt.algorithm.KeyEncryptionAlgorithm; /** @@ -257,9 +258,13 @@ protected VerificationKeyResolver getVerificationKeyResolver(JWTAuthContextInfo if (keyResolver == null) { synchronized (this) { if (keyResolver == null) - keyResolver = authContextInfo.isVerifyCertificateThumbprint() - ? new X509KeyLocationResolver(authContextInfo) - : new KeyLocationResolver(authContextInfo); + if (KeyProvider.AWS_ALB == authContextInfo.getKeyProvider()) { + keyResolver = new AwsAlbKeyResolver(authContextInfo); + } else { + keyResolver = authContextInfo.isVerifyCertificateThumbprint() + ? new X509KeyLocationResolver(authContextInfo) + : new KeyLocationResolver(authContextInfo); + } } } return keyResolver; diff --git a/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/JWTAuthContextInfo.java b/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/JWTAuthContextInfo.java index 770d1d5f..eb4673a9 100644 --- a/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/JWTAuthContextInfo.java +++ b/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/JWTAuthContextInfo.java @@ -27,6 +27,7 @@ import javax.crypto.SecretKey; import io.smallrye.jwt.KeyFormat; +import io.smallrye.jwt.KeyProvider; import io.smallrye.jwt.algorithm.KeyEncryptionAlgorithm; import io.smallrye.jwt.algorithm.SignatureAlgorithm; @@ -47,7 +48,7 @@ public class JWTAuthContextInfo { private String publicKeyContent; private String decryptionKeyLocation; private String decryptionKeyContent; - private Integer jwksRefreshInterval; + private Integer jwksRefreshInterval = 60; private int forcedJwksRefreshInterval = 30; private String tokenHeader = "Authorization"; private String tokenCookie; @@ -64,6 +65,7 @@ public class JWTAuthContextInfo { private Set keyEncryptionAlgorithm = new HashSet<>(Arrays.asList(KeyEncryptionAlgorithm.RSA_OAEP, KeyEncryptionAlgorithm.RSA_OAEP_256)); private KeyFormat keyFormat = KeyFormat.ANY; + private KeyProvider keyProvider = KeyProvider.DEFAULT; private Set expectedAudience; private String groupsSeparator = " "; private Set requiredClaims; @@ -75,6 +77,8 @@ public class JWTAuthContextInfo { private boolean tlsTrustAll; private String httpProxyHost; private int httpProxyPort; + private int keyCacheSize = 100; + private int keyCacheTimeToLive = 10; public JWTAuthContextInfo() { } @@ -129,6 +133,9 @@ public JWTAuthContextInfo(JWTAuthContextInfo orig) { this.signatureAlgorithm = orig.getSignatureAlgorithm(); this.keyEncryptionAlgorithm = orig.getKeyEncryptionAlgorithm(); this.keyFormat = orig.getKeyFormat(); + this.keyProvider = orig.getKeyProvider(); + this.keyCacheSize = orig.getKeyCacheSize(); + this.keyCacheTimeToLive = orig.getKeyCacheTimeToLive(); this.expectedAudience = orig.getExpectedAudience(); this.groupsSeparator = orig.getGroupsSeparator(); this.requiredClaims = orig.getRequiredClaims(); @@ -378,6 +385,14 @@ public void setKeyFormat(KeyFormat keyFormat) { this.keyFormat = keyFormat; } + public KeyProvider getKeyProvider() { + return keyProvider; + } + + public void setKeyProvider(KeyProvider keyProvider) { + this.keyProvider = keyProvider; + } + public boolean isAlwaysCheckAuthorization() { return alwaysCheckAuthorization; } @@ -424,6 +439,9 @@ public String toString() { ", signatureAlgorithm=" + signatureAlgorithm + ", keyEncryptionAlgorithm=" + keyEncryptionAlgorithm + ", keyFormat=" + keyFormat + + ", keyProvider=" + keyProvider + + ", keyCacheSize=" + keyCacheSize + + ", keyCacheTimeToLive=" + keyCacheTimeToLive + ", expectedAudience=" + expectedAudience + ", groupsSeparator='" + groupsSeparator + '\'' + ", relaxVerificationKeyValidation=" + relaxVerificationKeyValidation + @@ -515,4 +533,20 @@ public int getClockSkew() { public void setClockSkew(int clockSkew) { this.clockSkew = clockSkew; } + + public int getKeyCacheTimeToLive() { + return keyCacheTimeToLive; + } + + public void setKeyCacheTimeToLive(int keyCacheTimeToLive) { + this.keyCacheTimeToLive = keyCacheTimeToLive; + } + + public int getKeyCacheSize() { + return keyCacheSize; + } + + public void setKeyCacheSize(int keyCacheSize) { + this.keyCacheSize = keyCacheSize; + } } diff --git a/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/PrincipalMessages.java b/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/PrincipalMessages.java index 31169135..57944558 100644 --- a/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/PrincipalMessages.java +++ b/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/PrincipalMessages.java @@ -74,4 +74,10 @@ interface PrincipalMessages { @Message(id = 7018, value = "The token age has exceeded %d seconds") ParseException tokenAgeExceeded(long tokenAge); + + @Message(id = 7019, value = "Required key location is null") + UnresolvableKeyException nullKeyLocation(); + + @Message(id = 7020, value = "Required key identifier is null") + UnresolvableKeyException nullKeyIdentifier(); } \ No newline at end of file diff --git a/implementation/jwt-auth/src/main/java/io/smallrye/jwt/config/JWTAuthContextInfoProvider.java b/implementation/jwt-auth/src/main/java/io/smallrye/jwt/config/JWTAuthContextInfoProvider.java index 263a3d94..1db280bd 100644 --- a/implementation/jwt-auth/src/main/java/io/smallrye/jwt/config/JWTAuthContextInfoProvider.java +++ b/implementation/jwt-auth/src/main/java/io/smallrye/jwt/config/JWTAuthContextInfoProvider.java @@ -35,6 +35,7 @@ import org.eclipse.microprofile.jwt.config.Names; import io.smallrye.jwt.KeyFormat; +import io.smallrye.jwt.KeyProvider; import io.smallrye.jwt.SmallryeJwtUtils; import io.smallrye.jwt.algorithm.KeyEncryptionAlgorithm; import io.smallrye.jwt.algorithm.SignatureAlgorithm; @@ -188,6 +189,7 @@ private static JWTAuthContextInfoProvider create(String publicKey, provider.mpJwtDecryptKeyAlgorithm = new HashSet<>(Arrays.asList(KeyEncryptionAlgorithm.RSA_OAEP, KeyEncryptionAlgorithm.RSA_OAEP_256)); provider.keyFormat = KeyFormat.ANY; + provider.keyProvider = KeyProvider.DEFAULT; provider.mpJwtVerifyAudiences = Optional.empty(); provider.expectedAudience = Optional.empty(); provider.groupsSeparator = DEFAULT_GROUPS_SEPARATOR; @@ -488,6 +490,51 @@ private static JWTAuthContextInfoProvider create(String publicKey, @ConfigProperty(name = "smallrye.jwt.verify.key-format", defaultValue = "ANY") private KeyFormat keyFormat; + /** + * Supported verification key provider. + */ + @Inject + @ConfigProperty(name = "smallrye.jwt.verify.key-provider", defaultValue = "DEFAULT") + private KeyProvider keyProvider; + + /** + * Key cache size. + * If the verification key resolver acquires keys dynamically on every request then it may + * use this property, as well as {@link #keyCacheTimeToLive}, to control the key cache. + *

+ * For example, setting `smallrye.jwt.verify.key-provider=AWS_ALB` will activate + * AWS ALB verification key resolver which will use the current token `kid` header value + * to fetch the key from the AWS ALB key endpoint. This resolve will need to cache the acquired + * keys and control the cache size. + *

+ * If the maximum key cache size has been reached then the cache entries which have been in the cache + * for longer than the configured {@link #keyCacheTimeToLive} duration, should be removed and a new + * key added instead. If the cache size can not be reduced then the key resolver should + * return this key without caching it. + */ + @Inject + @ConfigProperty(name = "smallrye.jwt.key-cache-size", defaultValue = "100") + private int keyCacheSize; + + /** + * Key cache entry time-to-live duration in minutes. + * If the verification key resolver acquires keys dynamically on every request then it may + * use this property, as well as {@link #keyCacheSize}, to control the key cache. + *

+ * For example, setting `smallrye.jwt.verify.key-provider=AWS_ALB` will activate + * AWS ALB verification key resolver which will use the current token `kid` header value + * to fetch the key from the AWS ALB key endpoint. This resolve will need to cache the acquired + * keys. + *

+ * If the maximum key cache size {@link #keyCacheSize} has been reached then the cache entries which have been in the cache + * for longer than the configured time-to-live duration, should be removed and a new + * key added instead. If the cache size can not be reduced then the key resolver should + * return this key without caching it. + */ + @Inject + @ConfigProperty(name = "smallrye.jwt.key-cache-time-to-live", defaultValue = "10") + private int keyCacheTimeToLive; + /** * Relax the validation of the verification keys. * Public RSA keys with the 1024 bit length will be allowed if this property is set to 'true'. @@ -762,6 +809,9 @@ Optional getOptionalContextInfo() { } contextInfo.setKeyEncryptionAlgorithm(theDecryptionKeyAlgorithm); contextInfo.setKeyFormat(keyFormat); + contextInfo.setKeyProvider(keyProvider); + contextInfo.setKeyCacheSize(keyCacheSize); + contextInfo.setKeyCacheTimeToLive(keyCacheTimeToLive); if (mpJwtVerifyAudiences.isPresent()) { contextInfo.setExpectedAudience(mpJwtVerifyAudiences.get()); } else if (expectedAudience.isPresent()) { 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 new file mode 100644 index 00000000..9b9e8bcf --- /dev/null +++ b/implementation/jwt-auth/src/test/java/io/smallrye/jwt/auth/principal/AwsAlbKeyResolverTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2019 Red Hat, Inc, and individual contributors. + * + * 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 io.smallrye.jwt.auth.principal; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +import java.security.Key; +import java.security.interfaces.ECPublicKey; +import java.util.List; + +import org.jose4j.http.SimpleGet; +import org.jose4j.http.SimpleResponse; +import org.jose4j.jwk.JsonWebKey; +import org.jose4j.jws.JsonWebSignature; +import org.jose4j.jwx.Headers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import io.smallrye.jwt.algorithm.SignatureAlgorithm; + +@ExtendWith(MockitoExtension.class) +class AwsAlbKeyResolverTest { + + private static final String AWS_ALB_KEY = "-----BEGIN PUBLIC KEY-----" + + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjPHY1j9umvc8nZEswOzs+lPpLKLn" + + "qCBqvyZGJfBlXapmtGiqYEwpIqh/lZdkr4wDii7CP1DzIUSHONbc+jufiQ==" + + "-----END PUBLIC KEY-----"; + + @Mock + JsonWebSignature signature; + @Mock + Headers headers; + @Mock + SimpleGet simpleGet; + @Mock + SimpleResponse simpleResponse; + + AwsAlbKeyResolverTest() throws Exception { + + } + + @Test + void loadAwsAlbVerificationKey() throws Exception { + JWTAuthContextInfo contextInfo = new JWTAuthContextInfo( + "https://localhost:8080", + "https://cognito-idp.eu-central-1.amazonaws.com"); + contextInfo.setSignatureAlgorithm(SignatureAlgorithm.ES256); + + AwsAlbKeyResolver keyLocationResolver = new AwsAlbKeyResolver(contextInfo); + keyLocationResolver = Mockito.spy(keyLocationResolver); + + when(keyLocationResolver.getHttpGet()).thenReturn(simpleGet); + + when(simpleGet.get("https://localhost:8080/c2f80c8b-c05c-4068-af14-17299f7896b1")) + .thenReturn(simpleResponse); + + when(simpleResponse.getBody()).thenReturn(AWS_ALB_KEY); + + when(signature.getHeaders()).thenReturn(headers); + when(headers.getStringHeaderValue(JsonWebKey.KEY_ID_PARAMETER)).thenReturn("c2f80c8b-c05c-4068-af14-17299f7896b1"); + + Key key = keyLocationResolver.resolveKey(signature, List.of()); + assertTrue(key instanceof ECPublicKey); + // Confirm the cached key is returned + Key key2 = keyLocationResolver.resolveKey(signature, List.of()); + assertTrue(key2 == key); + } +} diff --git a/testsuite/basic/src/test/java/io/smallrye/jwt/auth/principal/AwsAlbTokenTest.java b/testsuite/basic/src/test/java/io/smallrye/jwt/auth/principal/AwsAlbTokenTest.java new file mode 100644 index 00000000..b4434eec --- /dev/null +++ b/testsuite/basic/src/test/java/io/smallrye/jwt/auth/principal/AwsAlbTokenTest.java @@ -0,0 +1,36 @@ +package io.smallrye.jwt.auth.principal; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.eclipse.microprofile.jwt.JsonWebToken; +import org.junit.jupiter.api.Test; + +import io.smallrye.jwt.algorithm.SignatureAlgorithm; + +public class AwsAlbTokenTest { + + private static final String AWS_ALB_KEY = "-----BEGIN PUBLIC KEY-----" + + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjPHY1j9umvc8nZEswOzs+lPpLKLn" + + "qCBqvyZGJfBlXapmtGiqYEwpIqh/lZdkr4wDii7CP1DzIUSHONbc+jufiQ==" + + "-----END PUBLIC KEY-----"; + + private static final String JWT = "eyJ0eXAiOiJKV1QiLCJraWQiOiJjMmY4MGM4Yi1jMDVjLTQwNjgtYWYxNC0xNzI5OWY3ODk2YjEiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXUtY2VudHJhbC0xLmFtYXpvbmF3cy5jb20vZXUtY2VudHJhbC0xX015UnJPQ0hRdyIsImNsaWVudCI6IjRmbXZodDIydGpyZ2Q3ZDNrM3RnaHR0Y3Q3Iiwic2lnbmVyIjoiYXJuOmF3czplbGFzdGljbG9hZGJhbGFuY2luZzpldS1jZW50cmFsLTE6MTk3MjgwOTU4MjI1OmxvYWRiYWxhbmNlci9hcHAvZWNzLXdpdGgtY29nbml0by1sYi82Mjg0YmU2NWI4MjdjNTk4IiwiZXhwIjoxNjg3NzQ4MDQ1fQ==" + + ".eyJzdWIiOiIyM2Q0OThiMi0zMDMxLTcwZDItOGExNS00OWRkODg2YTA4N2IiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6ImR1a2VAc3VuLmNvbSIsInVzZXJuYW1lIjoiZHVrZSIsImV4cCI6MTY4Nzc0ODA0NSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5ldS1jZW50cmFsLTEuYW1hem9uYXdzLmNvbS9ldS1jZW50cmFsLTFfTXlSck9DSFF3In0=" + + ".Jd7RXHsOj8vw2b4irZCxxWO-0UQBZ2X1bRNsKZ9D02JWJaNOvOnrV8T-qrcmWNpl7MjNhsGSm1C4e2rAjaF0jg=="; + + @Test + void parseToken() throws Exception { + JWTAuthContextInfo config = new JWTAuthContextInfo(); + config.setPublicKeyContent(AWS_ALB_KEY); + config.setIssuedBy("https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_MyRrOCHQw"); + // ES256 is used to sign + config.setSignatureAlgorithm(SignatureAlgorithm.ES256); + // Token has no `iat` + config.setMaxTimeToLiveSecs(-1L); + // It has already expired so for the test to pass the clock skew has to be set + config.setClockSkew(Integer.MAX_VALUE); + JWTParser parser = new DefaultJWTParser(config); + JsonWebToken jwt = parser.parse(JWT); + assertEquals("duke", jwt.getClaim("username")); + } +}