From 1c1339fb4b001937b4545636b574db5cd9fadf87 Mon Sep 17 00:00:00 2001 From: Thijs Lemmens Date: Tue, 16 Jul 2024 17:49:03 +0200 Subject: [PATCH 1/9] ACC-1493 prevent looking up the keys on every sign call --- .../JwtInternalIssuerConfiguration.java | 14 ++- .../PropertiesBasedJwtClaimsSigner.java | 83 ++++++-------- .../PropertiesBasedJwtSignerRegistry.java | 45 ++++++-- .../jwk/source/FilebasedJWKSetSource.java | 108 ++++++++++++++++++ .../LoggingJWKSetSourceEventListener.java | 32 ++++++ .../PropertiesBasedJwtClaimsSignerTest.java | 83 ++++++++------ 6 files changed, 268 insertions(+), 97 deletions(-) create mode 100644 src/main/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSource.java create mode 100644 src/main/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/LoggingJWKSetSourceEventListener.java diff --git a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/JwtInternalIssuerConfiguration.java b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/JwtInternalIssuerConfiguration.java index 5593ef55..6193f4db 100644 --- a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/JwtInternalIssuerConfiguration.java +++ b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/JwtInternalIssuerConfiguration.java @@ -2,7 +2,12 @@ import com.contentgrid.gateway.security.jwt.issuer.JwtInternalIssuerConfiguration.ContentgridGatewayJwtProperties; import com.contentgrid.gateway.security.jwt.issuer.actuate.JWKSetEndpoint; +import com.contentgrid.gateway.security.jwt.issuer.jwk.source.FilebasedJWKSetSource; +import com.contentgrid.gateway.security.jwt.issuer.jwk.source.LoggingJWKSetSourceEventListener; import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.jwk.source.JWKSourceBuilder; +import com.nimbusds.jose.proc.SecurityContext; import io.micrometer.observation.ObservationRegistry; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; @@ -22,16 +27,17 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.support.ResourcePatternResolver; @RequiredArgsConstructor @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(ContentgridGatewayJwtProperties.class) @Slf4j public class JwtInternalIssuerConfiguration { - + @Bean - JwtSignerRegistry jwtSignerRegistry(ContentgridGatewayJwtProperties properties, ApplicationContext applicationContext) { - return new PropertiesBasedJwtSignerRegistry(properties, applicationContext); + JwtSignerRegistry jwtSignerRegistry(ContentgridGatewayJwtProperties gatewayJwtProperties, ResourcePatternResolver resourcePatternResolver) { + return new PropertiesBasedJwtSignerRegistry(gatewayJwtProperties, resourcePatternResolver); } @Bean @@ -75,7 +81,7 @@ static class ContentgridGatewayJwtProperties { static class JwtSignerProperties implements PropertiesBasedJwtClaimsSigner.JwtClaimsSignerProperties { @NotNull private String activeKeys; - private String allKeys; + private String retiredKeys; @Builder.Default private Set algorithms = Set.of(JWSAlgorithm.RS256); diff --git a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSigner.java b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSigner.java index 965e95ba..167e40ff 100644 --- a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSigner.java +++ b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSigner.java @@ -7,17 +7,28 @@ import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory; import com.nimbusds.jose.jwk.ECKey; import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKMatcher; +import com.nimbusds.jose.jwk.JWKSelector; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.KeyUse; import com.nimbusds.jose.jwk.OctetKeyPair; import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.CachingJWKSetSource; +import com.nimbusds.jose.jwk.source.JWKSetSource; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.jwk.source.JWKSourceBuilder; +import com.nimbusds.jose.jwk.source.URLBasedJWKSetSource; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jose.proc.SimpleSecurityContext; import com.nimbusds.jose.produce.JWSSignerFactory; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; import java.nio.charset.StandardCharsets; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Random; @@ -30,63 +41,29 @@ @RequiredArgsConstructor public class PropertiesBasedJwtClaimsSigner implements JwtClaimsSigner { - private final JwtClaimsSignerProperties properties; - private final ResourcePatternResolver resourcePatternResolver; + private final JWSSignerFactory jwsSignerFactory; private final Random random; + private final JWKSource jwkSource; + private final Set algorithms; + - public PropertiesBasedJwtClaimsSigner(JwtClaimsSignerProperties properties, ResourcePatternResolver resourcePatternResolver) { - this(properties, resourcePatternResolver, new DefaultJWSSignerFactory(), new Random()); + public PropertiesBasedJwtClaimsSigner(JWKSource jwkSource, Set algorithms) { + this(new DefaultJWSSignerFactory(), new Random(), jwkSource, algorithms); } public interface JwtClaimsSignerProperties { String getActiveKeys(); - String getAllKeys(); + String getRetiredKeys(); Set getAlgorithms(); } @SneakyThrows - private static JWK createFromSigningKey(Resource resource) { - var jwk = JWK.parseFromPEMEncodedObjects(resource.getContentAsString(StandardCharsets.UTF_8)); - if(jwk instanceof RSAKey rsaKey) { - return new RSAKey.Builder(rsaKey) - .keyIDFromThumbprint() - .keyUse(KeyUse.SIGNATURE) - .build(); - } else if(jwk instanceof ECKey ecKey) { - return new ECKey.Builder(ecKey) - .keyIDFromThumbprint() - .keyUse(KeyUse.SIGNATURE) - .build(); - } else if(jwk instanceof OctetKeyPair octetKeyPair) { - return new OctetKeyPair.Builder(octetKeyPair) - .keyIDFromThumbprint() - .keyUse(KeyUse.SIGNATURE) - .build(); - } else { - throw new IllegalArgumentException("Unsupported JWK key type %s; use RSA, EC or OKP".formatted(jwk.getKeyType())); - } - } - - @SneakyThrows - private Stream createSigningKeysFromPath(String path) { - if(path == null) { - return Stream.empty(); - } - return Arrays.stream(resourcePatternResolver.getResources(path)) - .map(PropertiesBasedJwtClaimsSigner::createFromSigningKey); - } - - private List getActiveSigningKeys() { - return createSigningKeysFromPath(properties.getActiveKeys()) - .toList(); - } - private List getAllSigningKeys() { - return Stream.concat( - getActiveSigningKeys().stream(), - createSigningKeysFromPath(properties.getAllKeys()) - ).toList(); + return jwkSource.get(new JWKSelector(new JWKMatcher.Builder() + .keyUse(KeyUse.SIGNATURE) + .build()), + new SimpleSecurityContext()); } @Override @@ -96,15 +73,21 @@ public JWKSet getSigningKeys() { @Override public SignedJWT sign(JWTClaimsSet jwtClaimsSet) throws JOSEException { - var activeKeys = new ArrayList<>(getActiveSigningKeys()); - Collections.shuffle(activeKeys, this.random); // Randomly shuffle our active keys, so we pick an arbitrary one first + var jwks = new ArrayList<>(getAllSigningKeys()); + + Collections.shuffle(jwks, this.random); // Randomly shuffle our active keys, so we pick an arbitrary one first Set algorithmsSupportedByKeys = new HashSet<>(); - for (JWK selectedKey : activeKeys) { + for (JWK selectedKey : jwks) { + if (selectedKey.getExpirationTime() != null && !new Date().before(selectedKey.getExpirationTime())) { + // Skip retired keys + continue; + } + var selectedSigner = jwsSignerFactory.createJWSSigner(selectedKey); algorithmsSupportedByKeys.addAll(selectedSigner.supportedJWSAlgorithms()); - var firstSupportedAlgorithm = properties.getAlgorithms() + var firstSupportedAlgorithm = algorithms .stream() .filter(selectedSigner.supportedJWSAlgorithms()::contains) .findFirst(); @@ -122,7 +105,7 @@ public SignedJWT sign(JWTClaimsSet jwtClaimsSet) throws JOSEException { return signedJwt; } throw new IllegalStateException("No active signing keys support any of the configured algorithms (%s); algorithms that can be used by these keys are %s".formatted( - properties.getAlgorithms(), + algorithms, algorithmsSupportedByKeys )); } diff --git a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtSignerRegistry.java b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtSignerRegistry.java index cf1016a9..62d599de 100644 --- a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtSignerRegistry.java +++ b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtSignerRegistry.java @@ -1,7 +1,13 @@ package com.contentgrid.gateway.security.jwt.issuer; import com.contentgrid.gateway.security.jwt.issuer.JwtInternalIssuerConfiguration.ContentgridGatewayJwtProperties; +import com.contentgrid.gateway.security.jwt.issuer.jwk.source.FilebasedJWKSetSource; +import com.contentgrid.gateway.security.jwt.issuer.jwk.source.LoggingJWKSetSourceEventListener; import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.jwk.source.JWKSourceBuilder; +import com.nimbusds.jose.proc.SecurityContext; +import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; @@ -11,13 +17,14 @@ @RequiredArgsConstructor class PropertiesBasedJwtSignerRegistry implements JwtSignerRegistry { - private final ContentgridGatewayJwtProperties properties; - private final ResourcePatternResolver resourcePatternResolver; private final Map instantiatedSigners = new ConcurrentHashMap<>(); + private final ContentgridGatewayJwtProperties gatewayJwtProperties; + private final ResourcePatternResolver resourcePatternResolver; + @Override public boolean hasSigner(String signerName) { - return properties.getSigners().containsKey(signerName); + return getJwkSourceMap().containsKey(signerName); } @Override @@ -42,14 +49,38 @@ public JwtClaimsSigner getRequiredSigner(String signerName) { return instantiatedSigners.computeIfAbsent(signerName, this::createSigner); } + private Map> getJwkSourceMap() { + Map> jwkSourceMap = new HashMap<>(); + gatewayJwtProperties.getSigners().keySet().stream().forEach( + signerName -> { + var signerProperties = gatewayJwtProperties.getSigners().get(signerName); + if (signerProperties == null) { + throw new IllegalArgumentException( + "No JWT signer named '%s'. Available signers are %s".formatted(signerName, + gatewayJwtProperties.getSigners().keySet())); + } + var jwkSource = new FilebasedJWKSetSource( + resourcePatternResolver, + signerProperties.getActiveKeys(), + signerProperties.getRetiredKeys() + ); + jwkSourceMap.put(signerName, JWKSourceBuilder.create(jwkSource) + .refreshAheadCache(JWKSourceBuilder.DEFAULT_REFRESH_AHEAD_TIME, true, new LoggingJWKSetSourceEventListener<>()) + .build()); + } + ); + + return jwkSourceMap; + } + private JwtClaimsSigner createSigner(String signerName) { - var signerProperties = properties.getSigners().get(signerName); - if (signerProperties == null) { + if (!hasSigner(signerName)) { throw new IllegalArgumentException( "No JWT signer named '%s'. Available signers are %s".formatted(signerName, - properties.getSigners().keySet())); + getJwkSourceMap().keySet())); } - return new PropertiesBasedJwtClaimsSigner(signerProperties, resourcePatternResolver); + + return new PropertiesBasedJwtClaimsSigner(getJwkSourceMap().get(signerName), gatewayJwtProperties.getSigners().get(signerName).getAlgorithms()); } } diff --git a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSource.java b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSource.java new file mode 100644 index 00000000..29da0bcc --- /dev/null +++ b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSource.java @@ -0,0 +1,108 @@ +package com.contentgrid.gateway.security.jwt.issuer.jwk.source; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.KeySourceException; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.OctetKeyPair; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.JWKSetCacheRefreshEvaluator; +import com.nimbusds.jose.jwk.source.JWKSetParseException; +import com.nimbusds.jose.jwk.source.JWKSetSource; +import com.nimbusds.jose.jwk.source.JWKSetUnavailableException; +import com.nimbusds.jose.proc.SecurityContext; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.ResourcePatternResolver; + +/** + * Loads JWKs from PEM-encoded private key files. + * + * There is a distinction made between active and retired signing keys + */ +@RequiredArgsConstructor +public class FilebasedJWKSetSource implements JWKSetSource { + private final ResourcePatternResolver resourcePatternResolver; + + private final String activeSigningKeysPattern; + private final String retiredSigningKeysPattern; + + @Override + public JWKSet getJWKSet(JWKSetCacheRefreshEvaluator refreshEvaluator, long currentTime, SecurityContext context) + throws KeySourceException { + try { + Map keys = new LinkedHashMap<>(); + for (Resource signingKeyResource : resourcePatternResolver.getResources(activeSigningKeysPattern)) { + var signingKey = createFromSigningKey(signingKeyResource, null); + keys.put(signingKey.getKeyID(), signingKey); + } + if (retiredSigningKeysPattern != null) { + for (Resource signingKeyResource : resourcePatternResolver.getResources(retiredSigningKeysPattern)) { + var signingKey = createFromSigningKey(signingKeyResource, new Date(currentTime)); + // putIfAbsent, so we don't replace an active key with a retired key if it is matched by both patterns + keys.putIfAbsent(signingKey.getKeyID(), signingKey); + } + } + return new JWKSet(new ArrayList<>(keys.values())); + } catch (IOException e) { + throw new JWKSetUnavailableException("Failed to load JWKs from file", e); + } catch (JOSEException e) { + throw new JWKSetParseException("Failed to load JWKs from keys", e); + } + } + + private static JWK createFromSigningKey(Resource resource, Date expirationTime) throws IOException, JOSEException { + String signingKeyString = resource.getContentAsString(StandardCharsets.UTF_8); + JWK jwk; + if(signingKeyString.startsWith("{")) { + try { + jwk = JWK.parse(signingKeyString); + } catch (ParseException e) { + throw new IllegalArgumentException("Can not parse JWK %s: %s".formatted(resource, e.getMessage()), e); + } + } else { + var decodedKey = JWK.parseFromPEMEncodedObjects(signingKeyString); + if(decodedKey instanceof RSAKey rsaKey) { + jwk = new RSAKey.Builder(rsaKey) + .keyIDFromThumbprint() + .keyUse(KeyUse.SIGNATURE) + .expirationTime(expirationTime) + .build(); + } else if(decodedKey instanceof ECKey ecKey) { + jwk = new ECKey.Builder(ecKey) + .keyIDFromThumbprint() + .keyUse(KeyUse.SIGNATURE) + .expirationTime(expirationTime) + .build(); + } else if(decodedKey instanceof OctetKeyPair octetKeyPair) { + jwk = new OctetKeyPair.Builder(octetKeyPair) + .keyIDFromThumbprint() + .keyUse(KeyUse.SIGNATURE) + .expirationTime(expirationTime) + .build(); + } else { + throw new IllegalArgumentException("Unsupported JWK %s: Unsupported key type %s".formatted(resource, decodedKey.getKeyType())); + } + } + + if(jwk.toPublicJWK() == null) { + throw new IllegalArgumentException("Unsupported JWK %s: no public key available".formatted(resource)); + } + + return jwk; + } + + @Override + public void close() throws IOException { + + } +} diff --git a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/LoggingJWKSetSourceEventListener.java b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/LoggingJWKSetSourceEventListener.java new file mode 100644 index 00000000..48b6361c --- /dev/null +++ b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/LoggingJWKSetSourceEventListener.java @@ -0,0 +1,32 @@ +package com.contentgrid.gateway.security.jwt.issuer.jwk.source; + +import com.nimbusds.jose.jwk.source.CachingJWKSetSource; +import com.nimbusds.jose.jwk.source.JWKSetSource; +import com.nimbusds.jose.jwk.source.OutageTolerantJWKSetSource; +import com.nimbusds.jose.jwk.source.RefreshAheadCachingJWKSetSource; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jose.util.events.Event; +import com.nimbusds.jose.util.events.EventListener; +import java.time.Duration; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class LoggingJWKSetSourceEventListener, C extends SecurityContext> implements EventListener { + + @Override + public void notify(Event event) { + if(event instanceof RefreshAheadCachingJWKSetSource.ScheduledRefreshFailed scheduledRefreshFailedEvent) { + log.error("Failed to refresh JWKSet {}", scheduledRefreshFailedEvent.getSource(), + scheduledRefreshFailedEvent.getException()); + } else if(event instanceof CachingJWKSetSource.UnableToRefreshEvent unableToRefreshEvent) { + log.error("Failed to refresh JWKSet {}", unableToRefreshEvent.getSource()); + } else if(event instanceof CachingJWKSetSource.RefreshInitiatedEvent refreshInitiatedEvent) { + log.debug("Starting refresh of JWKSet {}", refreshInitiatedEvent.getSource()); + } else if(event instanceof CachingJWKSetSource.RefreshCompletedEvent refreshCompletedEvent) { + log.debug("JWKSet {} was refreshed", refreshCompletedEvent.getSource()); + } else if(event instanceof OutageTolerantJWKSetSource.OutageEvent outageEvent) { + log.warn("JWKSet {} is unavailable; fallback remains available for {}", outageEvent.getSource(), Duration.ofMillis(outageEvent.getRemainingTime()), outageEvent.getException()); + } + + } +} diff --git a/src/test/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSignerTest.java b/src/test/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSignerTest.java index 50807d77..b197c519 100644 --- a/src/test/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSignerTest.java +++ b/src/test/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSignerTest.java @@ -1,9 +1,12 @@ package com.contentgrid.gateway.security.jwt.issuer; -import static com.contentgrid.gateway.test.security.CryptoTestUtils.*; +import static com.contentgrid.gateway.test.security.CryptoTestUtils.createKeyPair; +import static com.contentgrid.gateway.test.security.CryptoTestUtils.toPrivateKeyResource; +import static com.contentgrid.gateway.test.security.CryptoTestUtils.toPublicKeyResource; import static org.assertj.core.api.Assertions.assertThat; import com.contentgrid.gateway.security.jwt.issuer.PropertiesBasedJwtClaimsSigner.JwtClaimsSignerProperties; +import com.contentgrid.gateway.security.jwt.issuer.jwk.source.FilebasedJWKSetSource; import com.contentgrid.gateway.test.util.MockResourcePatternResolver; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JWSAlgorithm; @@ -14,6 +17,9 @@ import com.nimbusds.jose.crypto.factories.DefaultJWSVerifierFactory; import com.nimbusds.jose.jwk.AsymmetricJWK; import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.source.JWKSetBasedJWKSource; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.jwk.source.JWKSourceBuilder; import com.nimbusds.jwt.JWTClaimsSet; import java.nio.charset.StandardCharsets; import java.security.interfaces.ECPublicKey; @@ -40,6 +46,13 @@ static Random createDeterministicRandom() { return new Random(5); } + static JWKSource getJwkSource(ResourcePatternResolver resolver) { + var filebasedJWKSetSource = new FilebasedJWKSetSource(resolver, "file:/keys/active*.pem", + "file:/keys/retired-*.pem"); + return new JWKSetBasedJWKSource(filebasedJWKSetSource); + } + + @Test void signs_with_active_rsa_key() throws ParseException, JOSEException { var activeKey = createKeyPair("RSA", 2048); @@ -47,10 +60,9 @@ void signs_with_active_rsa_key() throws ParseException, JOSEException { .resource("file:/keys/active.pem", toPrivateKeyResource(activeKey)) .build(); - var signer = new PropertiesBasedJwtClaimsSigner(MockJwtClaimsSignerProperties.builder() - .activeKeys("file:/keys/active.pem") - .build(), - resolver + var signer = new PropertiesBasedJwtClaimsSigner( + getJwkSource(resolver), + Set.of(JWSAlgorithm.RS256) ); var signedJwt = signer.sign(JWTClaimsSet.parse(Map.of("test", "test"))); @@ -72,11 +84,8 @@ void provides_all_signing_keys() throws ParseException, JOSEException { .build(); var signer = new PropertiesBasedJwtClaimsSigner( - MockJwtClaimsSignerProperties.builder() - .activeKeys("file:/keys/active.pem") - .allKeys("file:/keys/retired-*.pem") - .build(), - resolver + getJwkSource(resolver), + Set.of(JWSAlgorithm.RS256) ); var signedJwt = signer.sign(JWTClaimsSet.parse(Map.of("test", "test"))); @@ -87,9 +96,12 @@ void provides_all_signing_keys() throws ParseException, JOSEException { assertThat(signer.getSigningKeys()).satisfies(jwks -> { assertThat(jwks.getKeys()).hasSize(3); - assertThat(jwks.containsJWK(JWK.parseFromPEMEncodedObjects(toPublicKeyResource(activeKey).getContentAsString(StandardCharsets.UTF_8)))).isTrue(); - assertThat(jwks.containsJWK(JWK.parseFromPEMEncodedObjects(toPublicKeyResource(retiredKey1).getContentAsString(StandardCharsets.UTF_8)))).isTrue(); - assertThat(jwks.containsJWK(JWK.parseFromPEMEncodedObjects(toPublicKeyResource(retiredKey2).getContentAsString(StandardCharsets.UTF_8)))).isTrue(); + assertThat(jwks.containsJWK(JWK.parseFromPEMEncodedObjects( + toPublicKeyResource(activeKey).getContentAsString(StandardCharsets.UTF_8)))).isTrue(); + assertThat(jwks.containsJWK(JWK.parseFromPEMEncodedObjects( + toPublicKeyResource(retiredKey1).getContentAsString(StandardCharsets.UTF_8)))).isTrue(); + assertThat(jwks.containsJWK(JWK.parseFromPEMEncodedObjects( + toPublicKeyResource(retiredKey2).getContentAsString(StandardCharsets.UTF_8)))).isTrue(); }); } @@ -112,11 +124,8 @@ void uses_new_keys_when_rotated() throws ParseException, JOSEException { var resolver = new DelegateResourcePatternResolver(oldResolver); var signer = new PropertiesBasedJwtClaimsSigner( - MockJwtClaimsSignerProperties.builder() - .activeKeys("file:/keys/active.pem") - .allKeys("file:/keys/retired-*.pem") - .build(), - resolver + getJwkSource(resolver), + Set.of(JWSAlgorithm.RS256) ); var oldSignedJwt = signer.sign(JWTClaimsSet.parse(Map.of("test", "test"))); @@ -127,8 +136,10 @@ void uses_new_keys_when_rotated() throws ParseException, JOSEException { assertThat(signer.getSigningKeys()).satisfies(jwks -> { assertThat(jwks.getKeys()).hasSize(2); - assertThat(jwks.containsJWK(JWK.parseFromPEMEncodedObjects(toPublicKeyResource(oldActiveKey).getContentAsString(StandardCharsets.UTF_8)))).isTrue(); - assertThat(jwks.containsJWK(JWK.parseFromPEMEncodedObjects(toPublicKeyResource(oldRetiredKey).getContentAsString(StandardCharsets.UTF_8)))).isTrue(); + assertThat(jwks.containsJWK(JWK.parseFromPEMEncodedObjects( + toPublicKeyResource(oldActiveKey).getContentAsString(StandardCharsets.UTF_8)))).isTrue(); + assertThat(jwks.containsJWK(JWK.parseFromPEMEncodedObjects( + toPublicKeyResource(oldRetiredKey).getContentAsString(StandardCharsets.UTF_8)))).isTrue(); }); // Rotate the keys @@ -142,9 +153,12 @@ void uses_new_keys_when_rotated() throws ParseException, JOSEException { assertThat(signer.getSigningKeys()).satisfies(jwks -> { assertThat(jwks.getKeys()).hasSize(3); - assertThat(jwks.containsJWK(JWK.parseFromPEMEncodedObjects(toPublicKeyResource(newActiveKey).getContentAsString(StandardCharsets.UTF_8)))).isTrue(); - assertThat(jwks.containsJWK(JWK.parseFromPEMEncodedObjects(toPublicKeyResource(oldActiveKey).getContentAsString(StandardCharsets.UTF_8)))).isTrue(); - assertThat(jwks.containsJWK(JWK.parseFromPEMEncodedObjects(toPublicKeyResource(oldRetiredKey).getContentAsString(StandardCharsets.UTF_8)))).isTrue(); + assertThat(jwks.containsJWK(JWK.parseFromPEMEncodedObjects( + toPublicKeyResource(newActiveKey).getContentAsString(StandardCharsets.UTF_8)))).isTrue(); + assertThat(jwks.containsJWK(JWK.parseFromPEMEncodedObjects( + toPublicKeyResource(oldActiveKey).getContentAsString(StandardCharsets.UTF_8)))).isTrue(); + assertThat(jwks.containsJWK(JWK.parseFromPEMEncodedObjects( + toPublicKeyResource(oldRetiredKey).getContentAsString(StandardCharsets.UTF_8)))).isTrue(); }); } @@ -160,12 +174,10 @@ void rotates_multiple_active_keys() throws ParseException, JOSEException { .build(); var signer = new PropertiesBasedJwtClaimsSigner( - MockJwtClaimsSignerProperties.builder() - .activeKeys("file:/keys/active-*.pem") - .build(), - resolver, new DefaultJWSSignerFactory(), - createDeterministicRandom() + createDeterministicRandom(), + getJwkSource(resolver), + Set.of(JWSAlgorithm.RS256) ); Set signingKeyIds = new HashSet<>(); @@ -196,13 +208,10 @@ void uses_allowed_signing_methods_only() throws ParseException, JOSEException { .build(); var signer = new PropertiesBasedJwtClaimsSigner( - MockJwtClaimsSignerProperties.builder() - .activeKeys("file:/keys/active-*.pem") - .algorithms(Set.of(JWSAlgorithm.ES256)) - .build(), - resolver, new DefaultJWSSignerFactory(), - createDeterministicRandom() + createDeterministicRandom(), + getJwkSource(resolver), + Set.of(JWSAlgorithm.ES256) ); // try 20 times to sign JWTs, so we can collect a sample of the signing keys @@ -211,7 +220,7 @@ void uses_allowed_signing_methods_only() throws ParseException, JOSEException { // The keys selected are actually the same order as in the rotates_multiple_active_keys test, // so if selecting only allowed algorithms was not working, we would expect signatures with both keysn // given that the rotates_multiple_active_keys test is passing - for(int i = 0; i < 20; i++) { + for (int i = 0; i < 20; i++) { var signedJwt = signer.sign(JWTClaimsSet.parse(Map.of("test", "test"))); assertThat(signedJwt.getHeader().getAlgorithm()).isEqualTo(JWSAlgorithm.ES256); var verifier = new ECDSAVerifier((ECPublicKey) activeKey2.getPublic()); @@ -223,6 +232,7 @@ void uses_allowed_signing_methods_only() throws ParseException, JOSEException { @RequiredArgsConstructor private static class DelegateResourcePatternResolver implements ResourcePatternResolver { + @Delegate @Setter @NonNull @@ -232,8 +242,9 @@ private static class DelegateResourcePatternResolver implements ResourcePatternR @Value @Builder static class MockJwtClaimsSignerProperties implements JwtClaimsSignerProperties { + String activeKeys; - String allKeys; + String retiredKeys; @Default Set algorithms = Family.SIGNATURE; } From daa73dcb1b3ccdf01f8d18dd6c7ce6b79e5db6cd Mon Sep 17 00:00:00 2001 From: Thijs Lemmens Date: Wed, 17 Jul 2024 14:13:38 +0200 Subject: [PATCH 2/9] ACC-1493 cache the JWSSigners --- .../PropertiesBasedJwtClaimsSigner.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSigner.java b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSigner.java index 167e40ff..0eeb52f2 100644 --- a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSigner.java +++ b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSigner.java @@ -4,6 +4,7 @@ import com.nimbusds.jose.JOSEObjectType; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory; import com.nimbusds.jose.jwk.ECKey; import com.nimbusds.jose.jwk.JWK; @@ -38,6 +39,7 @@ import lombok.SneakyThrows; import org.springframework.core.io.Resource; import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.util.ConcurrentLruCache; @RequiredArgsConstructor public class PropertiesBasedJwtClaimsSigner implements JwtClaimsSigner { @@ -85,7 +87,7 @@ public SignedJWT sign(JWTClaimsSet jwtClaimsSet) throws JOSEException { continue; } - var selectedSigner = jwsSignerFactory.createJWSSigner(selectedKey); + var selectedSigner = getJwsSigner(selectedKey); algorithmsSupportedByKeys.addAll(selectedSigner.supportedJWSAlgorithms()); var firstSupportedAlgorithm = algorithms .stream() @@ -109,4 +111,20 @@ public SignedJWT sign(JWTClaimsSet jwtClaimsSet) throws JOSEException { algorithmsSupportedByKeys )); } + + private ConcurrentLruCache signerCache; + + private JWSSigner getJwsSigner(JWK jwk) throws JOSEException { + if (signerCache == null) { + signerCache = new ConcurrentLruCache<>(100, + key -> { + try { + return jwsSignerFactory.createJWSSigner(key); + } catch (JOSEException e) { + throw new RuntimeException(e); + } + }); + } + return signerCache.get(jwk); + } } From 8ce33f138f835caf5436736f598539fef2c59ffa Mon Sep 17 00:00:00 2001 From: Thijs Lemmens Date: Fri, 26 Jul 2024 14:03:31 +0200 Subject: [PATCH 3/9] ACC-1512 Cleanup Exception handling, handling sonar remarks --- .../security/jwt/issuer/JwtClaimsSigner.java | 3 +- .../PropertiesBasedJwtClaimsSigner.java | 33 ++++++++----------- .../security/jwt/issuer/SignedJwtIssuer.java | 8 +---- .../jwk/source/FilebasedJWKSetSource.java | 2 +- .../jwt/SingleKeyJwtClaimsSigner.java | 3 +- 5 files changed, 18 insertions(+), 31 deletions(-) diff --git a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/JwtClaimsSigner.java b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/JwtClaimsSigner.java index a782c8c0..84c32233 100644 --- a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/JwtClaimsSigner.java +++ b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/JwtClaimsSigner.java @@ -1,11 +1,10 @@ package com.contentgrid.gateway.security.jwt.issuer; -import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; public interface JwtClaimsSigner { JWKSet getSigningKeys(); - SignedJWT sign(JWTClaimsSet jwtClaimsSet) throws JOSEException; + SignedJWT sign(JWTClaimsSet jwtClaimsSet); } diff --git a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSigner.java b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSigner.java index 0eeb52f2..ea41a8c2 100644 --- a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSigner.java +++ b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSigner.java @@ -6,41 +6,29 @@ import com.nimbusds.jose.JWSHeader; import com.nimbusds.jose.JWSSigner; import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory; -import com.nimbusds.jose.jwk.ECKey; import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.JWKMatcher; import com.nimbusds.jose.jwk.JWKSelector; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.KeyUse; -import com.nimbusds.jose.jwk.OctetKeyPair; -import com.nimbusds.jose.jwk.RSAKey; -import com.nimbusds.jose.jwk.source.CachingJWKSetSource; -import com.nimbusds.jose.jwk.source.JWKSetSource; import com.nimbusds.jose.jwk.source.JWKSource; -import com.nimbusds.jose.jwk.source.JWKSourceBuilder; -import com.nimbusds.jose.jwk.source.URLBasedJWKSetSource; import com.nimbusds.jose.proc.SecurityContext; import com.nimbusds.jose.proc.SimpleSecurityContext; import com.nimbusds.jose.produce.JWSSignerFactory; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; -import java.nio.charset.StandardCharsets; -import java.time.Instant; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Random; import java.util.Set; -import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; -import org.springframework.core.io.Resource; -import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.util.ConcurrentLruCache; + @RequiredArgsConstructor public class PropertiesBasedJwtClaimsSigner implements JwtClaimsSigner { @@ -55,8 +43,11 @@ public PropertiesBasedJwtClaimsSigner(JWKSource jwkSource, Set< } public interface JwtClaimsSignerProperties { + String getActiveKeys(); + String getRetiredKeys(); + Set getAlgorithms(); } @@ -74,7 +65,8 @@ public JWKSet getSigningKeys() { } @Override - public SignedJWT sign(JWTClaimsSet jwtClaimsSet) throws JOSEException { + @SneakyThrows + public SignedJWT sign(JWTClaimsSet jwtClaimsSet) { var jwks = new ArrayList<>(getAllSigningKeys()); Collections.shuffle(jwks, this.random); // Randomly shuffle our active keys, so we pick an arbitrary one first @@ -93,7 +85,7 @@ public SignedJWT sign(JWTClaimsSet jwtClaimsSet) throws JOSEException { .stream() .filter(selectedSigner.supportedJWSAlgorithms()::contains) .findFirst(); - if(firstSupportedAlgorithm.isEmpty()) { + if (firstSupportedAlgorithm.isEmpty()) { // Signer does not support any of the signing algorithms; continue to a next key continue; } @@ -106,15 +98,16 @@ public SignedJWT sign(JWTClaimsSet jwtClaimsSet) throws JOSEException { signedJwt.sign(selectedSigner); return signedJwt; } - throw new IllegalStateException("No active signing keys support any of the configured algorithms (%s); algorithms that can be used by these keys are %s".formatted( - algorithms, - algorithmsSupportedByKeys - )); + throw new IllegalStateException( + "No active signing keys support any of the configured algorithms (%s); algorithms that can be used by these keys are %s".formatted( + algorithms, + algorithmsSupportedByKeys + )); } private ConcurrentLruCache signerCache; - private JWSSigner getJwsSigner(JWK jwk) throws JOSEException { + private JWSSigner getJwsSigner(JWK jwk) { if (signerCache == null) { signerCache = new ConcurrentLruCache<>(100, key -> { diff --git a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/SignedJwtIssuer.java b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/SignedJwtIssuer.java index 24d3ee13..1b3cc3b5 100644 --- a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/SignedJwtIssuer.java +++ b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/SignedJwtIssuer.java @@ -47,13 +47,7 @@ public Mono issueSubstitutionToken(ServerWebExchange exchange) { } return Mono.empty(); }) - .flatMap(claims -> { - try { - return Mono.just(claimsSigner.sign(claims)); - } catch (JOSEException e) { - return Mono.error(e); - } - }) + .flatMap(claims -> Mono.just(claimsSigner.sign(claims))) .flatMap(signedJwt -> { try { var signedJwtClaims = signedJwt.getJWTClaimsSet().getClaims(); diff --git a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSource.java b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSource.java index 29da0bcc..1c5f3af6 100644 --- a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSource.java +++ b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSource.java @@ -103,6 +103,6 @@ private static JWK createFromSigningKey(Resource resource, Date expirationTime) @Override public void close() throws IOException { - + // Nothing to close } } diff --git a/src/testFixtures/java/com/contentgrid/gateway/test/security/jwt/SingleKeyJwtClaimsSigner.java b/src/testFixtures/java/com/contentgrid/gateway/test/security/jwt/SingleKeyJwtClaimsSigner.java index 69953c84..7b488b8e 100644 --- a/src/testFixtures/java/com/contentgrid/gateway/test/security/jwt/SingleKeyJwtClaimsSigner.java +++ b/src/testFixtures/java/com/contentgrid/gateway/test/security/jwt/SingleKeyJwtClaimsSigner.java @@ -46,7 +46,8 @@ public JWKSet getSigningKeys() { } @Override - public SignedJWT sign(JWTClaimsSet jwtClaimsSet) throws JOSEException { + @SneakyThrows + public SignedJWT sign(JWTClaimsSet jwtClaimsSet) { var jwt = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(key.getKeyID()).build(), jwtClaimsSet); jwt.sign(new DefaultJWSSignerFactory().createJWSSigner(key)); return jwt; From d6e78d40779c0557363b00f36bfbe2e32f18b38c Mon Sep 17 00:00:00 2001 From: Thijs Lemmens Date: Fri, 26 Jul 2024 14:17:40 +0200 Subject: [PATCH 4/9] ACC-1512 Improve test coverage --- .../jwk/source/FilebasedJWKSetSourceTest.java | 62 ++++++++++++++++ .../source/MockResourcePatternResolver.java | 73 +++++++++++++++++++ .../test/security/CryptoTestUtils.java | 23 ++++++ 3 files changed, 158 insertions(+) create mode 100644 src/test/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSourceTest.java create mode 100644 src/test/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/MockResourcePatternResolver.java diff --git a/src/test/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSourceTest.java b/src/test/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSourceTest.java new file mode 100644 index 00000000..6058b604 --- /dev/null +++ b/src/test/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSourceTest.java @@ -0,0 +1,62 @@ +package com.contentgrid.gateway.security.jwt.issuer.jwk.source; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.contentgrid.gateway.test.security.CryptoTestUtils; +import com.nimbusds.jose.jwk.JWK; +import java.nio.charset.StandardCharsets; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; + +class FilebasedJWKSetSourceTest { + + @Test + @SneakyThrows + void testGetJWKSet() { + var activeRSAKey = CryptoTestUtils.createKeyPair("RSA", 2048); + var activeRSAResource = CryptoTestUtils.toPrivateKeyResource(activeRSAKey); + var retiredRSAKey = CryptoTestUtils.createKeyPair("RSA", 2048); + var retiredRSAResource = CryptoTestUtils.toPrivateKeyResource(retiredRSAKey); + + var activeECKey = CryptoTestUtils.createKeyPair("EC", 256); + var activeECResource = CryptoTestUtils.toPrivateKeyResource(activeECKey); + var retiredECKey = CryptoTestUtils.createKeyPair("EC", 256); + var retiredECResource = CryptoTestUtils.toPrivateKeyResource(retiredECKey); + + var activeOctetKeyPair = CryptoTestUtils.createOctetKeyPair(); + var activeOctetResource = CryptoTestUtils.toPrivateKeyResource(activeOctetKeyPair); + var retiredOctetKeyPair = CryptoTestUtils.createOctetKeyPair(); + var retiredOctetResource = CryptoTestUtils.toPrivateKeyResource(retiredOctetKeyPair); + + var resourcePatternResolver = MockResourcePatternResolver.builder() + .resource("file:/keys/active_rsa.pem", activeRSAResource) + .resource("file:/keys/retired_rsa.pem", retiredRSAResource) + .resource("file:/keys/active_ec.pem", activeECResource) + .resource("file:/keys/retired_ec.pem", retiredECResource) + .resource("file:/keys/active_octet.pem", activeOctetResource) + .resource("file:/keys/retired_octet.pem", retiredOctetResource) + .build(); + + var filebasedJWKSetSource = new FilebasedJWKSetSource(resourcePatternResolver, "file:/keys/active_*.pem", + "file:/keys/retired_*.pem"); + + var jwkSet = filebasedJWKSetSource.getJWKSet(null, System.currentTimeMillis(), null); + + var activeRSAJWK = JWK.parseFromPEMEncodedObjects(activeRSAResource.getContentAsString(StandardCharsets.UTF_8)); + var retiredRSAJWK = JWK.parseFromPEMEncodedObjects(retiredRSAResource.getContentAsString(StandardCharsets.UTF_8)); + var activeECJWK = JWK.parseFromPEMEncodedObjects(activeECResource.getContentAsString(StandardCharsets.UTF_8)); + var retiredECJWK = JWK.parseFromPEMEncodedObjects(retiredECResource.getContentAsString(StandardCharsets.UTF_8)); + var activeOctetJWK = JWK.parseFromPEMEncodedObjects(activeOctetResource.getContentAsString(StandardCharsets.UTF_8)); + var retiredOctetJWK = JWK.parseFromPEMEncodedObjects(retiredOctetResource.getContentAsString(StandardCharsets.UTF_8)); + + assertEquals(6, jwkSet.getKeys().size()); + assertNotNull(jwkSet.getKeyByKeyId(activeRSAJWK.computeThumbprint().toString())); + assertNotNull(jwkSet.getKeyByKeyId(retiredRSAJWK.computeThumbprint().toString())); + assertNotNull(jwkSet.getKeyByKeyId(activeECJWK.computeThumbprint().toString())); + assertNotNull(jwkSet.getKeyByKeyId(retiredECJWK.computeThumbprint().toString())); + assertNotNull(jwkSet.getKeyByKeyId(activeOctetJWK.computeThumbprint().toString())); + assertNotNull(jwkSet.getKeyByKeyId(retiredOctetJWK.computeThumbprint().toString())); + } + +} \ No newline at end of file diff --git a/src/test/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/MockResourcePatternResolver.java b/src/test/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/MockResourcePatternResolver.java new file mode 100644 index 00000000..8b2d8aac --- /dev/null +++ b/src/test/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/MockResourcePatternResolver.java @@ -0,0 +1,73 @@ +package com.contentgrid.gateway.security.jwt.issuer.jwk.source; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import lombok.Builder; +import lombok.RequiredArgsConstructor; +import lombok.Singular; +import org.springframework.core.io.AbstractResource; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; + +@RequiredArgsConstructor +@Builder +public class MockResourcePatternResolver implements ResourcePatternResolver { + + @Singular + private final Map resources; + + private final PathMatcher pathMatcher = new AntPathMatcher(); + + + @Override + public Resource getResource(String location) { + return resources.getOrDefault(location, new NonExistingResource(location)); + } + + @Override + public ClassLoader getClassLoader() { + return null; + } + + @Override + public Resource[] getResources(String locationPattern) throws IOException { + return resources.keySet() + .stream() + .filter(path -> pathMatcher.match(locationPattern, path)) + .map(this::getResource) + .toArray(Resource[]::new); + } + + @RequiredArgsConstructor + private static class NonExistingResource extends AbstractResource { + + private final String path; + + @Override + public String getDescription() { + return "NonExistingResource [%s]".formatted(path); + } + + @Override + public InputStream getInputStream() throws IOException { + throw new FileNotFoundException(getDescription() + " can not be opened because it does not exist"); + } + + @Override + public boolean exists() { + return false; + } + } + + public static class MockResourcePatternResolverBuilder { + public MockResourcePatternResolverBuilder textResource(String resourceKey, String resource) { + return resource(resourceKey, new ByteArrayResource(resource.getBytes(StandardCharsets.UTF_8))); + } + } +} diff --git a/src/testFixtures/java/com/contentgrid/gateway/test/security/CryptoTestUtils.java b/src/testFixtures/java/com/contentgrid/gateway/test/security/CryptoTestUtils.java index 23acda3f..b8cd0b78 100644 --- a/src/testFixtures/java/com/contentgrid/gateway/test/security/CryptoTestUtils.java +++ b/src/testFixtures/java/com/contentgrid/gateway/test/security/CryptoTestUtils.java @@ -1,7 +1,11 @@ package com.contentgrid.gateway.test.security; +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jose.jwk.OctetKeyPair; +import com.nimbusds.jose.jwk.gen.OctetKeyPairGenerator; import java.io.ByteArrayOutputStream; import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.util.List; @@ -49,4 +53,23 @@ public static Resource toPublicKeyResource(KeyPair keyPair) { new PemObject("PUBLIC KEY", keyPair.getPublic().getEncoded()) )); } + + @SneakyThrows + public static OctetKeyPair createOctetKeyPair() { + return new OctetKeyPairGenerator(Curve.Ed25519).generate(); + } + + @SneakyThrows + public static Resource toPrivateKeyResource(OctetKeyPair keyPair) { + var privateKeyOutput = new ByteArrayOutputStream(); + try (var writer = new OutputStreamWriter(privateKeyOutput)) { + try (var pemWriter = new PemWriter(writer)) { + pemWriter.writeObject(new PemObject("PRIVATE KEY", keyPair.toPrivateKey().toString().getBytes( + StandardCharsets.UTF_8))); + pemWriter.writeObject( + new PemObject("PUBLIC KEY", keyPair.toPublicKey().toString().getBytes(StandardCharsets.UTF_8))); + } + } + return new InMemoryResource(privateKeyOutput.toByteArray()); + } } From 40cdea9416096a7e4b1399685599202d07581a55 Mon Sep 17 00:00:00 2001 From: Thijs Lemmens Date: Fri, 26 Jul 2024 15:06:44 +0200 Subject: [PATCH 5/9] ACC-1512 Kick out OctetKeyPair implementation as it was not working --- .../jwk/source/FilebasedJWKSetSource.java | 6 ------ .../jwk/source/FilebasedJWKSetSourceTest.java | 13 +------------ .../test/security/CryptoTestUtils.java | 19 ------------------- 3 files changed, 1 insertion(+), 37 deletions(-) diff --git a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSource.java b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSource.java index 1c5f3af6..7def1bad 100644 --- a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSource.java +++ b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSource.java @@ -83,12 +83,6 @@ private static JWK createFromSigningKey(Resource resource, Date expirationTime) .keyUse(KeyUse.SIGNATURE) .expirationTime(expirationTime) .build(); - } else if(decodedKey instanceof OctetKeyPair octetKeyPair) { - jwk = new OctetKeyPair.Builder(octetKeyPair) - .keyIDFromThumbprint() - .keyUse(KeyUse.SIGNATURE) - .expirationTime(expirationTime) - .build(); } else { throw new IllegalArgumentException("Unsupported JWK %s: Unsupported key type %s".formatted(resource, decodedKey.getKeyType())); } diff --git a/src/test/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSourceTest.java b/src/test/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSourceTest.java index 6058b604..59396111 100644 --- a/src/test/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSourceTest.java +++ b/src/test/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSourceTest.java @@ -24,18 +24,11 @@ void testGetJWKSet() { var retiredECKey = CryptoTestUtils.createKeyPair("EC", 256); var retiredECResource = CryptoTestUtils.toPrivateKeyResource(retiredECKey); - var activeOctetKeyPair = CryptoTestUtils.createOctetKeyPair(); - var activeOctetResource = CryptoTestUtils.toPrivateKeyResource(activeOctetKeyPair); - var retiredOctetKeyPair = CryptoTestUtils.createOctetKeyPair(); - var retiredOctetResource = CryptoTestUtils.toPrivateKeyResource(retiredOctetKeyPair); - var resourcePatternResolver = MockResourcePatternResolver.builder() .resource("file:/keys/active_rsa.pem", activeRSAResource) .resource("file:/keys/retired_rsa.pem", retiredRSAResource) .resource("file:/keys/active_ec.pem", activeECResource) .resource("file:/keys/retired_ec.pem", retiredECResource) - .resource("file:/keys/active_octet.pem", activeOctetResource) - .resource("file:/keys/retired_octet.pem", retiredOctetResource) .build(); var filebasedJWKSetSource = new FilebasedJWKSetSource(resourcePatternResolver, "file:/keys/active_*.pem", @@ -47,16 +40,12 @@ void testGetJWKSet() { var retiredRSAJWK = JWK.parseFromPEMEncodedObjects(retiredRSAResource.getContentAsString(StandardCharsets.UTF_8)); var activeECJWK = JWK.parseFromPEMEncodedObjects(activeECResource.getContentAsString(StandardCharsets.UTF_8)); var retiredECJWK = JWK.parseFromPEMEncodedObjects(retiredECResource.getContentAsString(StandardCharsets.UTF_8)); - var activeOctetJWK = JWK.parseFromPEMEncodedObjects(activeOctetResource.getContentAsString(StandardCharsets.UTF_8)); - var retiredOctetJWK = JWK.parseFromPEMEncodedObjects(retiredOctetResource.getContentAsString(StandardCharsets.UTF_8)); - assertEquals(6, jwkSet.getKeys().size()); + assertEquals(4, jwkSet.getKeys().size()); assertNotNull(jwkSet.getKeyByKeyId(activeRSAJWK.computeThumbprint().toString())); assertNotNull(jwkSet.getKeyByKeyId(retiredRSAJWK.computeThumbprint().toString())); assertNotNull(jwkSet.getKeyByKeyId(activeECJWK.computeThumbprint().toString())); assertNotNull(jwkSet.getKeyByKeyId(retiredECJWK.computeThumbprint().toString())); - assertNotNull(jwkSet.getKeyByKeyId(activeOctetJWK.computeThumbprint().toString())); - assertNotNull(jwkSet.getKeyByKeyId(retiredOctetJWK.computeThumbprint().toString())); } } \ No newline at end of file diff --git a/src/testFixtures/java/com/contentgrid/gateway/test/security/CryptoTestUtils.java b/src/testFixtures/java/com/contentgrid/gateway/test/security/CryptoTestUtils.java index b8cd0b78..462e489d 100644 --- a/src/testFixtures/java/com/contentgrid/gateway/test/security/CryptoTestUtils.java +++ b/src/testFixtures/java/com/contentgrid/gateway/test/security/CryptoTestUtils.java @@ -53,23 +53,4 @@ public static Resource toPublicKeyResource(KeyPair keyPair) { new PemObject("PUBLIC KEY", keyPair.getPublic().getEncoded()) )); } - - @SneakyThrows - public static OctetKeyPair createOctetKeyPair() { - return new OctetKeyPairGenerator(Curve.Ed25519).generate(); - } - - @SneakyThrows - public static Resource toPrivateKeyResource(OctetKeyPair keyPair) { - var privateKeyOutput = new ByteArrayOutputStream(); - try (var writer = new OutputStreamWriter(privateKeyOutput)) { - try (var pemWriter = new PemWriter(writer)) { - pemWriter.writeObject(new PemObject("PRIVATE KEY", keyPair.toPrivateKey().toString().getBytes( - StandardCharsets.UTF_8))); - pemWriter.writeObject( - new PemObject("PUBLIC KEY", keyPair.toPublicKey().toString().getBytes(StandardCharsets.UTF_8))); - } - } - return new InMemoryResource(privateKeyOutput.toByteArray()); - } } From bed7949355ce56e9ba62ace0637d15c214875dcd Mon Sep 17 00:00:00 2001 From: Thijs Lemmens Date: Fri, 26 Jul 2024 15:28:44 +0200 Subject: [PATCH 6/9] ACC-1512 Assert that it throws on algorithm mismatch --- .../PropertiesBasedJwtClaimsSignerTest.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSignerTest.java b/src/test/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSignerTest.java index b197c519..a7507f23 100644 --- a/src/test/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSignerTest.java +++ b/src/test/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSignerTest.java @@ -4,6 +4,7 @@ import static com.contentgrid.gateway.test.security.CryptoTestUtils.toPrivateKeyResource; import static com.contentgrid.gateway.test.security.CryptoTestUtils.toPublicKeyResource; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; import com.contentgrid.gateway.security.jwt.issuer.PropertiesBasedJwtClaimsSigner.JwtClaimsSignerProperties; import com.contentgrid.gateway.security.jwt.issuer.jwk.source.FilebasedJWKSetSource; @@ -19,7 +20,6 @@ import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.source.JWKSetBasedJWKSource; import com.nimbusds.jose.jwk.source.JWKSource; -import com.nimbusds.jose.jwk.source.JWKSourceBuilder; import com.nimbusds.jwt.JWTClaimsSet; import java.nio.charset.StandardCharsets; import java.security.interfaces.ECPublicKey; @@ -230,6 +230,24 @@ void uses_allowed_signing_methods_only() throws ParseException, JOSEException { } + @Test + void throws_on_algorith_mismatch() throws ParseException { + var activeKey1 = createKeyPair("RSA", 4096); + + var resolver = MockResourcePatternResolver.builder() + .resource("file:/keys/active-1.pem", toPrivateKeyResource(activeKey1)) + .build(); + + var signer = new PropertiesBasedJwtClaimsSigner( + new DefaultJWSSignerFactory(), + createDeterministicRandom(), + getJwkSource(resolver), + Set.of(JWSAlgorithm.ES256) + ); + + assertThrows(IllegalStateException.class, () -> signer.sign(JWTClaimsSet.parse(Map.of("test", "test")))); + } + @RequiredArgsConstructor private static class DelegateResourcePatternResolver implements ResourcePatternResolver { From 20af0a4468c4bbe0302b2911aa322a87e0aafeab Mon Sep 17 00:00:00 2001 From: Thijs Lemmens Date: Fri, 26 Jul 2024 15:42:46 +0200 Subject: [PATCH 7/9] ACC-1512 Sonar remarks --- .../security/jwt/issuer/jwk/source/FilebasedJWKSetSource.java | 1 - .../security/jwt/issuer/PropertiesBasedJwtClaimsSignerTest.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSource.java b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSource.java index 7def1bad..fb44f5af 100644 --- a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSource.java +++ b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSource.java @@ -6,7 +6,6 @@ import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.KeyUse; -import com.nimbusds.jose.jwk.OctetKeyPair; import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.jwk.source.JWKSetCacheRefreshEvaluator; import com.nimbusds.jose.jwk.source.JWKSetParseException; diff --git a/src/test/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSignerTest.java b/src/test/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSignerTest.java index a7507f23..08de11a4 100644 --- a/src/test/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSignerTest.java +++ b/src/test/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSignerTest.java @@ -231,7 +231,7 @@ void uses_allowed_signing_methods_only() throws ParseException, JOSEException { } @Test - void throws_on_algorith_mismatch() throws ParseException { + void throws_on_algorith_mismatch() { var activeKey1 = createKeyPair("RSA", 4096); var resolver = MockResourcePatternResolver.builder() From 8c57bcd6ce9931530d908dc5fa8e7a328141769e Mon Sep 17 00:00:00 2001 From: Thijs Lemmens Date: Mon, 29 Jul 2024 09:00:30 +0200 Subject: [PATCH 8/9] ACC-1512 Address PR remarks --- ...ner.java => JwkSourceJwtClaimsSigner.java} | 42 ++++++++----------- .../jwt/issuer/JwtClaimsSignerProperties.java | 13 ++++++ .../JwtInternalIssuerConfiguration.java | 8 +--- .../PropertiesBasedJwtSignerRegistry.java | 2 +- .../security/jwt/issuer/SignedJwtIssuer.java | 2 +- .../jwk/source/FilebasedJWKSetSource.java | 2 +- .../PropertiesBasedJwtClaimsSignerTest.java | 13 +++--- 7 files changed, 41 insertions(+), 41 deletions(-) rename src/main/java/com/contentgrid/gateway/security/jwt/issuer/{PropertiesBasedJwtClaimsSigner.java => JwkSourceJwtClaimsSigner.java} (79%) create mode 100644 src/main/java/com/contentgrid/gateway/security/jwt/issuer/JwtClaimsSignerProperties.java diff --git a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSigner.java b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/JwkSourceJwtClaimsSigner.java similarity index 79% rename from src/main/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSigner.java rename to src/main/java/com/contentgrid/gateway/security/jwt/issuer/JwkSourceJwtClaimsSigner.java index ea41a8c2..df36af41 100644 --- a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSigner.java +++ b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/JwkSourceJwtClaimsSigner.java @@ -24,31 +24,37 @@ import java.util.List; import java.util.Random; import java.util.Set; -import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.springframework.util.ConcurrentLruCache; -@RequiredArgsConstructor -public class PropertiesBasedJwtClaimsSigner implements JwtClaimsSigner { +public class JwkSourceJwtClaimsSigner implements JwtClaimsSigner { - private final JWSSignerFactory jwsSignerFactory; private final Random random; private final JWKSource jwkSource; private final Set algorithms; - public PropertiesBasedJwtClaimsSigner(JWKSource jwkSource, Set algorithms) { + public JwkSourceJwtClaimsSigner(JWKSource jwkSource, Set algorithms) { this(new DefaultJWSSignerFactory(), new Random(), jwkSource, algorithms); } - public interface JwtClaimsSignerProperties { - - String getActiveKeys(); - - String getRetiredKeys(); + private ConcurrentLruCache signerCache; - Set getAlgorithms(); + public JwkSourceJwtClaimsSigner(JWSSignerFactory jwsSignerFactory, Random random, + JWKSource jwkSource, Set algorithms) { + this.random = random; + this.jwkSource = jwkSource; + this.algorithms = algorithms; + + signerCache = new ConcurrentLruCache<>(10, + key -> { + try { + return jwsSignerFactory.createJWSSigner(key); + } catch (JOSEException e) { + throw new RuntimeException(e); + } + }); } @SneakyThrows @@ -69,7 +75,7 @@ public JWKSet getSigningKeys() { public SignedJWT sign(JWTClaimsSet jwtClaimsSet) { var jwks = new ArrayList<>(getAllSigningKeys()); - Collections.shuffle(jwks, this.random); // Randomly shuffle our active keys, so we pick an arbitrary one first + Collections.shuffle(jwks, this.random); // Randomly shuffle our keys, so we pick an arbitrary one first Set algorithmsSupportedByKeys = new HashSet<>(); @@ -105,19 +111,7 @@ public SignedJWT sign(JWTClaimsSet jwtClaimsSet) { )); } - private ConcurrentLruCache signerCache; - private JWSSigner getJwsSigner(JWK jwk) { - if (signerCache == null) { - signerCache = new ConcurrentLruCache<>(100, - key -> { - try { - return jwsSignerFactory.createJWSSigner(key); - } catch (JOSEException e) { - throw new RuntimeException(e); - } - }); - } return signerCache.get(jwk); } } diff --git a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/JwtClaimsSignerProperties.java b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/JwtClaimsSignerProperties.java new file mode 100644 index 00000000..e82b2c32 --- /dev/null +++ b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/JwtClaimsSignerProperties.java @@ -0,0 +1,13 @@ +package com.contentgrid.gateway.security.jwt.issuer; + +import com.nimbusds.jose.JWSAlgorithm; +import java.util.Set; + +public interface JwtClaimsSignerProperties { + + String getActiveKeys(); + + String getRetiredKeys(); + + Set getAlgorithms(); +} diff --git a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/JwtInternalIssuerConfiguration.java b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/JwtInternalIssuerConfiguration.java index 6193f4db..a8f57ae7 100644 --- a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/JwtInternalIssuerConfiguration.java +++ b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/JwtInternalIssuerConfiguration.java @@ -2,12 +2,7 @@ import com.contentgrid.gateway.security.jwt.issuer.JwtInternalIssuerConfiguration.ContentgridGatewayJwtProperties; import com.contentgrid.gateway.security.jwt.issuer.actuate.JWKSetEndpoint; -import com.contentgrid.gateway.security.jwt.issuer.jwk.source.FilebasedJWKSetSource; -import com.contentgrid.gateway.security.jwt.issuer.jwk.source.LoggingJWKSetSourceEventListener; import com.nimbusds.jose.JWSAlgorithm; -import com.nimbusds.jose.jwk.source.JWKSource; -import com.nimbusds.jose.jwk.source.JWKSourceBuilder; -import com.nimbusds.jose.proc.SecurityContext; import io.micrometer.observation.ObservationRegistry; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; @@ -24,7 +19,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.gateway.config.conditional.ConditionalOnEnabledFilter; -import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.support.ResourcePatternResolver; @@ -78,7 +72,7 @@ static class ContentgridGatewayJwtProperties { @Data @Builder @AllArgsConstructor - static class JwtSignerProperties implements PropertiesBasedJwtClaimsSigner.JwtClaimsSignerProperties { + static class JwtSignerProperties implements JwtClaimsSignerProperties { @NotNull private String activeKeys; private String retiredKeys; diff --git a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtSignerRegistry.java b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtSignerRegistry.java index 62d599de..72289950 100644 --- a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtSignerRegistry.java +++ b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtSignerRegistry.java @@ -80,7 +80,7 @@ private JwtClaimsSigner createSigner(String signerName) { getJwkSourceMap().keySet())); } - return new PropertiesBasedJwtClaimsSigner(getJwkSourceMap().get(signerName), gatewayJwtProperties.getSigners().get(signerName).getAlgorithms()); + return new JwkSourceJwtClaimsSigner(getJwkSourceMap().get(signerName), gatewayJwtProperties.getSigners().get(signerName).getAlgorithms()); } } diff --git a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/SignedJwtIssuer.java b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/SignedJwtIssuer.java index 1b3cc3b5..92cbcf02 100644 --- a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/SignedJwtIssuer.java +++ b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/SignedJwtIssuer.java @@ -47,7 +47,7 @@ public Mono issueSubstitutionToken(ServerWebExchange exchange) { } return Mono.empty(); }) - .flatMap(claims -> Mono.just(claimsSigner.sign(claims))) + .map(claimsSigner::sign) .flatMap(signedJwt -> { try { var signedJwtClaims = signedJwt.getJWTClaimsSet().getClaims(); diff --git a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSource.java b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSource.java index fb44f5af..46bc8bb2 100644 --- a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSource.java +++ b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/jwk/source/FilebasedJWKSetSource.java @@ -24,7 +24,7 @@ import org.springframework.core.io.support.ResourcePatternResolver; /** - * Loads JWKs from PEM-encoded private key files. + * Loads JWKs from PEM-encoded or JWK-encoded private key files. * * There is a distinction made between active and retired signing keys */ diff --git a/src/test/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSignerTest.java b/src/test/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSignerTest.java index 08de11a4..8ccaab47 100644 --- a/src/test/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSignerTest.java +++ b/src/test/java/com/contentgrid/gateway/security/jwt/issuer/PropertiesBasedJwtClaimsSignerTest.java @@ -6,7 +6,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import com.contentgrid.gateway.security.jwt.issuer.PropertiesBasedJwtClaimsSigner.JwtClaimsSignerProperties; import com.contentgrid.gateway.security.jwt.issuer.jwk.source.FilebasedJWKSetSource; import com.contentgrid.gateway.test.util.MockResourcePatternResolver; import com.nimbusds.jose.JOSEException; @@ -60,7 +59,7 @@ void signs_with_active_rsa_key() throws ParseException, JOSEException { .resource("file:/keys/active.pem", toPrivateKeyResource(activeKey)) .build(); - var signer = new PropertiesBasedJwtClaimsSigner( + var signer = new JwkSourceJwtClaimsSigner( getJwkSource(resolver), Set.of(JWSAlgorithm.RS256) ); @@ -83,7 +82,7 @@ void provides_all_signing_keys() throws ParseException, JOSEException { .resource("file:/keys/retired-2.pem", toPublicKeyResource(retiredKey2)) .build(); - var signer = new PropertiesBasedJwtClaimsSigner( + var signer = new JwkSourceJwtClaimsSigner( getJwkSource(resolver), Set.of(JWSAlgorithm.RS256) ); @@ -123,7 +122,7 @@ void uses_new_keys_when_rotated() throws ParseException, JOSEException { var resolver = new DelegateResourcePatternResolver(oldResolver); - var signer = new PropertiesBasedJwtClaimsSigner( + var signer = new JwkSourceJwtClaimsSigner( getJwkSource(resolver), Set.of(JWSAlgorithm.RS256) ); @@ -173,7 +172,7 @@ void rotates_multiple_active_keys() throws ParseException, JOSEException { .resource("file:/keys/active-2.pem", toPrivateKeyResource(activeKey2)) .build(); - var signer = new PropertiesBasedJwtClaimsSigner( + var signer = new JwkSourceJwtClaimsSigner( new DefaultJWSSignerFactory(), createDeterministicRandom(), getJwkSource(resolver), @@ -207,7 +206,7 @@ void uses_allowed_signing_methods_only() throws ParseException, JOSEException { .resource("file:/keys/active-2.pem", toPrivateKeyResource(activeKey2)) .build(); - var signer = new PropertiesBasedJwtClaimsSigner( + var signer = new JwkSourceJwtClaimsSigner( new DefaultJWSSignerFactory(), createDeterministicRandom(), getJwkSource(resolver), @@ -238,7 +237,7 @@ void throws_on_algorith_mismatch() { .resource("file:/keys/active-1.pem", toPrivateKeyResource(activeKey1)) .build(); - var signer = new PropertiesBasedJwtClaimsSigner( + var signer = new JwkSourceJwtClaimsSigner( new DefaultJWSSignerFactory(), createDeterministicRandom(), getJwkSource(resolver), From 4f98d99f41ba4cdcf79b3eebe64d0aa587d35316 Mon Sep 17 00:00:00 2001 From: Thijs Lemmens Date: Tue, 30 Jul 2024 09:44:06 +0200 Subject: [PATCH 9/9] ACC-1512 remove unused inputs --- .../contentgrid/gateway/test/security/CryptoTestUtils.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/testFixtures/java/com/contentgrid/gateway/test/security/CryptoTestUtils.java b/src/testFixtures/java/com/contentgrid/gateway/test/security/CryptoTestUtils.java index 462e489d..23acda3f 100644 --- a/src/testFixtures/java/com/contentgrid/gateway/test/security/CryptoTestUtils.java +++ b/src/testFixtures/java/com/contentgrid/gateway/test/security/CryptoTestUtils.java @@ -1,11 +1,7 @@ package com.contentgrid.gateway.test.security; -import com.nimbusds.jose.jwk.Curve; -import com.nimbusds.jose.jwk.OctetKeyPair; -import com.nimbusds.jose.jwk.gen.OctetKeyPairGenerator; import java.io.ByteArrayOutputStream; import java.io.OutputStreamWriter; -import java.nio.charset.StandardCharsets; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.util.List;