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()); + } }