Loading pom.xml 0 → 100644 +101 −0 Original line number Diff line number Diff line <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.etsi.osl.util</groupId> <artifactId>servicespecificationfetcher</artifactId> <version>0.0.1-SNAPSHOT</version> <!-- Specify the Java version you're using --> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> </properties> <dependencies> <!-- Jackson JSON Processor --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.12.3</version> </dependency> <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.16</version> </dependency> <!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.5.8</version> </dependency> <!-- Google Gson --> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.6</version> </dependency> <!-- Your custom library dependency --> <dependency> <groupId>org.etsi.osl</groupId> <artifactId>org.etsi.osl.model.tmf</artifactId> <version>1.0.0</version> </dependency> </dependencies> <build> <plugins> <!-- Maven Compiler Plugin --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>17</source> <target>17</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.2.0</version> <configuration> <archive> <manifest> <mainClass>org.osl.etsi.util.ServiceSpecificationFetcher</mainClass> </manifest> </archive> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.4.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <createDependencyReducedPom>true</createDependencyReducedPom> <transformers> <!-- Ensures that the manifest specifies the main class --> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>org.osl.etsi.util.ServiceSpecificationFetcher</mainClass> <!-- Fully Qualified Name --> </transformer> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build> </project> src/main/java/org/osl/etsi/util/KeycloakAuthenticator.java 0 → 100644 +175 −0 Original line number Diff line number Diff line 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
pom.xml 0 → 100644 +101 −0 Original line number Diff line number Diff line <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.etsi.osl.util</groupId> <artifactId>servicespecificationfetcher</artifactId> <version>0.0.1-SNAPSHOT</version> <!-- Specify the Java version you're using --> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> </properties> <dependencies> <!-- Jackson JSON Processor --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.12.3</version> </dependency> <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.16</version> </dependency> <!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.5.8</version> </dependency> <!-- Google Gson --> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.6</version> </dependency> <!-- Your custom library dependency --> <dependency> <groupId>org.etsi.osl</groupId> <artifactId>org.etsi.osl.model.tmf</artifactId> <version>1.0.0</version> </dependency> </dependencies> <build> <plugins> <!-- Maven Compiler Plugin --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>17</source> <target>17</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.2.0</version> <configuration> <archive> <manifest> <mainClass>org.osl.etsi.util.ServiceSpecificationFetcher</mainClass> </manifest> </archive> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.4.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <createDependencyReducedPom>true</createDependencyReducedPom> <transformers> <!-- Ensures that the manifest specifies the main class --> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>org.osl.etsi.util.ServiceSpecificationFetcher</mainClass> <!-- Fully Qualified Name --> </transformer> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>
src/main/java/org/osl/etsi/util/KeycloakAuthenticator.java 0 → 100644 +175 −0 Original line number Diff line number Diff line 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); } } }