Skip to content
Draft
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
@@ -0,0 +1,93 @@
/*-
* ---license-start
* keycloak-config-cli
* ---
* Copyright (C) 2017 - 2021 adorsys GmbH & Co. KG @ https://adorsys.com
* ---
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ---license-end
*/

package de.adorsys.keycloak.config.configuration;

import de.adorsys.keycloak.config.properties.KeycloakConfigProperties;
import org.apache.http.ssl.SSLContextBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

import java.io.FileInputStream;
import java.security.KeyStore;
import javax.net.ssl.SSLContext;

@Component
@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true)
public class RestClientX509Config {

private static final Logger logger = LoggerFactory.getLogger(RestClientX509Config.class);

private final KeycloakConfigProperties.X509Config x509Config;
private SSLContext sslContext;

@Autowired
public RestClientX509Config(KeycloakConfigProperties properties) {
this.x509Config = properties.getX509();
}

public boolean isX509Configured() {
return x509Config != null && x509Config.isConfigured();
}

public SSLContext getSslContext() throws Exception {

if (sslContext == null && isX509Configured()) {
sslContext = createSslContext();
}

return sslContext;
}

private SSLContext createSslContext() throws Exception {

logger.info("Loading X.509 certificates for client authentication");

// Load keystore
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
try (FileInputStream keystoreFile = new FileInputStream(x509Config.keystorePath())) {
keystore.load(keystoreFile, x509Config.keystorePassword().toCharArray());
logger.debug("Keystore loaded from: {}", x509Config.keystorePath());
}

// Load truststore
KeyStore truststore = KeyStore.getInstance(KeyStore.getDefaultType());
try (FileInputStream truststoreFile = new FileInputStream(x509Config.truststorePath())) {
truststore.load(truststoreFile, x509Config.truststorePassword().toCharArray());
logger.debug("Truststore loaded from: {}", x509Config.truststorePath());
}

// Build SSLContext
SSLContext context = SSLContextBuilder.create()
.loadKeyMaterial(keystore, x509Config.keystorePassword().toCharArray())
.loadTrustMaterial(truststore, null)
.build();

logger.info("SSL context successfully created for X.509 authentication");
return context;
}
}




Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,8 @@ public KeycloakProviderException(String message) {
public KeycloakProviderException(Throwable cause) {
super(cause);
}

public KeycloakProviderException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ public class KeycloakConfigProperties {
@Valid
private final KeycloakAvailabilityCheck availabilityCheck;

@Valid
private final X509Config x509;

public KeycloakConfigProperties(
@DefaultValue("master") String loginRealm,
@DefaultValue("admin-cli") String clientId,
Expand All @@ -82,7 +85,8 @@ public KeycloakConfigProperties(
URL httpProxy,
@DefaultValue KeycloakAvailabilityCheck availabilityCheck,
@DefaultValue("10s") Duration connectTimeout,
@DefaultValue("10s") Duration readTimeout
@DefaultValue("10s") Duration readTimeout,
@DefaultValue X509Config x509
) {
this.loginRealm = loginRealm;
this.clientId = clientId;
Expand All @@ -97,6 +101,7 @@ public KeycloakConfigProperties(
this.availabilityCheck = availabilityCheck;
this.connectTimeout = connectTimeout;
this.readTimeout = readTimeout;
this.x509 = x509;
}

public String getLoginRealm() {
Expand Down Expand Up @@ -151,6 +156,21 @@ public Duration getReadTimeout() {
return readTimeout;
}

public X509Config getX509() {
return x509;
}

public record X509Config(String keystorePath, String keystorePassword, String truststorePath,
String truststorePassword) {

public boolean isConfigured() {
return keystorePath != null && !keystorePath.isEmpty()
&& keystorePassword != null && !keystorePassword.isEmpty()
&& truststorePath != null && !truststorePath.isEmpty()
&& truststorePassword != null && !truststorePassword.isEmpty();
}
}

public static class KeycloakAvailabilityCheck {
@NotNull
private final boolean enabled;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.adorsys.keycloak.config.configuration.RestClientX509Config;
import de.adorsys.keycloak.config.exception.KeycloakProviderException;
import de.adorsys.keycloak.config.properties.KeycloakConfigProperties;
import de.adorsys.keycloak.config.token.RestClientX509TokenManager;
import de.adorsys.keycloak.config.util.ResteasyUtil;
import dev.failsafe.Failsafe;
import dev.failsafe.RetryPolicy;
Expand Down Expand Up @@ -61,6 +63,8 @@ public class KeycloakProvider implements AutoCloseable {
private static final Logger logger = LoggerFactory.getLogger(KeycloakProvider.class);

private final KeycloakConfigProperties properties;
private final RestClientX509Config restClientX509Config;
private final RestClientX509TokenManager customTokenManager;
private final Supplier<ResteasyClient> resteasyClientSupplier;

private Keycloak keycloak;
Expand All @@ -69,14 +73,23 @@ public class KeycloakProvider implements AutoCloseable {
private String version;

@Autowired
private KeycloakProvider(KeycloakConfigProperties properties) {
private KeycloakProvider(KeycloakConfigProperties properties, RestClientX509Config restClientX509Config) {
this.properties = properties;
this.resteasyClientSupplier = () -> ResteasyUtil.getClient(
!this.properties.isSslVerify(),
this.properties.getHttpProxy(),
this.properties.getConnectTimeout(),
this.properties.getReadTimeout()
);
this.restClientX509Config = restClientX509Config;
this.resteasyClientSupplier = () -> {
try {
return ResteasyUtil.getClient(
!this.properties.isSslVerify(),
this.properties.getHttpProxy(),
this.properties.getConnectTimeout(),
this.properties.getReadTimeout(),
this.restClientX509Config
);
} catch (Exception e) {
throw new KeycloakProviderException(e);
}
};
this.customTokenManager = new RestClientX509TokenManager(properties, resteasyClientSupplier);
}

public Keycloak getInstance() {
Expand All @@ -100,7 +113,11 @@ public String getKeycloakVersion() {
}

public void refreshToken() {
getInstance().tokenManager().refreshToken();
if (restClientX509Config.isX509Configured()) {
customTokenManager.refreshToken();
} else {
getInstance().tokenManager().refreshToken();
}
}

public <T> T getCustomApiProxy(Class<T> proxyClass) {
Expand Down Expand Up @@ -150,22 +167,45 @@ private Keycloak getKeycloakWithRetry() {

private Keycloak getKeycloak() {
Keycloak keycloakInstance = getKeycloakInstance(properties.getUrl());
keycloakInstance.tokenManager().getAccessToken();
if (!restClientX509Config.isX509Configured()) {
keycloakInstance.tokenManager().getAccessToken();
}

return keycloakInstance;
}

private Keycloak getKeycloakInstance(String serverUrl) {
return KeycloakBuilder.builder()
.serverUrl(serverUrl)
.realm(properties.getLoginRealm())
.clientId(properties.getClientId())
.grantType(properties.getGrantType())
.clientSecret(properties.getClientSecret())
.username(properties.getUser())
.password(properties.getPassword())
.resteasyClient(resteasyClient)
.build();
if (restClientX509Config.isX509Configured()) {
return getKeycloakInstanceWithX509(serverUrl);
} else {
return KeycloakBuilder.builder()
.serverUrl(serverUrl)
.realm(properties.getLoginRealm())
.clientId(properties.getClientId())
.grantType(properties.getGrantType())
.clientSecret(properties.getClientSecret())
.username(properties.getUser())
.password(properties.getPassword())
.resteasyClient(resteasyClient)
.build();
}
}

private Keycloak getKeycloakInstanceWithX509(String serverUrl) {
try {
String accessToken = customTokenManager.getAccessTokenString();
logger.debug("Building Keycloak client with X.509 authentication for server: {}", serverUrl);
return KeycloakBuilder.builder()
.serverUrl(serverUrl)
.realm(properties.getLoginRealm())
.clientId(properties.getClientId())
.authorization("Bearer " + accessToken)
.resteasyClient(resteasyClient)
.build();
} catch (Exception e) {
logger.error("Failed to build Keycloak client with X.509 authentication", e);
throw new KeycloakProviderException("Could not build Keycloak client with X.509 authentication: " + e.getMessage(), e);
}
}

private void checkServerVersion() {
Expand Down Expand Up @@ -206,9 +246,11 @@ public void close() {
* returns 204 if successful, 400 if not with a json error response.
*/
private void logout() {

String refreshToken = this.keycloak.tokenManager().getAccessToken().getRefreshToken();
// if we do not have a refreshToken, we are not able ot logout (grant_type=client_credentials)
if (refreshToken == null) {
// or authenticating with a x509 certificate does not use a refresh token
if (refreshToken == null || restClientX509Config.isX509Configured()) {
return;
}

Expand Down
Loading
Loading