Skip to content

Commit

Permalink
Merge pull request #23145 from sberyozkin/oidc_mtls
Browse files Browse the repository at this point in the history
Support OIDC Mutual TLS
  • Loading branch information
sberyozkin authored Jan 24, 2022
2 parents bba0749 + ff4880b commit 8e6518f
Show file tree
Hide file tree
Showing 18 changed files with 343 additions and 78 deletions.
5 changes: 3 additions & 2 deletions build-parent/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,9 @@

<!-- The image to use for tests that run Keycloak -->
<!-- IMPORTANT: If this is changed you must also update bom/application/pom.xml and KeycloakBuildTimeConfig/DevServicesConfig in quarkus-oidc/deployment to match the version -->
<keycloak.docker.image>quay.io/keycloak/keycloak:16.1.0</keycloak.docker.image>

<keycloak.version>16.1.0</keycloak.version>
<keycloak.docker.image>quay.io/keycloak/keycloak:${keycloak.version}</keycloak.docker.image>

<unboundid-ldap.version>6.0.3</unboundid-ldap.version>

<wiremock-jre8.version>2.27.2</wiremock-jre8.version>
Expand Down
25 changes: 25 additions & 0 deletions docs/src/main/asciidoc/security-openid-connect-client.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,31 @@ quarkus.oidc-client.credentials.jwt.subject=${apple.subject}
quarkus.oidc-client.credentials.jwt.issuer=${apple.issuer}
----

==== Mutual TLS

Some OpenID Connect Providers may require that a client is authenticated as part of the `Mutual TLS` (`MTLS`) authentication process.

`quarkus-oidc-client` can be configured as follows to support `MTLS`:

[source,properties]
----
quarkus.oidc.tls.verification=certificate-validation
# Keystore configuration
quarkus.oidc.client.tls.key-store-file=client-keystore.jks
quarkus.oidc.client.tls.key-store-password=${key-store-password}
# Add more keystore properties if needed:
#quarkus.oidc.client.tls.key-store-alias=keyAlias
#quarkus.oidc.client.tls.key-store-alias-password=keyAliasPassword
# Truststore configuration
quarkus.oidc.client.tls.trust-store-file=client-truststore.jks
quarkus.oidc.client.tls.trust-store-password=${trust-store-password}
# Add more truststore properties if needed:
#quarkus.oidc.client.tls.trust-store-alias=certAlias
----

[[integration-testing-oidc-client]]
=== Testing

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -978,7 +978,6 @@ quarkus.oidc.credentials.jwt.subject=custom-subject
quarkus.oidc.credentials.jwt.issuer=custom-issuer
----


==== Apple POST JWT

Apple OpenID Connect Provider uses a `client_secret_post` method where a secret is a JWT produced with a `private_key_jwt` authentication method but with Apple account specific issuer and subject claims.
Expand All @@ -999,6 +998,31 @@ quarkus.oidc.credentials.jwt.subject=${apple.subject}
quarkus.oidc.credentials.jwt.issuer=${apple.issuer}
----

==== Mutual TLS

Some OpenID Connect Providers may require that a client is authenticated as part of the `Mutual TLS` (`MTLS`) authentication process.

`quarkus-oidc` can be configured as follows to support `MTLS`:

[source,properties]
----
quarkus.oidc.tls.verification=certificate-validation
# Keystore configuration
quarkus.oidc.tls.key-store-file=client-keystore.jks
quarkus.oidc.tls.key-store-password=${key-store-password}
# Add more keystore properties if needed:
#quarkus.oidc.tls.key-store-alias=keyAlias
#quarkus.oidc.tls.key-store-alias-password=keyAliasPassword
# Truststore configuration
quarkus.oidc.tls.trust-store-file=client-truststore.jks
quarkus.oidc.tls.trust-store-password=${trust-store-password}
# Add more truststore properties if needed:
#quarkus.oidc.tls.trust-store-alias=certAlias
----

