Commit 940611e3 authored by Ioannis Chatzis's avatar Ioannis Chatzis
Browse files

Implementation Commit

parent f06ce0c9
Loading
Loading
Loading
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>  
+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