Skip to content

Commit

Permalink
TrustStore support for OIDC and OIDCClient
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin committed Jul 26, 2021
1 parent fcde34d commit 90243f7
Show file tree
Hide file tree
Showing 9 changed files with 177 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ 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"));
if (Verification.CERTIFICATE_VALIDATION == oidcConfig.tls.verification.orElse(Verification.REQUIRED)) {
adapterConfig.setAllowAnyHostname(true);
}
}
adapterConfig.setConnectionPoolSize(keycloakPolicyEnforcerConfig.connectionPoolSize);

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -349,6 +350,12 @@ public enum Verification {
* Certificates are validated and hostname verification is enabled. This is the default value.
*/
REQUIRED,

/**
* Certificates are validated and hostname verification is disabled. This is the default value.
*/
CERTIFICATE_VALIDATION,

/**
* All certificated are trusted and hostname verification is disabled.
*/
Expand All @@ -362,6 +369,24 @@ public enum Verification {
@ConfigItem
public Optional<Verification> verification = Optional.empty();

/**
* An optional trust store which holds the certificate information of the certificates to trust
*/
@ConfigItem
public Optional<Path> trustStoreFile = Optional.empty();

/**
* A parameter to specify the password of the trust store file.
*/
@ConfigItem
public Optional<String> trustStorePassword = Optional.empty();

/**
* A parameter to specify the alias of the trust store certificate.
*/
@ConfigItem
public Optional<String> trustStoreCertAlias = Optional.empty();

public Optional<Verification> getVerification() {
return verification;
}
Expand All @@ -370,6 +395,30 @@ public void setVerification(Verification verification) {
this.verification = Optional.of(verification);
}

public Optional<Path> getTrustStoreFile() {
return trustStoreFile;
}

public void setTrustStoreFile(Path trustStoreFile) {
this.trustStoreFile = Optional.of(trustStoreFile);
}

public Optional<String> getTrustStorePassword() {
return trustStorePassword;
}

public void setTrustStorePassword(String trustStorePassword) {
this.trustStorePassword = Optional.of(trustStorePassword);
}

public Optional<String> getTrustStoreCertAlias() {
return trustStoreCertAlias;
}

public void setTrustStoreCertAlias(String trustStoreCertAlias) {
this.trustStoreCertAlias = Optional.of(trustStoreCertAlias);
}

}

@ConfigGroup
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package io.quarkus.oidc.common.runtime;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ConnectException;
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;
Expand Down Expand Up @@ -35,6 +39,7 @@
import io.smallrye.mutiny.Uni;
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;
Expand Down Expand Up @@ -119,6 +124,23 @@ 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"))
.setAlias(oidcConfig.tls.getTrustStoreCertAlias().orElse(null))
.setValue(io.vertx.core.buffer.Buffer.buffer(trustStoreData))
.setType("JKS");
options.setTrustOptions(trustStoreOptions);
if (Verification.CERTIFICATE_VALIDATION == oidcConfig.tls.verification.orElse(Verification.REQUIRED)) {
options.setVerifyHost(false);
}
} 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<ProxyOptions> proxyOpt = toProxyOptions(oidcConfig.getProxy());
if (proxyOpt.isPresent()) {
Expand Down Expand Up @@ -319,4 +341,29 @@ public static Uni<JsonObject> discoverMetadata(WebClient client, String authServ
.expireIn(connectionDelayInMillisecs)
.onFailure().transform(t -> t.getCause());
}

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();
}
}
6 changes: 0 additions & 6 deletions integration-tests/oidc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,6 @@
</dependencies>

<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ 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
Expand All @@ -18,4 +21,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
quarkus.http.auth.permission.bearer.auth-mechanism=bearer
Binary file not shown.
Binary file added integration-tests/oidc/src/main/resources/tls.crt
Binary file not shown.
32 changes: 32 additions & 0 deletions integration-tests/oidc/src/main/resources/tls.key
Original file line number Diff line number Diff line change
@@ -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: <No 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-----
Original file line number Diff line number Diff line change
Expand Up @@ -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 final Boolean KEYCLOAK_TRUSTSTORE_REQUIRED = false;
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");
Expand All @@ -36,8 +39,18 @@ 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/https";
private static String KEYCLOAK_TLS_CRT = "tls.crt";
private static String KEYCLOAK_TLS_CRT_MOUNTED_PATH = "/etc/x509/https";

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")
Expand All @@ -51,13 +64,24 @@ public Map<String, String> 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")
.withEnv("KEYCLOAK_USER", "admin")
.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)
.withClasspathResourceMapping(KEYCLOAK_TLS_CRT, KEYCLOAK_TLS_CRT_MOUNTED_PATH, BindMode.READ_ONLY);
//.withCopyFileToContainer(MountableFile.forClasspathResource(KEYCLOAK_TLS_KEY),
// KEYCLOAK_TLS_KEY_MOUNTED_PATH)
//.withCopyFileToContainer(MountableFile.forClasspathResource(KEYCLOAK_TLS_CRT),
// KEYCLOAK_TLS_CRT_MOUNTED_PATH);
}

keycloak.start();

if (KEYCLOAK_USE_HTTPS) {
Expand All @@ -75,11 +99,9 @@ public Map<String, String> 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()
Expand Down Expand Up @@ -121,8 +143,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")
Expand Down Expand Up @@ -178,9 +199,7 @@ private static UserRepresentation createUser(String username, List<String> 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)
Expand All @@ -191,9 +210,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)
Expand All @@ -205,9 +222,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);

Expand All @@ -221,4 +236,12 @@ private static List<String> getAdminRoles() {
private static List<String> 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;
}
}

0 comments on commit 90243f7

Please sign in to comment.