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