Develop

Merge request reports

Loading
+175 −0
Changes for src/main/java/org/osl/etsi/util/KeycloakAuthenticator.java: 175 added lines, 0 removed lines.
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);
        }
    }
}
+415 −0

File added.

Preview size limit exceeded, changes collapsed.

+17 −0
Changes for src/main/resources/Dockerfile: 17 added lines, 0 removed lines.
Original line number Diff line number Diff line
# Select a base image with Java installed
FROM openjdk:17-slim

# Set the working directory
WORKDIR /app

# Copy the executable jar file of the application to the image
COPY servicespecificationfetcher-0.0.1-SNAPSHOT.jar /app/servicespecificationfetcher.jar

# Set the command that will run when the container starts
# Note: Pass the data folder path as an environment variable or argument during runtime.

# Example command to pass data folder as a runtime argument:
# docker run -v /local/path/to/config.properties:/app/config.properties -v /local/path/to/your-data-folder:/app/data-folder your-image uuid

# Updated CMD to accept runtime arguments for data folder
ENTRYPOINT ["sh", "-c", "java -jar /app/servicespecificationfetcher.jar --configfile /app/config.properties --servicespecuuid $0"]
 No newline at end of file
+7 −0
Changes for src/main/resources/config.properties: 7 added lines, 0 removed lines.
Original line number Diff line number Diff line
keycloak.url=http://keycloak:8080/auth/realms/openslice/protocol/openid-connect/token
client.id=osapiWebClientId
client.secret=admin
username=admin
password=admin
sourceApiEndpoint.url=http://localhost/tmf-api
serviceSpecification.uuid=487b9377-460d-4498-a8f3-a23cd7595b06
 No newline at end of file
+20 −0
Changes for src/main/resources/logback.xml: 20 added lines, 0 removed lines.
Original line number Diff line number Diff line
<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>app.log</file>
        <append>true</append>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="debug">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="FILE" />
    </root>
</configuration>
 No newline at end of file
Loading
Loading