RestAuthConfig.java
package com.bonitasoft.processbuilder.records;
import com.bonitasoft.processbuilder.enums.RestApiKeyLocation;
import com.bonitasoft.processbuilder.enums.RestAuthenticationType;
import com.bonitasoft.processbuilder.enums.RestOAuth2ClientAuthMethod;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import java.util.Base64;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
/**
* Sealed interface representing authentication configuration for REST services.
* <p>
* This interface defines the contract for all authentication configuration types.
* Each implementation corresponds to a specific {@link RestAuthenticationType}.
* </p>
*
* @author Bonitasoft
* @since 1.0
*/
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "authType"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = RestAuthConfig.NoAuth.class, name = "none"),
@JsonSubTypes.Type(value = RestAuthConfig.BasicAuth.class, name = "basic"),
@JsonSubTypes.Type(value = RestAuthConfig.BearerAuth.class, name = "bearer"),
@JsonSubTypes.Type(value = RestAuthConfig.ApiKeyAuth.class, name = "apiKey"),
@JsonSubTypes.Type(value = RestAuthConfig.OAuth2ClientCredentials.class, name = "oauth2ClientCredentials"),
@JsonSubTypes.Type(value = RestAuthConfig.OAuth2Password.class, name = "oauth2Password"),
@JsonSubTypes.Type(value = RestAuthConfig.CustomAuth.class, name = "custom")
})
public sealed interface RestAuthConfig permits
RestAuthConfig.NoAuth,
RestAuthConfig.BasicAuth,
RestAuthConfig.BearerAuth,
RestAuthConfig.ApiKeyAuth,
RestAuthConfig.OAuth2ClientCredentials,
RestAuthConfig.OAuth2Password,
RestAuthConfig.CustomAuth {
/**
* Gets the authentication type for this configuration.
*
* @return The authentication type
*/
RestAuthenticationType getAuthType();
/**
* Generates the HTTP headers required for this authentication.
*
* @return Map of header name to header value
*/
Map<String, String> getAuthHeaders();
/**
* Gets any query parameters required for this authentication.
*
* @return Map of parameter name to parameter value
*/
default Map<String, String> getAuthQueryParams() {
return Collections.emptyMap();
}
// ========================================================================
// Factory Methods
// ========================================================================
/**
* Creates an empty/no authentication configuration.
*
* @return NoAuth instance
*/
static NoAuth none() {
return new NoAuth();
}
/**
* Creates a basic authentication configuration.
*
* @param username The username
* @param password The password
* @return BasicAuth instance
*/
static BasicAuth basic(String username, String password) {
return new BasicAuth(username, password, true);
}
/**
* Creates a bearer token authentication configuration.
*
* @param token The bearer token
* @return BearerAuth instance
*/
static BearerAuth bearer(String token) {
return new BearerAuth(token);
}
/**
* Creates an API key authentication configuration.
*
* @param keyName The name of the API key header/parameter
* @param keyValue The API key value
* @param location Where to send the key (HEADER or QUERY_PARAM)
* @return ApiKeyAuth instance
*/
static ApiKeyAuth apiKey(String keyName, String keyValue, RestApiKeyLocation location) {
return new ApiKeyAuth(keyName, keyValue, location);
}
/**
* Creates an OAuth2 Client Credentials configuration.
*
* @param tokenUrl The token endpoint URL
* @param clientId The client ID
* @param clientSecret The client secret
* @return OAuth2ClientCredentials instance
*/
static OAuth2ClientCredentials oauth2ClientCredentials(String tokenUrl, String clientId, String clientSecret) {
return new OAuth2ClientCredentials(tokenUrl, clientId, clientSecret, null, null, RestOAuth2ClientAuthMethod.BODY);
}
/**
* Parses authentication configuration from JSON.
*
* @param authNode The JSON node containing auth configuration
* @param logger Optional logger for warnings
* @return The parsed RestAuthConfig
*/
static RestAuthConfig fromJson(JsonNode authNode, Logger logger) {
if (authNode == null || authNode.isNull() || authNode.isEmpty()) {
return none();
}
String authType = getTextValue(authNode, "authType", "none");
return switch (authType.toLowerCase()) {
case "basic" -> new BasicAuth(
getTextValue(authNode, "username", ""),
getTextValue(authNode, "password", ""),
getBooleanValue(authNode, "preemptive", true)
);
case "bearer" -> new BearerAuth(
getTextValue(authNode, "token", "")
);
case "apikey", "api_key" -> new ApiKeyAuth(
getTextValue(authNode, "keyName", "X-API-Key"),
getTextValue(authNode, "keyValue", ""),
RestApiKeyLocation.fromKey(getTextValue(authNode, "location", "header"))
.orElse(RestApiKeyLocation.HEADER)
);
case "oauth2clientcredentials", "oauth2_client_credentials" -> new OAuth2ClientCredentials(
getTextValue(authNode, "tokenUrl", ""),
getTextValue(authNode, "clientId", ""),
getTextValue(authNode, "clientSecret", ""),
getTextValue(authNode, "scope", null),
getTextValue(authNode, "audience", null),
RestOAuth2ClientAuthMethod.fromKey(getTextValue(authNode, "clientAuthMethod", "body"))
.orElse(RestOAuth2ClientAuthMethod.BODY)
);
case "oauth2password", "oauth2_password" -> new OAuth2Password(
getTextValue(authNode, "tokenUrl", ""),
getTextValue(authNode, "clientId", ""),
getTextValue(authNode, "clientSecret", null),
getTextValue(authNode, "username", ""),
getTextValue(authNode, "password", ""),
getTextValue(authNode, "scope", null)
);
case "custom" -> {
Map<String, String> headers = Collections.emptyMap();
JsonNode headersNode = authNode.get("headers");
if (headersNode != null && headersNode.isObject()) {
try {
ObjectMapper mapper = new ObjectMapper();
headers = mapper.convertValue(headersNode,
mapper.getTypeFactory().constructMapType(Map.class, String.class, String.class));
} catch (Exception e) {
if (logger != null) {
logger.warn("Failed to parse custom auth headers: {}", e.getMessage());
}
}
}
yield new CustomAuth(headers);
}
default -> none();
};
}
private static String getTextValue(JsonNode node, String field, String defaultValue) {
JsonNode fieldNode = node.get(field);
if (fieldNode == null || fieldNode.isNull()) {
return defaultValue;
}
String value = fieldNode.asText();
return (value == null || value.isBlank()) ? defaultValue : value.trim();
}
private static boolean getBooleanValue(JsonNode node, String field, boolean defaultValue) {
JsonNode fieldNode = node.get(field);
if (fieldNode == null || fieldNode.isNull()) {
return defaultValue;
}
return fieldNode.asBoolean(defaultValue);
}
// ========================================================================
// Record Implementations
// ========================================================================
/**
* No authentication configuration.
*/
@JsonIgnoreProperties(ignoreUnknown = true)
record NoAuth() implements RestAuthConfig {
@Override
public RestAuthenticationType getAuthType() {
return RestAuthenticationType.NONE;
}
@Override
public Map<String, String> getAuthHeaders() {
return Collections.emptyMap();
}
}
/**
* HTTP Basic Authentication configuration.
*
* @param username The username
* @param password The password
* @param preemptive Whether to send credentials without waiting for 401
*/
@JsonIgnoreProperties(ignoreUnknown = true)
record BasicAuth(String username, String password, boolean preemptive) implements RestAuthConfig {
public BasicAuth {
username = username != null ? username : "";
password = password != null ? password : "";
}
@Override
public RestAuthenticationType getAuthType() {
return RestAuthenticationType.BASIC;
}
@Override
public Map<String, String> getAuthHeaders() {
String credentials = username + ":" + password;
String encoded = Base64.getEncoder().encodeToString(credentials.getBytes());
return Map.of("Authorization", "Basic " + encoded);
}
}
/**
* Bearer token authentication configuration.
*
* @param token The bearer token (JWT, OAuth2 access token, etc.)
*/
@JsonIgnoreProperties(ignoreUnknown = true)
record BearerAuth(String token) implements RestAuthConfig {
public BearerAuth {
token = token != null ? token : "";
}
@Override
public RestAuthenticationType getAuthType() {
return RestAuthenticationType.BEARER;
}
@Override
public Map<String, String> getAuthHeaders() {
return Map.of("Authorization", "Bearer " + token);
}
}
/**
* API Key authentication configuration.
*
* @param keyName The name of the header or query parameter
* @param keyValue The API key value
* @param location Where to send the key (HEADER or QUERY_PARAM)
*/
@JsonIgnoreProperties(ignoreUnknown = true)
record ApiKeyAuth(String keyName, String keyValue, RestApiKeyLocation location) implements RestAuthConfig {
public ApiKeyAuth {
keyName = keyName != null && !keyName.isBlank() ? keyName : "X-API-Key";
keyValue = keyValue != null ? keyValue : "";
location = location != null ? location : RestApiKeyLocation.HEADER;
}
@Override
public RestAuthenticationType getAuthType() {
return RestAuthenticationType.API_KEY;
}
@Override
public Map<String, String> getAuthHeaders() {
if (location == RestApiKeyLocation.HEADER) {
return Map.of(keyName, keyValue);
}
return Collections.emptyMap();
}
@Override
public Map<String, String> getAuthQueryParams() {
if (location == RestApiKeyLocation.QUERY_PARAM) {
return Map.of(keyName, keyValue);
}
return Collections.emptyMap();
}
}
/**
* OAuth 2.0 Client Credentials authentication configuration.
*
* @param tokenUrl The token endpoint URL
* @param clientId The client ID
* @param clientSecret The client secret
* @param scope Optional scope(s) to request
* @param audience Optional audience (for Auth0, etc.)
* @param clientAuthMethod How to send client credentials (BODY or HEADER)
*/
@JsonIgnoreProperties(ignoreUnknown = true)
record OAuth2ClientCredentials(
String tokenUrl,
String clientId,
String clientSecret,
String scope,
String audience,
RestOAuth2ClientAuthMethod clientAuthMethod
) implements RestAuthConfig {
public OAuth2ClientCredentials {
tokenUrl = tokenUrl != null ? tokenUrl : "";
clientId = clientId != null ? clientId : "";
clientSecret = clientSecret != null ? clientSecret : "";
clientAuthMethod = clientAuthMethod != null ? clientAuthMethod : RestOAuth2ClientAuthMethod.BODY;
}
@Override
public RestAuthenticationType getAuthType() {
return RestAuthenticationType.OAUTH2_CLIENT_CREDENTIALS;
}
@Override
public Map<String, String> getAuthHeaders() {
// Headers will be set after token exchange by the executor
return Collections.emptyMap();
}
/**
* Gets the headers for the token request (not the API request).
*
* @return Headers for token endpoint
*/
public Map<String, String> getTokenRequestHeaders() {
if (clientAuthMethod == RestOAuth2ClientAuthMethod.HEADER) {
String credentials = clientId + ":" + clientSecret;
String encoded = Base64.getEncoder().encodeToString(credentials.getBytes());
return Map.of(
"Authorization", "Basic " + encoded,
"Content-Type", "application/x-www-form-urlencoded"
);
}
return Map.of("Content-Type", "application/x-www-form-urlencoded");
}
/**
* Gets the body parameters for the token request.
*
* @return Form parameters for token endpoint
*/
public String getTokenRequestBody() {
StringBuilder body = new StringBuilder("grant_type=client_credentials");
if (clientAuthMethod == RestOAuth2ClientAuthMethod.BODY) {
body.append("&client_id=").append(urlEncode(clientId));
body.append("&client_secret=").append(urlEncode(clientSecret));
}
if (scope != null && !scope.isBlank()) {
body.append("&scope=").append(urlEncode(scope));
}
if (audience != null && !audience.isBlank()) {
body.append("&audience=").append(urlEncode(audience));
}
return body.toString();
}
private static String urlEncode(String value) {
try {
return java.net.URLEncoder.encode(value, java.nio.charset.StandardCharsets.UTF_8);
} catch (Exception e) {
return value;
}
}
}
/**
* OAuth 2.0 Resource Owner Password authentication configuration.
*
* @param tokenUrl The token endpoint URL
* @param clientId The client ID
* @param clientSecret The client secret (optional for public clients)
* @param username The resource owner username
* @param password The resource owner password
* @param scope Optional scope(s) to request
*/
@JsonIgnoreProperties(ignoreUnknown = true)
record OAuth2Password(
String tokenUrl,
String clientId,
String clientSecret,
String username,
String password,
String scope
) implements RestAuthConfig {
public OAuth2Password {
tokenUrl = tokenUrl != null ? tokenUrl : "";
clientId = clientId != null ? clientId : "";
username = username != null ? username : "";
password = password != null ? password : "";
}
@Override
public RestAuthenticationType getAuthType() {
return RestAuthenticationType.OAUTH2_PASSWORD;
}
@Override
public Map<String, String> getAuthHeaders() {
return Collections.emptyMap();
}
/**
* Gets the body parameters for the token request.
*
* @return Form parameters for token endpoint
*/
public String getTokenRequestBody() {
StringBuilder body = new StringBuilder("grant_type=password");
body.append("&username=").append(urlEncode(username));
body.append("&password=").append(urlEncode(password));
body.append("&client_id=").append(urlEncode(clientId));
if (clientSecret != null && !clientSecret.isBlank()) {
body.append("&client_secret=").append(urlEncode(clientSecret));
}
if (scope != null && !scope.isBlank()) {
body.append("&scope=").append(urlEncode(scope));
}
return body.toString();
}
private static String urlEncode(String value) {
try {
return java.net.URLEncoder.encode(value, java.nio.charset.StandardCharsets.UTF_8);
} catch (Exception e) {
return value;
}
}
}
/**
* Custom authentication with user-defined headers.
*
* @param headers Custom headers to add to requests
*/
@JsonIgnoreProperties(ignoreUnknown = true)
record CustomAuth(Map<String, String> headers) implements RestAuthConfig {
public CustomAuth {
headers = headers != null ? Map.copyOf(headers) : Collections.emptyMap();
}
@Override
public RestAuthenticationType getAuthType() {
return RestAuthenticationType.CUSTOM;
}
@Override
public Map<String, String> getAuthHeaders() {
return headers;
}
}
}