[[integration-testing]]
=== Testing

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,39 @@ public enum Verification {
@ConfigItem
public Optional<Verification> verification = Optional.empty();

/**
* An optional key store which holds the certificate information instead of specifying separate files.
*/
@ConfigItem
public Optional<Path> keyStoreFile = Optional.empty();

/**
* An optional parameter to specify type of the key store file. If not given, the type is automatically detected
* based on the file name.
*/
@ConfigItem
public Optional<String> keyStoreFileType = Optional.empty();

/**
* A parameter to specify the password of the key store file. If not given, the default ("password") is used.
*/
@ConfigItem(defaultValue = "password")
public String keyStorePassword;

/**
* An optional parameter to select a specific key in the key store. When SNI is disabled, if the key store contains
* multiple
* keys and no alias is specified, the behavior is undefined.
*/
@ConfigItem
public Optional<String> keyStoreKeyAlias = Optional.empty();

/**
* An optional parameter to define the password for the key, in case it's different from {@link #keyStorePassword}.
*/
@ConfigItem
public Optional<String> keyStoreKeyPassword = Optional.empty();

/**
* An optional trust store which holds the certificate information of the certificates to trust
*/
Expand All @@ -444,6 +477,13 @@ public enum Verification {
@ConfigItem
public Optional<String> trustStoreCertAlias = Optional.empty();

/**
* An optional parameter to specify type of the trust store file. If not given, the type is automatically detected
* based on the file name.
*/
@ConfigItem
public Optional<String> trustStoreFileType = Optional.empty();

public Optional<Verification> getVerification() {
return verification;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ public static void setHttpClientOptions(OidcCommonConfig oidcConfig, TlsConfig t
.setPassword(oidcConfig.tls.getTrustStorePassword().orElse("password"))
.setAlias(oidcConfig.tls.getTrustStoreCertAlias().orElse(null))
.setValue(io.vertx.core.buffer.Buffer.buffer(trustStoreData))
.setType("JKS");
.setType(getStoreType(oidcConfig.tls.trustStoreFileType, oidcConfig.tls.trustStoreFile.get()));
options.setTrustOptions(trustStoreOptions);
if (Verification.CERTIFICATE_VALIDATION == oidcConfig.tls.verification.orElse(Verification.REQUIRED)) {
options.setVerifyHost(false);
Expand All @@ -141,6 +141,23 @@ public static void setHttpClientOptions(OidcCommonConfig oidcConfig, TlsConfig t
oidcConfig.tls.trustStoreFile.get().toString()), ex);
}
}
if (oidcConfig.tls.keyStoreFile.isPresent()) {
try {
byte[] keyStoreData = getFileContent(oidcConfig.tls.keyStoreFile.get());
io.vertx.core.net.KeyStoreOptions keyStoreOptions = new KeyStoreOptions()
.setPassword(oidcConfig.tls.keyStorePassword)
.setAlias(oidcConfig.tls.keyStoreKeyAlias.orElse(null))
.setAliasPassword(oidcConfig.tls.keyStoreKeyPassword.orElse(null))
.setValue(io.vertx.core.buffer.Buffer.buffer(keyStoreData))
.setType(getStoreType(oidcConfig.tls.keyStoreFileType, oidcConfig.tls.keyStoreFile.get()));
options.setKeyCertOptions(keyStoreOptions);

} catch (IOException ex) {
throw new ConfigurationException(String.format(
"OIDC keystore file does not exist or can not be read",
oidcConfig.tls.keyStoreFile.get().toString()), ex);
}
}
Optional<ProxyOptions> proxyOpt = toProxyOptions(oidcConfig.getProxy());
if (proxyOpt.isPresent()) {
options.setProxyOptions(proxyOpt.get());
Expand All @@ -154,6 +171,19 @@ public static void setHttpClientOptions(OidcCommonConfig oidcConfig, TlsConfig t
options.setConnectTimeout((int) oidcConfig.getConnectionTimeout().toMillis());
}

private static String getStoreType(Optional<String> fileType, Path storePath) {
if (fileType.isPresent()) {
return fileType.get().toUpperCase();
}
final String pathName = storePath.toString();
if (pathName.endsWith(".p12") || pathName.endsWith(".pkcs12") || pathName.endsWith(".pfx")) {
return "PKCS12";
} else {
// assume jks
return "JKS";
}
}

public static String getAuthServerUrl(OidcCommonConfig oidcConfig) {
return removeLastPathSeparator(oidcConfig.getAuthServerUrl().get());
}
Expand Down
16 changes: 12 additions & 4 deletions integration-tests/oidc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,23 @@
</dependency>
<!-- test dependencies -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-keycloak-server</artifactId>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-security-oidc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
Expand Down Expand Up @@ -146,7 +154,7 @@
<configuration>
<skip>false</skip>
<systemPropertyVariables>
<keycloak.docker.image>${keycloak.docker.image}</keycloak.docker.image>
<keycloak.version>${keycloak.version}</keycloak.version>
</systemPropertyVariables>
</configuration>
</plugin>
Expand All @@ -155,7 +163,7 @@
<configuration>
<skip>false</skip>
<systemPropertyVariables>
<keycloak.docker.image>${keycloak.docker.image}</keycloak.docker.image>
<keycloak.version>${keycloak.version}</keycloak.version>
</systemPropertyVariables>
</configuration>
</plugin>
Expand Down
14 changes: 9 additions & 5 deletions integration-tests/oidc/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
# Configuration file
quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus/
quarkus.oidc.auth-server-url=${quarkus.oidc.auth-server-url}
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.oidc.tls.verification=certificate-validation
quarkus.oidc.tls.trust-store-file=client-truststore.jks
quarkus.oidc.tls.trust-store-password=password
quarkus.oidc.tls.key-store-file=client-keystore.jks
quarkus.oidc.tls.key-store-password=password
quarkus.native.additional-build-args=-H:IncludeResources=.*\\.jks

quarkus.http.cors=true

quarkus.http.auth.basic=true
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file removed integration-tests/oidc/src/main/resources/tls.crt
Binary file not shown.
32 changes: 0 additions & 32 deletions integration-tests/oidc/src/main/resources/tls.key

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package io.quarkus.it.keycloak;

import static io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager.getAccessToken;
import static io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager.getRefreshToken;
import static io.quarkus.it.keycloak.KeycloakXTestResourceLifecycleManager.getAccessToken;
import static io.quarkus.it.keycloak.KeycloakXTestResourceLifecycleManager.getRefreshToken;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.equalTo;

Expand All @@ -14,14 +14,13 @@

import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager;
import io.restassured.RestAssured;

/**
* @author <a href="mailto:[email protected]">Pedro Igor</a>
*/
@QuarkusTest
@QuarkusTestResource(KeycloakTestResourceLifecycleManager.class)
@QuarkusTestResource(KeycloakXTestResourceLifecycleManager.class)
public class BearerTokenAuthorizationTest {

@Test
Expand Down
Loading

0 comments on commit 8e6518f

Please sign in to comment.