Skip to content
Snippets Groups Projects

Develop

Merged trantzas requested to merge develop into main
7 files
+ 811
62
Compare changes
  • Side-by-side
  • Inline
Files
7
 
package org.osl.etsi.util;
 
 
import com.fasterxml.jackson.databind.JsonNode;
 
import com.fasterxml.jackson.databind.ObjectMapper;
 
import org.slf4j.Logger;
 
import org.slf4j.LoggerFactory;
 
 
import java.io.IOException;
 
import java.net.URI;
 
import java.net.URLEncoder;
 
import java.net.http.HttpClient;
 
import java.net.http.HttpRequest;
 
import java.net.http.HttpResponse;
 
import java.nio.charset.StandardCharsets;
 
import java.time.Instant;
 
import java.util.Properties;
 
 
public class KeycloakAuthenticator {
 
 
private static final Logger logger = LoggerFactory.getLogger(KeycloakAuthenticator.class.getName());
 
 
private final Properties config;
 
private final HttpClient client;
 
private final ObjectMapper objectMapper;
 
 
private String currentToken;
 
private long tokenExpiryTime;
 
private long tokenRefreshBufferSeconds;
 
 
/**
 
* Constructs a KeycloakAuthenticator with the specified configuration file path.
 
*
 
* @param configFilePath The file path to the configuration properties file.
 
* @throws IOException If there is an error loading the configuration file.
 
*/
 
public KeycloakAuthenticator(Properties config) throws IOException {
 
this.config = config;
 
this.client = HttpClient.newHttpClient();
 
this.objectMapper = new ObjectMapper();
 
setTokenRefreshBufferSeconds();
 
}
 
 
/**
 
* Loads the configuration properties from the specified file path.
 
*
 
* @param configFilePath The file path to the configuration properties file.
 
* @return The token refresh buffer time in seconds.
 
* @throws IOException If the configuration file cannot be read.
 
*/
 
private void setTokenRefreshBufferSeconds() throws IOException {
 
// Load the token refresh buffer time, defaulting to 60 seconds if not specified
 
String bufferSecondsStr = this.config.getProperty("token.refresh.buffer.seconds", "60");
 
long bufferSeconds;
 
try {
 
bufferSeconds = Long.parseLong(bufferSecondsStr);
 
if (bufferSeconds < 0) {
 
throw new NumberFormatException("Buffer seconds cannot be negative.");
 
}
 
} catch (NumberFormatException ex) {
 
logger.warn("Invalid token.refresh.buffer.seconds value: " + bufferSecondsStr + ". Using default of 60 seconds.");
 
bufferSeconds = 60;
 
}
 
 
logger.info("Token refresh buffer set to " + bufferSeconds + " seconds.");
 
this.tokenRefreshBufferSeconds=bufferSeconds;
 
}
 
 
/**
 
* Retrieves a valid access token. If the current token is expired or not present,
 
* it authenticates with Keycloak to obtain a new one.
 
*
 
* @return A valid access token as a String.
 
* @throws IOException If an I/O error occurs during authentication.
 
* @throws InterruptedException If the HTTP request is interrupted.
 
*/
 
public synchronized String getToken() throws IOException, InterruptedException {
 
long currentEpochSeconds = Instant.now().getEpochSecond();
 
 
if (currentToken != null && currentEpochSeconds < (tokenExpiryTime - tokenRefreshBufferSeconds)) {
 
logger.info("Using cached token. Token expires at " + Instant.ofEpochSecond(tokenExpiryTime));
 
return currentToken;
 
} else {
 
logger.info("Cached token is missing or nearing expiration. Authenticating to obtain a new token.");
 
return authenticateAndGetToken();
 
}
 
}
 
 
/**
 
* Authenticates with Keycloak and retrieves a new access token.
 
*
 
* @return The new access token as a String.
 
* @throws IOException If the authentication request fails.
 
* @throws InterruptedException If the HTTP request is interrupted.
 
*/
 
private String authenticateAndGetToken() throws IOException, InterruptedException {
 
String keycloakUrl = config.getProperty("keycloak.url");
 
String clientId = config.getProperty("client.id");
 
String clientSecret = config.getProperty("client.secret");
 
String username = config.getProperty("username");
 
String password = config.getProperty("password");
 
 
// Validate required properties
 
if (keycloakUrl == null || clientId == null || username == null || password == null) {
 
String errorMsg = "Missing required configuration properties.";
 
logger.error(errorMsg);
 
throw new IOException(errorMsg);
 
}
 
 
// Build the form data with URL encoding
 
String form = buildFormData(clientId, clientSecret, username, password);
 
 
HttpRequest request = HttpRequest.newBuilder()
 
.uri(URI.create(keycloakUrl))
 
.header("Content-Type", "application/x-www-form-urlencoded")
 
.POST(HttpRequest.BodyPublishers.ofString(form))
 
.build();
 
 
logger.info("Sending authentication request to Keycloak.");
 
 
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
 
 
if (response.statusCode() == 200) {
 
JsonNode responseJson = objectMapper.readTree(response.body());
 
currentToken = responseJson.get("access_token").asText();
 
long expiresIn = responseJson.get("expires_in").asLong();
 
 
tokenExpiryTime = Instant.now().getEpochSecond() + expiresIn;
 
logger.info("Authentication successful. Token obtained. Token expires in " + expiresIn + " seconds.");
 
 
return currentToken;
 
} else {
 
String errorMsg = "Authentication failed: HTTP status " + response.statusCode() + ": " + response.body();
 
logger.error(errorMsg);
 
throw new IOException(errorMsg);
 
}
 
}
 
 
/**
 
* Builds the URL-encoded form data for the authentication request.
 
*
 
* @param clientId The client ID.
 
* @param clientSecret The client secret (optional).
 
* @param username The username.
 
* @param password The password.
 
* @return The URL-encoded form data as a String.
 
* @throws IOException If URL encoding fails.
 
*/
 
private String buildFormData(String clientId, String clientSecret, String username, String password) throws IOException {
 
StringBuilder form = new StringBuilder();
 
form.append("client_id=").append(urlEncode(clientId));
 
if (clientSecret != null && !clientSecret.isEmpty()) {
 
form.append("&client_secret=").append(urlEncode(clientSecret));
 
}
 
form.append("&username=").append(urlEncode(username));
 
form.append("&password=").append(urlEncode(password));
 
form.append("&grant_type=password");
 
return form.toString();
 
}
 
 
/**
 
* URL-encodes a string using UTF-8 encoding.
 
*
 
* @param value The string to encode.
 
* @return The URL-encoded string.
 
* @throws IOException If UTF-8 encoding is not supported.
 
*/
 
private String urlEncode(String value) throws IOException {
 
try {
 
return URLEncoder.encode(value, StandardCharsets.UTF_8.toString());
 
} catch (Exception ex) {
 
logger.error("URL encoding failed for value: " + value, ex);
 
throw new IOException("URL encoding failed.", ex);
 
}
 
}
 
}
Loading