AuthPipeline.java
package com.bonitasoft.processbuilder.execution;
import com.bonitasoft.processbuilder.extension.PasswordCrypto;
import com.bonitasoft.processbuilder.records.RestAuthConfig;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Pipeline for normalizing and decrypting authentication configuration.
* <p>
* Extracted from ExecuteRestService to be shared between REST Extension and custom connector.
* Handles backward-compatible field name normalization and AES/GCM decryption.
* </p>
*/
public final class AuthPipeline {
private static final Logger LOGGER = LoggerFactory.getLogger(AuthPipeline.class);
private static final ObjectMapper MAPPER = new ObjectMapper();
private AuthPipeline() {}
/**
* Full pipeline: normalize → decrypt → parse.
*
* @param authNode Raw auth JSON from PBConfiguration
* @return Parsed RestAuthConfig ready for HTTP execution
*/
public static RestAuthConfig resolve(JsonNode authNode) {
if (authNode == null || authNode.isNull() || !authNode.isObject()) {
return RestAuthConfig.none();
}
JsonNode normalized = normalizeAuthConfig(authNode);
JsonNode decrypted = decryptSensitiveFields(normalized);
RestAuthConfig config = RestAuthConfig.fromJson(decrypted, LOGGER);
return config != null ? config : RestAuthConfig.none();
}
/**
* Normalizes auth configuration JSON for backward compatibility.
* Handles field name variations: "type"→"authType", "apiKeyName"→"keyName", etc.
*/
public static JsonNode normalizeAuthConfig(JsonNode authNode) {
if (authNode == null || authNode.isNull() || !authNode.isObject()) {
return authNode;
}
ObjectNode normalized = MAPPER.createObjectNode();
String type = null;
if (authNode.has("type")) {
type = authNode.get("type").asText();
} else if (authNode.has("authType")) {
type = authNode.get("authType").asText();
}
if (type != null) {
normalized.put("authType", type.toLowerCase());
}
if (type != null && type.equalsIgnoreCase("API_KEY")) {
if (authNode.has("apiKeyName")) {
normalized.put("keyName", authNode.get("apiKeyName").asText());
} else if (authNode.has("keyName")) {
normalized.put("keyName", authNode.get("keyName").asText());
}
if (authNode.has("apiKeyValue")) {
normalized.put("keyValue", authNode.get("apiKeyValue").asText());
} else if (authNode.has("keyValue")) {
normalized.put("keyValue", authNode.get("keyValue").asText());
}
String location = null;
if (authNode.has("apiKeyLocation")) {
location = authNode.get("apiKeyLocation").asText();
} else if (authNode.has("location")) {
location = authNode.get("location").asText();
}
if (location != null) {
normalized.put("location", normalizeApiKeyLocation(location));
}
} else {
authNode.fields().forEachRemaining(field -> {
String key = field.getKey();
if (!normalized.has(key) && !"type".equals(key) && !"authType".equals(key)) {
normalized.set(key, field.getValue());
}
});
}
LOGGER.debug("Normalized auth config: {} -> {}", authNode, normalized);
return normalized;
}
/**
* Decrypts sensitive fields in auth configuration using PasswordCrypto (AES/GCM).
*/
public static JsonNode decryptSensitiveFields(JsonNode authNode) {
if (authNode == null || authNode.isNull() || !authNode.isObject()) {
return authNode;
}
if (!PasswordCrypto.isMasterPasswordConfigured()) {
LOGGER.debug("Master password not configured, skipping decryption");
return authNode;
}
ObjectNode decrypted = authNode.deepCopy();
String type = decrypted.has("authType") ? decrypted.get("authType").asText().toLowerCase() : "";
switch (type) {
case "basic" -> decryptField(decrypted, "password");
case "bearer" -> decryptField(decrypted, "token");
case "api_key" -> decryptField(decrypted, "keyValue");
case "oauth2_client_credentials" -> decryptField(decrypted, "clientSecret");
case "oauth2_password" -> {
decryptField(decrypted, "password");
decryptField(decrypted, "clientSecret");
}
default -> { /* No sensitive fields */ }
}
return decrypted;
}
private static void decryptField(ObjectNode node, String fieldName) {
if (node.has(fieldName) && !node.get(fieldName).isNull()) {
String encrypted = node.get(fieldName).asText();
if (encrypted != null && !encrypted.isEmpty()) {
try {
String decryptedValue = PasswordCrypto.decryptIfNeeded(encrypted);
node.put(fieldName, decryptedValue);
LOGGER.debug("Decrypted field '{}' successfully", fieldName);
} catch (Exception e) {
LOGGER.warn("Failed to decrypt field '{}', using original value: {}", fieldName, e.getMessage());
}
}
}
}
private static String normalizeApiKeyLocation(String location) {
if (location == null) return "header";
String upper = location.toUpperCase();
if (upper.equals("QUERY") || upper.equals("QUERY_PARAM") || upper.equals("QUERYPARAM")) {
return "queryParam";
} else if (upper.equals("HEADER")) {
return "header";
}
return location.toLowerCase();
}
}