Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TrustStore support for OIDC and OIDCClient #18012

Merged
merged 1 commit into from
Jul 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 but hostname verification is disabled.
*/
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;
}
}