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

Support OIDC Mutual TLS #23145

Merged
merged 1 commit into from
Jan 24, 2022
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
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