diff --git a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerRecorder.java b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerRecorder.java index 1cdf8e6d730df..a0e4787c7405b 100644 --- a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerRecorder.java +++ b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerRecorder.java @@ -76,6 +76,9 @@ private static PolicyEnforcer createPolicyEnforcer(OidcTenantConfig oidcConfig, if (trustAll) { adapterConfig.setDisableTrustManager(true); adapterConfig.setAllowAnyHostname(true); + } else if (oidcConfig.tls.trustStoreFile.isPresent()) { + adapterConfig.setTruststore(oidcConfig.tls.trustStoreFile.get().toString()); + adapterConfig.setTruststorePassword(oidcConfig.tls.trustStorePassword.orElse("password")); } adapterConfig.setConnectionPoolSize(keycloakPolicyEnforcerConfig.connectionPoolSize); diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java index 0a56c1763ea66..f55103982fb04 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java @@ -1,5 +1,6 @@ package io.quarkus.oidc.common.runtime; +import java.nio.file.Path; import java.time.Duration; import java.util.Optional; import java.util.OptionalInt; @@ -282,6 +283,18 @@ public enum Verification { @ConfigItem public Optional verification = Optional.empty(); + /** + * An optional trust store which holds the certificate information of the certificates to trust + */ + @ConfigItem + public Optional trustStoreFile; + + /** + * A parameter to specify the password of the trust store file. + */ + @ConfigItem + public Optional trustStorePassword; + public Optional getVerification() { return verification; } @@ -290,6 +303,22 @@ public void setVerification(Verification verification) { this.verification = Optional.of(verification); } + public Optional getTrustStoreFile() { + return trustStoreFile; + } + + public void setTrustStoreFile(Optional trustStoreFile) { + this.trustStoreFile = trustStoreFile; + } + + public Optional getTrustStorePassword() { + return trustStorePassword; + } + + public void setTrustStorePassword(Optional trustStorePassword) { + this.trustStorePassword = trustStorePassword; + } + } @ConfigGroup diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java index 86e166bd024d4..751563caa6289 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java @@ -1,9 +1,13 @@ package io.quarkus.oidc.common.runtime; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.security.Key; import java.security.KeyStore; import java.security.PrivateKey; @@ -25,6 +29,7 @@ import io.smallrye.jwt.util.ResourceUtils; import io.vertx.core.http.HttpClientOptions; import io.vertx.core.json.JsonObject; +import io.vertx.core.net.KeyStoreOptions; import io.vertx.core.net.ProxyOptions; import io.vertx.mutiny.core.MultiMap; import io.vertx.mutiny.core.buffer.Buffer; @@ -104,6 +109,19 @@ public static void setHttpClientOptions(OidcCommonConfig oidcConfig, TlsConfig t if (trustAll) { options.setTrustAll(true); options.setVerifyHost(false); + } else if (oidcConfig.tls.trustStoreFile.isPresent()) { + try { + byte[] trustStoreData = getFileContent(oidcConfig.tls.trustStoreFile.get()); + io.vertx.core.net.KeyStoreOptions trustStoreOptions = new KeyStoreOptions() + .setPassword(oidcConfig.tls.getTrustStorePassword().orElse("password")) + .setValue(io.vertx.core.buffer.Buffer.buffer(trustStoreData)) + .setType("JKS"); + options.setTrustOptions(trustStoreOptions); + } catch (IOException ex) { + throw new ConfigurationException(String.format( + "OIDC truststore file does not exist or can not be read", + oidcConfig.tls.trustStoreFile.get().toString()), ex); + } } Optional proxyOpt = toProxyOptions(oidcConfig.getProxy()); if (proxyOpt.isPresent()) { @@ -264,4 +282,29 @@ public static Key initClientJwtKey(OidcCommonConfig oidcConfig) { } return null; } + + private static byte[] getFileContent(Path path) throws IOException { + byte[] data; + final InputStream resource = Thread.currentThread().getContextClassLoader().getResourceAsStream(path.toString()); + if (resource != null) { + try (InputStream is = resource) { + data = doRead(is); + } + } else { + try (InputStream is = Files.newInputStream(path)) { + data = doRead(is); + } + } + return data; + } + + private static byte[] doRead(InputStream is) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[1024]; + int r; + while ((r = is.read(buf)) > 0) { + out.write(buf, 0, r); + } + return out.toByteArray(); + } } diff --git a/integration-tests/oidc/pom.xml b/integration-tests/oidc/pom.xml index c10218ddb9c03..32c18c42a6fa1 100644 --- a/integration-tests/oidc/pom.xml +++ b/integration-tests/oidc/pom.xml @@ -82,12 +82,6 @@ - - - src/main/resources - true - - maven-surefire-plugin diff --git a/integration-tests/oidc/src/main/resources/application.properties b/integration-tests/oidc/src/main/resources/application.properties index c0d1e79accd26..2b0437870511a 100644 --- a/integration-tests/oidc/src/main/resources/application.properties +++ b/integration-tests/oidc/src/main/resources/application.properties @@ -3,7 +3,9 @@ quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus/ quarkus.oidc.client-id=quarkus-service-app quarkus.oidc.credentials.secret=secret quarkus.oidc.token.principal-claim=email -quarkus.oidc.tls.verification=none +quarkus.oidc.tls.verification=required +quarkus.oidc.tls.trust-store-file=keycloak.jks +quarkus.oidc.tls.trust-store-password=secret quarkus.http.cors=true quarkus.http.auth.basic=true @@ -18,4 +20,4 @@ quarkus.http.auth.permission.basic.auth-mechanism=basic quarkus.http.auth.permission.bearer.paths=/bearer-only quarkus.http.auth.permission.bearer.policy=authenticated -quarkus.http.auth.permission.bearer.auth-mechanism=bearer \ No newline at end of file +quarkus.http.auth.permission.bearer.auth-mechanism=bearer diff --git a/integration-tests/oidc/src/main/resources/keycloak.jks b/integration-tests/oidc/src/main/resources/keycloak.jks new file mode 100644 index 0000000000000..f3035286e1f32 Binary files /dev/null and b/integration-tests/oidc/src/main/resources/keycloak.jks differ diff --git a/integration-tests/oidc/src/main/resources/tls.key b/integration-tests/oidc/src/main/resources/tls.key new file mode 100644 index 0000000000000..0cd503ce2aa4d --- /dev/null +++ b/integration-tests/oidc/src/main/resources/tls.key @@ -0,0 +1,32 @@ +Bag Attributes + friendlyName: localhost + localKeyID: 54 69 6D 65 20 31 36 32 34 30 33 32 35 33 30 39 31 33 +Key Attributes: +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCREYdYCTRamvG1 +/vFhAiknv8R95WV3GTMKIrJjHjYDTY8fx7kjX5JIy6y51emSBVW/jouhVCrfa12j +t+V9y66tkbEM8kQlhIldhuf3c4QTgEQHNd/4OGSGFmAYpvx+EfHILTD+5mArbhMA +FB3Cn5GfW8/4AdKotWsFEorZL2eqcbs+7qjO26CF0uV8Zk4F7vwhaenVKiWyUbaQ +8TtRPOXmtmUDaJvcBOTSJdmFxHYUsJOzojmDAMzJqu0M6sEOfoOzx8UGhYYPWLaz +kfpDOJ4Rua71usLjQGEOoeUD6xf6rpB4eKdlLPNPXlTa7JzwvBtPM6E/XXY3ZuCa +Mrx/PbA1AgMBAAECggEBAIOso2rXP/wVs9v8AmCJM43u1I1pkMWfy+IhSEYLf/9T +gNvZz0Q6VW9Z3/f2IEH4MbLj0f2nhhqxO5eFLfsWzACjw076/7wGJyELeLX01idV +P2pEDn0hwqyq1qLJv1k3NHz7+AMGXLhO+1QQ7kpfyDAbiBOWo/2aXf+Gqx0jmDbu +Ed+vRNmNpod5hOVHUjo2W500aFCcmtt2vMym713pVXfqNP6bQPAkO8VFJ7vdD63F +OIx85wcyTlTrCc0bitHaQouG3B56+T4Eg6OoVjMpFrjO4GCcqZAZyiN5QcwMEpZt +VkRCKGJfnIlNeES20I6/qURmhfkptHdJNRaD/v39cKkCgYEAzkDJ1BY+1EgyMHY0 +mRM6CiGzAbVOlW32cACYM3m6qYbM14iw2Gw2pbEmfTeuhUayRQgjOIyWEX7HzAiu +6OzI8lEXoow0ewW4E6duqHtqJy3rl2ZqYLUuVfPlhx+NcEH6cxYfknHGMCX4wUyU +pIf8Yf5qJ9zb38tqLE0bVDOt/18CgYEAtA7cy8z5g0YWmupgDKXB0n2D6XRDYl+z +8/+3PCJ9kJtNbwqREWkn7IvVQlMTCsKPME4wcvreoLedScMDIfLQNH+7F87Q6TC6 +/kOt8gvW/pkSVRXYujFK1O9KORUmuN1YHGD+rdX5T5ufA2DVWkX+Hc+5lXrHjlgY +/Eq2EnuWPOsCgYEAsgH0rxjr7ObKekzqpFqVsvzWs9i5E/qtwIii03pyAbIXxMVy +a7cpiuNTpqqR8vDLFw0o6LtdIYhcA9pSqzEBVTFrxpxfBvYuorfUp5CsU1gshqSb +lw+ICCLRrEctGP+4me80HH4ZYKDFCn9/omjDCAg9sl3JXmL/JXD+7zMTLt0CgYBD +KpQklgaxeHCwQyOnNCH0IgwWBt+oD6kyKL6yeO88BSLCfD+XLhHNhG/9+L1Oszr0 +uwYJrhlj/Hp47Hz7qfcOzmL9Q5Hcmuf2N0ro0o/Vk0YqZSbedcrDWavnVUOHjFH0 +7B20vO/uSU/s069iqF9dwYIqB43vRF+1pSz8AgwOFwKBgFGw8EkhByIzXpNX8Z9s +5nhC32vt2DgttcaSNCo0jqBUns3YgkKd1gLDppk66ZSU8xLP+TP7ge8DPpBEGERd +A/vrq2U515eqiOxu0RHOKp4cn57i+6lLpAqFz8hxkRBAPpeVNL4Yn6BHF/ouvPIW +yN6B3X4uVS/RCx4It50S8jlu +-----END PRIVATE KEY----- diff --git a/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/server/KeycloakTestResourceLifecycleManager.java b/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/server/KeycloakTestResourceLifecycleManager.java index f19c9a87b1933..fcdda70110d87 100644 --- a/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/server/KeycloakTestResourceLifecycleManager.java +++ b/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/server/KeycloakTestResourceLifecycleManager.java @@ -15,17 +15,20 @@ import org.keycloak.representations.idm.RolesRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.util.JsonSerialization; +import org.testcontainers.containers.BindMode; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; public class KeycloakTestResourceLifecycleManager implements QuarkusTestResourceLifecycleManager { private GenericContainer keycloak; private static String KEYCLOAK_SERVER_URL; + private static Boolean KEYCLOAK_TRUSTSTORE_REQUIRED; private static final String KEYCLOAK_REALM = System.getProperty("keycloak.realm", "quarkus"); private static final String KEYCLOAK_SERVICE_CLIENT = System.getProperty("keycloak.service.client", "quarkus-service-app"); private static final String KEYCLOAK_WEB_APP_CLIENT = System.getProperty("keycloak.web-app.client", "quarkus-web-app"); @@ -36,8 +39,16 @@ public class KeycloakTestResourceLifecycleManager implements QuarkusTestResource private static final String TOKEN_USER_ROLES = System.getProperty("keycloak.token.user-roles", "user"); private static final String TOKEN_ADMIN_ROLES = System.getProperty("keycloak.token.admin-roles", "user,admin"); + private static String KEYCLOAK_TRUSTSTORE_PATH = "keycloak.jks"; + private static String KEYCLOAK_TRUSTSTORE_SECRET = "secret"; + private static String KEYCLOAK_TLS_KEY = "tls.key"; + private static String KEYCLOAK_TLS_KEY_MOUNTED_PATH = "/etc/x509/http/tls.key"; + static { - RestAssured.useRelaxedHTTPSValidation(); + KEYCLOAK_TRUSTSTORE_REQUIRED = Thread.currentThread().getContextClassLoader().getResource(KEYCLOAK_TLS_KEY) != null; + if (KEYCLOAK_USE_HTTPS && !KEYCLOAK_TRUSTSTORE_REQUIRED) { + RestAssured.useRelaxedHTTPSValidation(); + } } @SuppressWarnings("resource") @@ -51,6 +62,7 @@ public Map start() { } else { throw new ConfigurationException("Please set either 'keycloak.docker.image' or 'keycloak.version' system property"); } + keycloak = new GenericContainer<>(keycloakDockerImage) .withExposedPorts(8080, 8443) .withEnv("DB_VENDOR", "H2") @@ -58,6 +70,11 @@ public Map start() { .withEnv("KEYCLOAK_PASSWORD", "admin") .waitingFor(Wait.forHttp("/auth").forPort(8080)); + if (KEYCLOAK_USE_HTTPS && KEYCLOAK_TRUSTSTORE_REQUIRED) { + keycloak = keycloak.withClasspathResourceMapping(KEYCLOAK_TLS_KEY, KEYCLOAK_TLS_KEY_MOUNTED_PATH, + BindMode.READ_ONLY); + } + keycloak.start(); if (KEYCLOAK_USE_HTTPS) { @@ -75,11 +92,9 @@ public Map start() { return conf; } - private void postRealm(RealmRepresentation realm) { + private static void postRealm(RealmRepresentation realm) { try { - RestAssured - .given() - .auth().oauth2(getAdminAccessToken()) + createRequestSpec().auth().oauth2(getAdminAccessToken()) .contentType("application/json") .body(JsonSerialization.writeValueAsBytes(realm)) .when() @@ -121,8 +136,7 @@ private static RealmRepresentation createRealm(String name) { } private static String getAdminAccessToken() { - return RestAssured - .given() + return createRequestSpec() .param("grant_type", "password") .param("username", "admin") .param("password", "admin") @@ -178,9 +192,7 @@ private static UserRepresentation createUser(String username, List realm } public static String getAccessToken(String userName) { - return RestAssured - .given() - .param("grant_type", "password") + return createRequestSpec().param("grant_type", "password") .param("username", userName) .param("password", userName) .param("client_id", KEYCLOAK_SERVICE_CLIENT) @@ -191,9 +203,7 @@ public static String getAccessToken(String userName) { } public static String getRefreshToken(String userName) { - return RestAssured - .given() - .param("grant_type", "password") + return createRequestSpec().param("grant_type", "password") .param("username", userName) .param("password", userName) .param("client_id", KEYCLOAK_SERVICE_CLIENT) @@ -205,9 +215,7 @@ public static String getRefreshToken(String userName) { @Override public void stop() { - RestAssured - .given() - .auth().oauth2(getAdminAccessToken()) + createRequestSpec().auth().oauth2(getAdminAccessToken()) .when() .delete(KEYCLOAK_SERVER_URL + "/admin/realms/" + KEYCLOAK_REALM).then().statusCode(204); @@ -221,4 +229,12 @@ private static List getAdminRoles() { private static List getUserRoles() { return Arrays.asList(TOKEN_USER_ROLES.split(",")); } + + private static RequestSpecification createRequestSpec() { + RequestSpecification spec = RestAssured.given(); + if (KEYCLOAK_TRUSTSTORE_REQUIRED) { + spec = spec.trustStore(KEYCLOAK_TRUSTSTORE_PATH, KEYCLOAK_TRUSTSTORE_SECRET); + } + return spec; + } }