diff --git a/components/org.wso2.carbon.identity.conditional.auth.functions.common/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/common/utils/ConfigProvider.java b/components/org.wso2.carbon.identity.conditional.auth.functions.common/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/common/utils/ConfigProvider.java index 6eb6306c..5cb8b445 100644 --- a/components/org.wso2.carbon.identity.conditional.auth.functions.common/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/common/utils/ConfigProvider.java +++ b/components/org.wso2.carbon.identity.conditional.auth.functions.common/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/common/utils/ConfigProvider.java @@ -31,6 +31,7 @@ import static org.wso2.carbon.identity.conditional.auth.functions.common.utils.Constants.HTTP_CONNECTION_TIMEOUT; import static org.wso2.carbon.identity.conditional.auth.functions.common.utils.Constants.HTTP_FUNCTION_ALLOWED_DOMAINS; import static org.wso2.carbon.identity.conditional.auth.functions.common.utils.Constants.HTTP_READ_TIMEOUT; +import static org.wso2.carbon.identity.conditional.auth.functions.common.utils.Constants.HTTP_REQUEST_RETRY_COUNT; public class ConfigProvider { @@ -39,6 +40,7 @@ public class ConfigProvider { private int connectionTimeout; private int readTimeout; private int connectionRequestTimeout; + private int requestRetryCount = 2; private List httpFunctionAllowedDomainList = new ArrayList<>(); private List choreoDomainList = new ArrayList<>(); private final String choreoTokenEndpoint; @@ -51,6 +53,7 @@ private ConfigProvider() { String connectionTimeoutString = IdentityUtil.getProperty(HTTP_CONNECTION_TIMEOUT); String readTimeoutString = IdentityUtil.getProperty(HTTP_READ_TIMEOUT); String connectionRequestTimeoutString = IdentityUtil.getProperty(HTTP_CONNECTION_REQUEST_TIMEOUT); + String requestRetryCountString = IdentityUtil.getProperty(HTTP_REQUEST_RETRY_COUNT); List httpFunctionAllowedDomainList = IdentityUtil.getPropertyAsList(HTTP_FUNCTION_ALLOWED_DOMAINS); List choreoDomainList = IdentityUtil.getPropertyAsList(CHOREO_DOMAINS); @@ -80,6 +83,15 @@ private ConfigProvider() { LOG.error("Error while parsing connection request timeout : " + connectionTimeoutString, e); } } + if (requestRetryCountString != null) { + try { + requestRetryCount = Integer.parseInt + (requestRetryCountString); + } catch (NumberFormatException e) { + LOG.error("Error while parsing max request attempts for api endpoint timeout : " + + requestRetryCountString, e); + } + } if (httpFunctionAllowedDomainList != null) { this.httpFunctionAllowedDomainList = httpFunctionAllowedDomainList; @@ -119,6 +131,11 @@ public int getConnectionRequestTimeout() { return connectionRequestTimeout; } + public int getRequestRetryCount() { + + return requestRetryCount; + } + public List getAllowedDomainsForHttpFunctions() { return httpFunctionAllowedDomainList; diff --git a/components/org.wso2.carbon.identity.conditional.auth.functions.common/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/common/utils/Constants.java b/components/org.wso2.carbon.identity.conditional.auth.functions.common/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/common/utils/Constants.java index 6d42cc7f..1a5fb7c9 100644 --- a/components/org.wso2.carbon.identity.conditional.auth.functions.common/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/common/utils/Constants.java +++ b/components/org.wso2.carbon.identity.conditional.auth.functions.common/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/common/utils/Constants.java @@ -26,6 +26,7 @@ public class Constants { public static final String RECEIVER_URL = "AdaptiveAuth.EventPublisher.ReceiverURL"; public static final String HTTP_CONNECTION_TIMEOUT = "AdaptiveAuth.HTTPConnectionTimeout"; + public static final String HTTP_REQUEST_RETRY_COUNT = "AdaptiveAuth.HTTPRequestRetryCount"; public static final String HTTP_READ_TIMEOUT = "AdaptiveAuth.HTTPReadTimeout"; public static final String HTTP_CONNECTION_REQUEST_TIMEOUT = "AdaptiveAuth.HTTPConnectionRequestTimeout"; public static final String AUTHENTICATION_ENABLED = "AdaptiveAuth.EventPublisher.BasicAuthentication.Enable"; @@ -46,4 +47,40 @@ public class Constants { public static final String HTTP_FUNCTION_ALLOWED_DOMAINS = "AdaptiveAuth.HTTPFunctionAllowedDomains.Domain"; public static final String CHOREO_DOMAINS = "AdaptiveAuth.ChoreoDomains.Domain"; public static final String CHOREO_TOKEN_ENDPOINT = "AdaptiveAuth.ChoreoTokenEndpoint"; + + /** + * Define logging constants. + */ + public static class LogConstants { + + public static final String ADAPTIVE_AUTH_SERVICE = "adaptive-auth-service"; + public static final String FAILED = "FAILED"; + + /** + * Define action IDs for diagnostic logs. + */ + public static class ActionIDs { + + public static final String RECEIVE_TOKEN = "receive-token"; + public static final String RECEIVE_API_RESPONSE = "receive-api-response"; + } + + /** + * Define common and reusable Input keys for diagnostic logs. + */ + public static class InputKeys { + + public static final String TOKEN_ENDPOINT = "token endpoint"; + public static final String API = "external api"; + } + + /** + * Define common and reusable Configuration keys for diagnostic logs. + */ + public static class ConfigKeys { + + public static final String SUPPORTED_GRANT_TYPES = "supported grant types"; + + } + } } diff --git a/components/org.wso2.carbon.identity.conditional.auth.functions.http/pom.xml b/components/org.wso2.carbon.identity.conditional.auth.functions.http/pom.xml index e8db92f5..39c788f7 100644 --- a/components/org.wso2.carbon.identity.conditional.auth.functions.http/pom.xml +++ b/components/org.wso2.carbon.identity.conditional.auth.functions.http/pom.xml @@ -103,6 +103,14 @@ msf4j-core test + + org.wso2.orbit.com.nimbusds + nimbus-jose-jwt + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.central.log.mgt + org.wso2.carbon.crypto org.wso2.carbon.crypto.impl @@ -176,10 +184,14 @@ org.wso2.carbon.identity.application.authentication.framework.config.model.graph; version="${carbon.identity.package.import.version.range}", org.wso2.carbon.identity.core.util; version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.central.log.mgt.*; version="${carbon.identity.package.import.version.range}", org.wso2.carbon.user.core; version="${carbon.kernel.package.import.version.range}", org.wso2.carbon.user.core.service; version="${carbon.kernel.package.import.version.range}", + org.wso2.carbon.utils;version="${carbon.kernel.package.import.version.range}", org.wso2.carbon.identity.conditional.auth.functions.common.utils, + com.nimbusds.jwt.*;version="${nimbusds.osgi.version.range}", org.wso2.carbon.identity.conditional.auth.functions.common.auth, + org.wso2.carbon.identity.core.cache; version="${carbon.identity.package.import.version.range}", diff --git a/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/AbstractHTTPFunction.java b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/AbstractHTTPFunction.java index 58fb58aa..b0971046 100644 --- a/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/AbstractHTTPFunction.java +++ b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/AbstractHTTPFunction.java @@ -18,6 +18,7 @@ package org.wso2.carbon.identity.conditional.auth.functions.http; import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.http.Header; @@ -33,8 +34,13 @@ import org.json.simple.parser.ParseException; import org.wso2.carbon.identity.application.authentication.framework.AsyncProcess; import org.wso2.carbon.identity.application.authentication.framework.config.model.graph.JsGraphBuilder; +import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; import org.wso2.carbon.identity.conditional.auth.functions.common.utils.ConfigProvider; import org.wso2.carbon.identity.conditional.auth.functions.common.utils.Constants; +import org.wso2.carbon.identity.conditional.auth.functions.http.util.AuthConfig; +import org.wso2.carbon.identity.conditional.auth.functions.http.util.AuthConfigFactory; +import org.wso2.carbon.identity.conditional.auth.functions.http.util.AuthConfigModel; +import org.wso2.carbon.utils.DiagnosticLog; import java.io.IOException; import java.net.SocketTimeoutException; @@ -56,12 +62,15 @@ public abstract class AbstractHTTPFunction { protected static final String TYPE_TEXT_PLAIN = "text/plain"; private static final char DOMAIN_SEPARATOR = '.'; private static final String RESPONSE = "response"; + private final int requestRetryCount; private final List allowedDomains; private CloseableHttpClient client; public AbstractHTTPFunction() { + requestRetryCount = ConfigProvider.getInstance(). + getRequestRetryCount(); RequestConfig config = RequestConfig.custom() .setConnectTimeout(ConfigProvider.getInstance().getConnectionTimeout()) .setConnectionRequestTimeout(ConfigProvider.getInstance().getConnectionRequestTimeout()) @@ -73,65 +82,247 @@ public AbstractHTTPFunction() { allowedDomains = ConfigProvider.getInstance().getAllowedDomainsForHttpFunctions(); } - protected void executeHttpMethod(HttpUriRequest request, Map eventHandlers) { + private enum RetryDecision { + RETRY, + NO_RETRY; + + public boolean shouldRetry() { + return this == RETRY; + } + } + + protected void executeHttpMethod(HttpUriRequest clientRequest, Map eventHandlers, + AuthConfigModel authConfigModel) { AsyncProcess asyncProcess = new AsyncProcess((context, asyncReturn) -> { - JSONObject json = null; - int responseCode; String outcome; String endpointURL = null; - if (request.getURI() != null) { - endpointURL = request.getURI().toString(); + HttpUriRequest request; + try { + if (authConfigModel != null) { + AuthConfig authConfig = AuthConfigFactory.getAuthConfig(authConfigModel, context, asyncReturn); + request = authConfig.applyAuth(clientRequest, authConfigModel); + } else { + request = clientRequest; + } + + if (request.getURI() != null) { + endpointURL = request.getURI().toString(); + } + + if (!isValidRequestDomain(request.getURI())) { + LOG.error("Request URL does not match with the allowed domain list. Request Url: " + + endpointURL); + asyncReturn.accept(context, Collections.emptyMap(), Constants.OUTCOME_FAIL); + } else { + Pair> result = executeRequest(request, endpointURL); + if (result.getLeft().shouldRetry()) { + LOG.info("Failed to invoke the endpoint. Url: " + endpointURL + ". Retrying the request."); + result = executeRequestWithRetries(request, endpointURL, requestRetryCount); + } + outcome = result.getRight().getLeft(); + JSONObject json = result.getRight().getRight(); + asyncReturn.accept(context, json != null ? json : Collections.emptyMap(), outcome); + } + } catch (Exception e) { + LOG.error("Error while applying authentication to the request.", e); + asyncReturn.accept(context, Collections.emptyMap(), Constants.OUTCOME_FAIL); } + }); + JsGraphBuilder.addLongWaitProcess(asyncProcess, eventHandlers); + } - if (!isValidRequestDomain(request.getURI())) { - outcome = Constants.OUTCOME_FAIL; - LOG.error("Provided Url does not contain a allowed domain. Invalid Url: " + endpointURL); - asyncReturn.accept(context, Collections.emptyMap(), outcome); - return; + /** + * Execute the request with retries. + * + * @param request HttpUriRequest. + * @param endpointURL Endpoint URL. + * @param maxRetries Maximum number of retries. + * @return Pair of outcome and json. + */ + private Pair> executeRequestWithRetries + (HttpUriRequest request, String endpointURL, int maxRetries) { + + Pair> result; + String outcome = Constants.OUTCOME_FAIL; + int attempts = 0; + RetryDecision isRetry = RetryDecision.NO_RETRY; + + while (attempts < maxRetries) { + attempts++; + LOG.warn("Retrying the request for endpoint: " + endpointURL + ". Attempt: " + attempts); + if (LoggerUtils.isDiagnosticLogsEnabled()) { + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new + DiagnosticLog.DiagnosticLogBuilder(Constants.LogConstants.ADAPTIVE_AUTH_SERVICE, + Constants.LogConstants.ActionIDs.RECEIVE_API_RESPONSE); + diagnosticLogBuilder.inputParam(Constants.LogConstants.InputKeys.API, endpointURL) + .resultMessage("Retrying the request for external api. Attempt: " + attempts) + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED); + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); } + result = executeRequest(request, endpointURL); + isRetry = result.getLeft(); + if (!isRetry.shouldRetry()) { + return result; + } + } + return Pair.of(isRetry, Pair.of(outcome, null)); + } - try (CloseableHttpResponse response = client.execute(request)) { - responseCode = response.getStatusLine().getStatusCode(); - if (responseCode >= 200 && responseCode < 300) { - outcome = Constants.OUTCOME_SUCCESS; - if (response.getEntity() != null) { - Header contentType = response.getEntity().getContentType(); - String jsonString = EntityUtils.toString(response.getEntity()); - if (contentType != null && contentType.getValue().contains(TYPE_TEXT_PLAIN)) { - // For 'text/plain', put the response body into the JSON object as a single field. - json = new JSONObject(); - json.put(RESPONSE, jsonString); - } else { - JSONParser parser = new JSONParser(); - json = (JSONObject) parser.parse(jsonString); - } + /** + * Execute the request. + * + * @param request HttpUriRequest. + * @param endpointURL Endpoint URL. + * @return Pair of outcome and json. + */ + private Pair> executeRequest(HttpUriRequest request, String endpointURL) { + + JSONObject json = null; + String outcome; + RetryDecision isRetry = RetryDecision.NO_RETRY; + + try (CloseableHttpResponse response = client.execute(request)) { + int responseCode = response.getStatusLine().getStatusCode(); + if (responseCode >= 200 && responseCode < 300) { + if (response.getEntity() != null) { + Header contentType = response.getEntity().getContentType(); + String jsonString = EntityUtils.toString(response.getEntity()); + if (contentType != null && contentType.getValue().contains(TYPE_TEXT_PLAIN)) { + json = new JSONObject(); + json.put(RESPONSE, jsonString); + } else { + JSONParser parser = new JSONParser(); + json = (JSONObject) parser.parse(jsonString); } - } else { - outcome = Constants.OUTCOME_FAIL; } - - } catch (IllegalArgumentException e) { - LOG.error("Invalid Url: " + endpointURL, e); + if (LoggerUtils.isDiagnosticLogsEnabled()) { + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new + DiagnosticLog.DiagnosticLogBuilder(Constants.LogConstants.ADAPTIVE_AUTH_SERVICE, + Constants.LogConstants.ActionIDs.RECEIVE_API_RESPONSE); + diagnosticLogBuilder.inputParam(Constants.LogConstants.InputKeys.API, endpointURL) + .resultMessage("Successfully called the external api. Status code: " + responseCode) + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.SUCCESS); + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); + } + LOG.info("Successfully called the external api. Status code: " + responseCode + ". Url: " + + endpointURL); + outcome = Constants.OUTCOME_SUCCESS; + return Pair.of(RetryDecision.NO_RETRY, Pair.of(outcome, json)); // Success, return immediately + } else if (responseCode >= 300 && responseCode < 400) { + if (LoggerUtils.isDiagnosticLogsEnabled()) { + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new + DiagnosticLog.DiagnosticLogBuilder(Constants.LogConstants.ADAPTIVE_AUTH_SERVICE, + Constants.LogConstants.ActionIDs.RECEIVE_API_RESPONSE); + diagnosticLogBuilder.inputParam(Constants.LogConstants.InputKeys.API, endpointURL) + .resultMessage("External api invocation returned a redirection. Status code: " + + responseCode) + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED); + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); + } + LOG.warn("External api invocation returned a redirection. Status code: " + + responseCode + ". Url: " + endpointURL); outcome = Constants.OUTCOME_FAIL; - } catch (ConnectTimeoutException e) { - LOG.error("Error while waiting to connect to " + endpointURL, e); - outcome = Constants.OUTCOME_TIMEOUT; - } catch (SocketTimeoutException e) { - LOG.error("Error while waiting for data from " + endpointURL, e); + return Pair.of(RetryDecision.NO_RETRY, Pair.of(outcome, null)); // Unauthorized, no retry + } else if (responseCode >= 400 && responseCode < 500) { + if (LoggerUtils.isDiagnosticLogsEnabled()) { + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new + DiagnosticLog.DiagnosticLogBuilder(Constants.LogConstants.ADAPTIVE_AUTH_SERVICE, + Constants.LogConstants.ActionIDs.RECEIVE_API_RESPONSE); + diagnosticLogBuilder.inputParam(Constants.LogConstants.InputKeys.API, endpointURL) + .resultMessage("External api invocation returned a client error. Status code: " + + responseCode) + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED); + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); + } + LOG.warn("External api invocation returned a client error. Status code: " + + responseCode + ". Url: " + endpointURL); + outcome = Constants.OUTCOME_FAIL; + return Pair.of(RetryDecision.NO_RETRY, Pair.of(outcome, null)); // Unauthorized, no retry + } else { + if (LoggerUtils.isDiagnosticLogsEnabled()) { + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new + DiagnosticLog.DiagnosticLogBuilder(Constants.LogConstants.ADAPTIVE_AUTH_SERVICE, + Constants.LogConstants.ActionIDs.RECEIVE_API_RESPONSE); + diagnosticLogBuilder.inputParam(Constants.LogConstants.InputKeys.API, endpointURL) + .resultMessage("Received unknown response from external API call. Status code: " + + responseCode) + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED); + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); + } + LOG.error("Received unknown response from external API call. Status code: " + + responseCode + ". Url: " + endpointURL); + outcome = Constants.OUTCOME_FAIL; + return Pair.of(RetryDecision.RETRY, Pair.of(outcome, null)); // Server error, retry if attempts left + } + } catch (Exception e) { + // Log the error based on its type + if (e instanceof IllegalArgumentException) { + if (LoggerUtils.isDiagnosticLogsEnabled()) { + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new + DiagnosticLog.DiagnosticLogBuilder(Constants.LogConstants.ADAPTIVE_AUTH_SERVICE, + Constants.LogConstants.ActionIDs.RECEIVE_API_RESPONSE); + diagnosticLogBuilder.inputParam(Constants.LogConstants.InputKeys.API, endpointURL) + .resultMessage("Invalid Url for external API call.") + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED); + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); + } + outcome = Constants.OUTCOME_FAIL; + LOG.error("Invalid Url: " + endpointURL, e); + } else if (e instanceof ConnectTimeoutException || e instanceof SocketTimeoutException) { + if (LoggerUtils.isDiagnosticLogsEnabled()) { + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new + DiagnosticLog.DiagnosticLogBuilder(Constants.LogConstants.ADAPTIVE_AUTH_SERVICE, + Constants.LogConstants.ActionIDs.RECEIVE_API_RESPONSE); + diagnosticLogBuilder.inputParam(Constants.LogConstants.InputKeys.API, endpointURL) + .resultMessage("Request for the external API timed out.") + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED); + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); + } + isRetry = RetryDecision.RETRY; // Timeout, retry if attempts left outcome = Constants.OUTCOME_TIMEOUT; - } catch (IOException e) { + LOG.error("Error while waiting to connect to " + endpointURL, e); + } else if (e instanceof IOException) { + outcome = Constants.OUTCOME_FAIL; LOG.error("Error while calling endpoint. ", e); + } else if (e instanceof ParseException) { + if (LoggerUtils.isDiagnosticLogsEnabled()) { + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new + DiagnosticLog.DiagnosticLogBuilder(Constants.LogConstants.ADAPTIVE_AUTH_SERVICE, + Constants.LogConstants.ActionIDs.RECEIVE_API_RESPONSE); + diagnosticLogBuilder.inputParam(Constants.LogConstants.InputKeys.API, endpointURL) + .resultMessage("Failed to parse the response from the external API.") + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED); + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); + } outcome = Constants.OUTCOME_FAIL; - } catch (ParseException e) { LOG.error("Error while parsing response. ", e); + } else { + if (LoggerUtils.isDiagnosticLogsEnabled()) { + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new + DiagnosticLog.DiagnosticLogBuilder(Constants.LogConstants.ADAPTIVE_AUTH_SERVICE, + Constants.LogConstants.ActionIDs.RECEIVE_API_RESPONSE); + diagnosticLogBuilder.inputParam(Constants.LogConstants.InputKeys.API, endpointURL) + .resultMessage("Received an error while invoking the external API.") + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED); + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); + } outcome = Constants.OUTCOME_FAIL; + LOG.error("Error while calling endpoint. ", e); } - - asyncReturn.accept(context, json != null ? json : Collections.emptyMap(), outcome); - }); - JsGraphBuilder.addLongWaitProcess(asyncProcess, eventHandlers); + } + // Return the outcome and json (which might be null if never successful) + return Pair.of(isRetry, Pair.of(outcome, json)); } private boolean isValidRequestDomain(URI url) { @@ -218,4 +409,21 @@ protected void setHeaders(HttpUriRequest request, Map headers) { .filter(entry -> StringUtils.isNotBlank(entry.getKey()) && !entry.getKey().equals("null")) .forEach(entry -> request.setHeader(entry.getKey(), entry.getValue())); } + + /** + * Get AuthConfigModel from the map. + * + * @param map Map of properties. + * @return AuthConfigModel. + */ + protected AuthConfigModel getAuthConfigModel(Map map) { + AuthConfigModel authConfig; + if (map.get("type") == null || map.get("properties") == null) { + throw new IllegalArgumentException("Invalid argument type. Expected {type: string, properties: map}"); + } + String type = (String) map.get("type"); + Map propertiesMap = (Map) map.get("properties"); + authConfig = new AuthConfigModel(type, propertiesMap); + return authConfig; + } } diff --git a/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/HTTPGetFunctionImpl.java b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/HTTPGetFunctionImpl.java index 5d2016c3..f8aa1fb9 100644 --- a/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/HTTPGetFunctionImpl.java +++ b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/HTTPGetFunctionImpl.java @@ -18,16 +18,14 @@ package org.wso2.carbon.identity.conditional.auth.functions.http; -import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.http.client.methods.HttpGet; +import org.wso2.carbon.identity.conditional.auth.functions.http.util.AuthConfigModel; import java.util.HashMap; import java.util.Map; -import static org.apache.http.HttpHeaders.ACCEPT; - /** * Implementation of the {@link HTTPGetFunction} */ @@ -45,6 +43,7 @@ public void httpGet(String endpointURL, Object... params) { Map eventHandlers; Map headers = new HashMap<>(); + AuthConfigModel authConfig = null; switch (params.length) { case 1: @@ -64,14 +63,25 @@ public void httpGet(String endpointURL, Object... params) { "and eventHandlers (Map) respectively."); } break; + case 3: + if (params[0] instanceof Map && params[1] instanceof Map && params[2] instanceof Map) { + headers = validateHeaders((Map) params[0]); + authConfig = getAuthConfigModel((Map) params[1]); + eventHandlers = (Map) params[2]; + } else { + throw new IllegalArgumentException("Invalid argument type. Expected " + + "headers (Map), authConfig (Map)," + + " and eventHandlers (Map) respectively."); + } + break; default: - throw new IllegalArgumentException("Invalid number of arguments. Expected 1 or 2, but got: " + + throw new IllegalArgumentException("Invalid number of arguments. Expected 1, 2 or 3, but got: " + params.length + "."); } HttpGet request = new HttpGet(endpointURL); setHeaders(request, headers); - executeHttpMethod(request, eventHandlers); + executeHttpMethod(request, eventHandlers, authConfig); } } diff --git a/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/HTTPPostFunctionImpl.java b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/HTTPPostFunctionImpl.java index f8becd3f..e5f7ab9a 100644 --- a/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/HTTPPostFunctionImpl.java +++ b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/HTTPPostFunctionImpl.java @@ -19,7 +19,6 @@ package org.wso2.carbon.identity.conditional.auth.functions.http; import org.apache.commons.collections.MapUtils; -import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.http.NameValuePair; @@ -28,6 +27,7 @@ import org.apache.http.entity.StringEntity; import org.apache.http.message.BasicNameValuePair; import org.json.simple.JSONObject; +import org.wso2.carbon.identity.conditional.auth.functions.http.util.AuthConfigModel; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -35,7 +35,6 @@ import java.util.List; import java.util.Map; -import static org.apache.http.HttpHeaders.ACCEPT; import static org.apache.http.HttpHeaders.CONTENT_TYPE; @@ -48,7 +47,7 @@ public class HTTPPostFunctionImpl extends AbstractHTTPFunction implements HTTPPo public HTTPPostFunctionImpl() { - super(); + super(); } @Override @@ -57,6 +56,7 @@ public void httpPost(String endpointURL, Object... params) { Map eventHandlers; Map payloadData = new HashMap<>(); Map headers = new HashMap<>(); + AuthConfigModel authConfig = null; switch (params.length) { case 1: @@ -87,8 +87,20 @@ public void httpPost(String endpointURL, Object... params) { "(Map) respectively."); } break; + case 4: + if (params[0] instanceof Map && params[1] instanceof Map && params[2] instanceof Map && params[3] instanceof Map) { + payloadData = (Map) params[0]; + headers = validateHeaders((Map) params[1]); + authConfig = getAuthConfigModel((Map) params[2]); + eventHandlers = (Map) params[3]; + } else { + throw new IllegalArgumentException("Invalid argument type. Expected payloadData " + + "(Map), headers (Map), authConfig (Map)," + + " and eventHandlers (Map) respectively."); + } + break; default: - throw new IllegalArgumentException("Invalid number of arguments. Expected 1, 2, or 3. Found: " + throw new IllegalArgumentException("Invalid number of arguments. Expected 1, 2, 3, or 4. Found: " + params.length + "."); } @@ -114,6 +126,7 @@ public void httpPost(String endpointURL, Object... params) { request.setEntity(new StringEntity(jsonObject.toJSONString(), StandardCharsets.UTF_8)); } } - executeHttpMethod(request, eventHandlers); + + executeHttpMethod(request, eventHandlers, authConfig); } } diff --git a/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/cache/APIAccessTokenCache.java b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/cache/APIAccessTokenCache.java new file mode 100644 index 00000000..705947c9 --- /dev/null +++ b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/cache/APIAccessTokenCache.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.wso2.carbon.identity.conditional.auth.functions.http.cache; + +import org.wso2.carbon.identity.core.cache.BaseCache; + +/** + * The cache implementation which stores the access tokens received from API. + */ +public class APIAccessTokenCache extends BaseCache { + + private static final String ACCESS_TOKEN_CACHE_NAME = "APIAccessTokenCache"; + + private APIAccessTokenCache() { + + super(ACCESS_TOKEN_CACHE_NAME); + } + + private static class AccessTokenCacheHolder { + static final APIAccessTokenCache INSTANCE = new APIAccessTokenCache(); + } + + public static APIAccessTokenCache getInstance() { + return AccessTokenCacheHolder.INSTANCE; + } + +} diff --git a/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/util/ApiKeyAuthConfig.java b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/util/ApiKeyAuthConfig.java new file mode 100644 index 00000000..09f02f27 --- /dev/null +++ b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/util/ApiKeyAuthConfig.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.wso2.carbon.identity.conditional.auth.functions.http.util; + +import org.apache.http.client.methods.HttpUriRequest; + +import java.util.Map; + +/** + * Implementation of the {@link AuthConfig} + * This class is used to configure the API key authentication. + * The API key is added to the request header. + */ +public class ApiKeyAuthConfig implements AuthConfig { + private String headerName = "X-API-KEY"; // Default header name + private String apiKey; + private static final String HEADER_NAME_VARIABLE_NAME = "headerName"; + private static final String API_KEY_VARIABLE_NAME = "apiKey"; + + public void setHeaderName(String headerName) { + this.headerName = headerName; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + public String getHeaderName() { + return headerName; + } + + public String getApiKey() { + return apiKey; + } + + @Override + public HttpUriRequest applyAuth(HttpUriRequest request, AuthConfigModel authConfigModel) { + + Map properties = authConfigModel.getProperties(); + setApiKey(properties.get(API_KEY_VARIABLE_NAME).toString()); + setHeaderName(properties.get(HEADER_NAME_VARIABLE_NAME).toString()); + request.addHeader(getHeaderName(), getApiKey()); + return request; + } +} diff --git a/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/util/AuthConfig.java b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/util/AuthConfig.java new file mode 100644 index 00000000..43ff420e --- /dev/null +++ b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/util/AuthConfig.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.wso2.carbon.identity.conditional.auth.functions.http.util; + +import org.apache.http.client.methods.HttpUriRequest; + +/** + * Interface for the authentication configurations. + */ +public interface AuthConfig { + + /** + * Apply the authentication configurations to the request. + * + * @param request HttpUriRequest + * @param authConfigModel Authentication configuration model + * @return HttpUriRequest + * @throws Exception + */ + HttpUriRequest applyAuth(HttpUriRequest request, AuthConfigModel authConfigModel) throws Exception; +} diff --git a/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/util/AuthConfigFactory.java b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/util/AuthConfigFactory.java new file mode 100644 index 00000000..cf3a934f --- /dev/null +++ b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/util/AuthConfigFactory.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.wso2.carbon.identity.conditional.auth.functions.http.util; + +import org.wso2.carbon.identity.application.authentication.framework.AsyncReturn; +import org.wso2.carbon.identity.application.authentication.framework.context.AuthenticationContext; + +/** + * Factory class to create the authentication configurations. + * This class is used to create the authentication configurations based on the authentication type. + * The supported authentication types are: + * 1. ClientCredential + * 2. BearerToken + * 3. ApiKey + * 4. BasicAuth + */ +public class AuthConfigFactory { + + /** + * Create the authentication configuration based on the authentication type. + * + * @param authConfigModel Authentication configuration model + * @param authenticationContext Authentication context + * @param asyncReturn AsyncReturn + * @return AuthConfig + */ + public static AuthConfig getAuthConfig(AuthConfigModel authConfigModel, + AuthenticationContext authenticationContext, AsyncReturn asyncReturn) { + + switch (authConfigModel.getType().toLowerCase()) { + case "clientcredential": + ClientCredentialAuthConfig clientCredentialAuthConfig = new ClientCredentialAuthConfig(); + clientCredentialAuthConfig.setAuthenticationContext(authenticationContext); + clientCredentialAuthConfig.setAsyncReturn(asyncReturn); + return clientCredentialAuthConfig; + case "bearertoken": + return new BearerTokenAuthConfig(); + case "apikey": + return new ApiKeyAuthConfig(); + case "basicauth": + return new BasicAuthConfig(); + default: + throw new IllegalArgumentException("Unsupported authentication type: " + authConfigModel.getType()); + } + } +} diff --git a/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/util/AuthConfigModel.java b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/util/AuthConfigModel.java new file mode 100644 index 00000000..e9209905 --- /dev/null +++ b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/util/AuthConfigModel.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.wso2.carbon.identity.conditional.auth.functions.http.util; + +import java.util.Map; + +/** + * Model class for the authentication configuration. + */ +public class AuthConfigModel { + String type; + private Map properties; + + public AuthConfigModel(String type, Map properties) { + this.type = type; + this.properties = properties; + } + + public void setType(String type) { + this.type = type; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + + public String getType() { + return type; + } + + public Map getProperties() { + return properties; + } +} diff --git a/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/util/BasicAuthConfig.java b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/util/BasicAuthConfig.java new file mode 100644 index 00000000..c4f79188 --- /dev/null +++ b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/util/BasicAuthConfig.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.wso2.carbon.identity.conditional.auth.functions.http.util; + +import org.apache.http.client.methods.HttpUriRequest; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; + +/** + * Implementation of the {@link AuthConfig} + * This class is used to configure the basic authentication. + * The username and password are added to the request header. + */ +public class BasicAuthConfig implements AuthConfig { + private String username; + private String password; + private static final String USERNAME_VARIABLE_NAME = "username"; + private static final String PASSWORD_VARIABLE_NAME = "password"; + + public void setUsername(String username) { + this.username = username; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + @Override + public HttpUriRequest applyAuth(HttpUriRequest request, AuthConfigModel authConfigModel) { + + Map properties = authConfigModel.getProperties(); + setUsername(properties.get(USERNAME_VARIABLE_NAME).toString()); + setPassword(properties.get(PASSWORD_VARIABLE_NAME).toString()); + String auth = getUsername() + ":" + getPassword(); + String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8)); + request.addHeader("Authorization", "Basic " + encodedAuth); + + return request; + } +} diff --git a/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/util/BearerTokenAuthConfig.java b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/util/BearerTokenAuthConfig.java new file mode 100644 index 00000000..be614d5c --- /dev/null +++ b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/util/BearerTokenAuthConfig.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.wso2.carbon.identity.conditional.auth.functions.http.util; + +import org.apache.http.client.methods.HttpUriRequest; + +import java.util.Map; + +/** + * Implementation of the {@link AuthConfig} + * This class is used to configure the bearer token authentication. + * The bearer token is added to the request header. + */ +public class BearerTokenAuthConfig implements AuthConfig { + private String token; + + public void setToken(String token) { + this.token = token; + } + + public String getToken() { + return token; + } + + @Override + public HttpUriRequest applyAuth(HttpUriRequest request, AuthConfigModel authConfigModel) { + + Map properties = authConfigModel.getProperties(); + setToken(properties.get("token").toString()); + request.setHeader("Authorization", "Bearer " + getToken()); + return request; + } +} diff --git a/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/util/ClientCredentialAuthConfig.java b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/util/ClientCredentialAuthConfig.java new file mode 100644 index 00000000..8217c0d1 --- /dev/null +++ b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/main/java/org/wso2/carbon/identity/conditional/auth/functions/http/util/ClientCredentialAuthConfig.java @@ -0,0 +1,464 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.wso2.carbon.identity.conditional.auth.functions.http.util; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.nimbusds.jwt.SignedJWT; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.conn.ConnectTimeoutException; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.wso2.carbon.identity.application.authentication.framework.AsyncReturn; +import org.wso2.carbon.identity.application.authentication.framework.context.AuthenticationContext; +import org.wso2.carbon.identity.application.authentication.framework.exception.FrameworkException; +import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; +import org.wso2.carbon.identity.conditional.auth.functions.common.utils.ConfigProvider; +import org.wso2.carbon.identity.conditional.auth.functions.common.utils.Constants; +import org.wso2.carbon.identity.conditional.auth.functions.http.cache.APIAccessTokenCache; +import org.wso2.carbon.utils.DiagnosticLog; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.net.SocketTimeoutException; +import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import static org.apache.http.HttpHeaders.ACCEPT; +import static org.apache.http.HttpHeaders.CONTENT_TYPE; +import static org.wso2.carbon.identity.conditional.auth.functions.common.utils.Constants.OUTCOME_FAIL; + +/** + * Implementation of the {@link AuthConfig} + * This class is used to configure the client credential authentication. + * The client credential is used to request the access token from the token endpoint. + */ +public class ClientCredentialAuthConfig implements AuthConfig { + + private static final Log LOG = LogFactory.getLog(ClientCredentialAuthConfig.class); + private static final String TYPE_APPLICATION_JSON = "application/json"; + private static final String TYPE_FORM_DATA = "application/x-www-form-urlencoded"; + private static final String AUTHORIZATION = "Authorization"; + private static final String GRANT_TYPE = "grant_type"; + private static final String GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials"; + private static final Gson GSON = new GsonBuilder().create(); + private static final String CONSUMER_KEY_VARIABLE_NAME = "consumerKey"; + private static final String CONSUMER_SECRET_VARIABLE_NAME = "consumerSecret"; + private static final String TOKEN_ENDPOINT = "tokenEndpoint"; + private static final String SCOPES = "scope"; + private static final String ACCESS_TOKEN_KEY = "access_token"; + private static final String JWT_EXP_CLAIM = "exp"; + private static final String BEARER = "Bearer "; + private static final String BASIC = "Basic "; + private int maxRequestAttemptsForAPIEndpointTimeout; + private APIAccessTokenCache apiAccessTokenCache; + private String consumerKey; + private String consumerSecret; + private String scopes; + private String tokenEndpoint; + private AuthenticationContext authenticationContext; + private AsyncReturn asyncReturn; + + public void setAuthenticationContext(AuthenticationContext authenticationContext) { + this.authenticationContext = authenticationContext; + } + + public void setAsyncReturn(AsyncReturn asyncReturn) { + this.asyncReturn = asyncReturn; + } + + public void setConsumerKey(String consumerKey) { + this.consumerKey = consumerKey; + } + + public void setConsumerSecret(String consumerSecret) { + this.consumerSecret = consumerSecret; + } + + public void setTokenEndpoint(String tokenEndpoint) { + this.tokenEndpoint = tokenEndpoint; + } + + public void setScopes(String scopes) { + this.scopes = scopes; + } + + public String getTokenEndpoint() { + return tokenEndpoint; + } + + public String getConsumerKey() { + return consumerKey; + } + + public String getConsumerSecret() { + return consumerSecret; + } + + public String getScopes() { + return scopes; + } + + private enum RetryDecision { + RETRY, + NO_RETRY; + + public boolean shouldRetry() { + return this == RETRY; + } + } + + @Override + public HttpUriRequest applyAuth(HttpUriRequest request, AuthConfigModel authConfigModel) + throws FrameworkException { + + maxRequestAttemptsForAPIEndpointTimeout = ConfigProvider.getInstance(). + getRequestRetryCount(); + this.apiAccessTokenCache = APIAccessTokenCache.getInstance(); + Map properties = authConfigModel.getProperties(); + validateRequiredProperties(properties); + + setConsumerKey(properties.get(CONSUMER_KEY_VARIABLE_NAME).toString()); + setConsumerSecret(properties.get(CONSUMER_SECRET_VARIABLE_NAME).toString()); + setTokenEndpoint(properties.get(TOKEN_ENDPOINT).toString()); + setScopes(properties.containsKey(SCOPES) ? properties.get(SCOPES).toString() : null); + + String accessToken = getAccessToken(); + if (accessToken == null) { + asyncReturn.accept(authenticationContext, Collections.emptyMap(), OUTCOME_FAIL); + if (LoggerUtils.isDiagnosticLogsEnabled()) { + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new + DiagnosticLog.DiagnosticLogBuilder(Constants.LogConstants.ADAPTIVE_AUTH_SERVICE, + Constants.LogConstants.ActionIDs.RECEIVE_TOKEN); + diagnosticLogBuilder.inputParam(Constants.LogConstants.InputKeys.TOKEN_ENDPOINT, getTokenEndpoint()) + .configParam(Constants.LogConstants.ConfigKeys.SUPPORTED_GRANT_TYPES, + GRANT_TYPE_CLIENT_CREDENTIALS) + .resultMessage("Failed to retrieve access token for the provided token endpoint.") + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED); + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); + } + LOG.error("Failed to retrieve access token. Aborting request."); + throw new FrameworkException("Failed to retrieve access token."); + } + request.setHeader(AUTHORIZATION, BEARER + accessToken); + return request; + } + + /** + * This method decodes access token and compare its expiry time with the current time to decide whether it's + * expired. + * + * @param accessToken Access token which needs to be evaluated + * @return A boolean value indicating whether the token is expired + * @throws ParseException {@link ParseException} + */ + private boolean isTokenExpired(String accessToken) throws ParseException { + + SignedJWT decodedToken = SignedJWT.parse(accessToken); + Date expiryDate = (Date) decodedToken.getJWTClaimsSet().getClaim(JWT_EXP_CLAIM); + LocalDateTime expiryTimestamp = LocalDateTime.ofInstant(expiryDate.toInstant(), ZoneId.systemDefault()); + return LocalDateTime.now().isAfter(expiryTimestamp); + } + + /** + * This method is used to get the access token from the token endpoint. + * + * @return Access token + * @throws FrameworkException {@link FrameworkException} + */ + private void validateRequiredProperties(Map properties) throws FrameworkException { + + if (!properties.containsKey(CONSUMER_KEY_VARIABLE_NAME) || + !properties.containsKey(CONSUMER_SECRET_VARIABLE_NAME) || + !properties.containsKey(TOKEN_ENDPOINT)) { + asyncReturn.accept(authenticationContext, Collections.emptyMap(), OUTCOME_FAIL); + LOG.error("Required properties not defined. Aborting token request."); + throw new FrameworkException("Missing required properties."); + } + } + + /** + * This method is used to get the access token from the cache or request a new token from the token endpoint. + * + * @return Access token + * @throws FrameworkException {@link FrameworkException} + */ + private String getAccessToken() throws FrameworkException { + String accessToken = apiAccessTokenCache.getValueFromCache(getConsumerKey(), + authenticationContext.getTenantDomain()); + try { + if (StringUtils.isNotEmpty(accessToken) && !isTokenExpired(accessToken)) { + LOG.debug("Unexpired access token available in cache."); + return accessToken; + } else { + // Attempt the first request for an access token + LOG.info("Attempting initial access token request for session data key: " + + authenticationContext.getContextIdentifier()); + Pair retryDecision = requestAccessToken(); + if (retryDecision.getLeft().shouldRetry()) { + return attemptAccessTokenRequest(maxRequestAttemptsForAPIEndpointTimeout); + } else { + return retryDecision.getRight(); + } + } + } catch (ParseException e) { + LOG.error("Error parsing token expiry.", e); + if (LoggerUtils.isDiagnosticLogsEnabled()) { + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new + DiagnosticLog.DiagnosticLogBuilder(Constants.LogConstants.ADAPTIVE_AUTH_SERVICE, + Constants.LogConstants.ActionIDs.RECEIVE_API_RESPONSE); + diagnosticLogBuilder.inputParam(Constants.LogConstants.InputKeys.TOKEN_ENDPOINT, getTokenEndpoint()) + .resultMessage("Failed to parse token expiry.") + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED); + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); + } + LOG.error("Error parsing token expiry.", e); + asyncReturn.accept(authenticationContext, Collections.emptyMap(), OUTCOME_FAIL); + } catch (IOException e) { + LOG.error("Error while calling token endpoint. ", e); + } + return null; + } + + /** + * This method is used to attempt the access token request from the token endpoint. + * + * @param maxAttempts Maximum number of attempts to request the access token + * @return Access token + */ + private String attemptAccessTokenRequest(int maxAttempts) { + + int attemptCount = 0; + + while (attemptCount < maxAttempts) { + + try { + if (LoggerUtils.isDiagnosticLogsEnabled()) { + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new + DiagnosticLog.DiagnosticLogBuilder(Constants.LogConstants.ADAPTIVE_AUTH_SERVICE, + Constants.LogConstants.ActionIDs.RECEIVE_TOKEN); + diagnosticLogBuilder.inputParam(Constants.LogConstants.InputKeys.TOKEN_ENDPOINT, getTokenEndpoint()) + .configParam(Constants.LogConstants.ConfigKeys.SUPPORTED_GRANT_TYPES, + GRANT_TYPE_CLIENT_CREDENTIALS) + .resultMessage("Retrying token request for the provided token endpoint. Attempt: " + + attemptCount + ".") + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED); + attemptCount++; LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); + } + LOG.info("Retrying token request for session data key: " + + this.authenticationContext.getContextIdentifier() + ". Attempt: " + attemptCount); + Pair retryDecision = requestAccessToken(); + if (!retryDecision.getLeft().shouldRetry()) { + return retryDecision.getRight(); + } + } catch (IOException e) { + LOG.error("Error while calling token endpoint. ", e); + } + attemptCount++; + } + + LOG.warn("Maximum token request attempts reached."); + return null; + } + + /** + * This method is used to request the access token from the token endpoint. + * + * @return Access token + * @throws IOException {@link IOException} + */ + private Pair requestAccessToken() throws IOException { + + RetryDecision isRetry = RetryDecision.NO_RETRY; + HttpPost request = new HttpPost(tokenEndpoint); + request.setHeader(ACCEPT, TYPE_APPLICATION_JSON); + request.setHeader(CONTENT_TYPE, TYPE_FORM_DATA); + + request.setHeader(AUTHORIZATION, BASIC + Base64.getEncoder() + .encodeToString((getConsumerKey() + ":" + getConsumerSecret()) + .getBytes(StandardCharsets.UTF_8))); + + List bodyParams = new ArrayList<>(); + bodyParams.add(new BasicNameValuePair(GRANT_TYPE, GRANT_TYPE_CLIENT_CREDENTIALS)); + if (StringUtils.isNotEmpty(getScopes())) { + bodyParams.add(new BasicNameValuePair(SCOPES, getScopes())); + } + request.setEntity(new UrlEncodedFormEntity(bodyParams)); + + RequestConfig config = RequestConfig.custom() + .setConnectTimeout(ConfigProvider.getInstance().getConnectionTimeout()) + .setConnectionRequestTimeout(ConfigProvider.getInstance().getConnectionRequestTimeout()) + .setSocketTimeout(ConfigProvider.getInstance().getReadTimeout()) + .setRedirectsEnabled(false) + .setRelativeRedirectsAllowed(false) + .build(); + + try (CloseableHttpClient client = HttpClientBuilder.create().setDefaultRequestConfig(config).build(); + CloseableHttpResponse response = client.execute(request)) { + + int responseCode = response.getStatusLine().getStatusCode(); + if (responseCode >= 200 && responseCode < 300) { + return processSuccessfulResponse(response); + } else if (responseCode >= 300 && responseCode < 400) { + if (LoggerUtils.isDiagnosticLogsEnabled()) { + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new + DiagnosticLog.DiagnosticLogBuilder(Constants.LogConstants.ADAPTIVE_AUTH_SERVICE, + Constants.LogConstants.ActionIDs.RECEIVE_API_RESPONSE); + diagnosticLogBuilder.inputParam(Constants.LogConstants.InputKeys.TOKEN_ENDPOINT, getTokenEndpoint()) + .resultMessage("Token endpoint returned a redirection. Status code: " + responseCode) + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED); + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); + } + LOG.warn("Token endpoint returned a redirection. Status code: " + responseCode + ". Url: " + + tokenEndpoint); + return Pair.of(RetryDecision.NO_RETRY, null); + } else if (responseCode >= 400 && responseCode < 500) { + if (LoggerUtils.isDiagnosticLogsEnabled()) { + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new + DiagnosticLog.DiagnosticLogBuilder(Constants.LogConstants.ADAPTIVE_AUTH_SERVICE, + Constants.LogConstants.ActionIDs.RECEIVE_API_RESPONSE); + diagnosticLogBuilder.inputParam(Constants.LogConstants.InputKeys.TOKEN_ENDPOINT, getTokenEndpoint()) + .resultMessage("Token endpoint returned a client error. Status code: " + responseCode) + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED); + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); + } + LOG.warn("Token endpoint returned a client error. Status code: " + responseCode + ". Url: " + + tokenEndpoint); + return Pair.of(RetryDecision.NO_RETRY, null); + } else { + if (LoggerUtils.isDiagnosticLogsEnabled()) { + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new + DiagnosticLog.DiagnosticLogBuilder(Constants.LogConstants.ADAPTIVE_AUTH_SERVICE, + Constants.LogConstants.ActionIDs.RECEIVE_API_RESPONSE); + diagnosticLogBuilder.inputParam(Constants.LogConstants.InputKeys.TOKEN_ENDPOINT, getTokenEndpoint()) + .resultMessage("Received unknown response from token endpoint. Status code: " + + responseCode) + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED); + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); + } + LOG.error("Received unknown response from token endpoint. Status code: " + responseCode + ". Url: " + + tokenEndpoint); + return Pair.of(RetryDecision.RETRY, null); // Server error, retry if attempts left + } + } catch (Exception e) { + // Log the error based on its type + if (e instanceof IllegalArgumentException) { + if (LoggerUtils.isDiagnosticLogsEnabled()) { + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new + DiagnosticLog.DiagnosticLogBuilder(Constants.LogConstants.ADAPTIVE_AUTH_SERVICE, + Constants.LogConstants.ActionIDs.RECEIVE_API_RESPONSE); + diagnosticLogBuilder.inputParam(Constants.LogConstants.InputKeys.TOKEN_ENDPOINT, getTokenEndpoint()) + .resultMessage("Invalid Url for token endpoint.") + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED); + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); + } + LOG.error("Invalid Url: " + tokenEndpoint, e); + } else if (e instanceof SocketTimeoutException || e instanceof ConnectTimeoutException) { + if (LoggerUtils.isDiagnosticLogsEnabled()) { + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new + DiagnosticLog.DiagnosticLogBuilder(Constants.LogConstants.ADAPTIVE_AUTH_SERVICE, + Constants.LogConstants.ActionIDs.RECEIVE_API_RESPONSE); + diagnosticLogBuilder.inputParam(Constants.LogConstants.InputKeys.TOKEN_ENDPOINT, getTokenEndpoint()) + .resultMessage("Request for the token endpoint timed out.") + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED); + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); + } + isRetry = RetryDecision.RETRY; // Timeout, retry if attempts left + LOG.error("Error while waiting to connect to " + tokenEndpoint, e); + } else if (e instanceof IOException) { + LOG.error("Error while calling token endpoint. ", e); + } else { + if (LoggerUtils.isDiagnosticLogsEnabled()) { + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new + DiagnosticLog.DiagnosticLogBuilder(Constants.LogConstants.ADAPTIVE_AUTH_SERVICE, + Constants.LogConstants.ActionIDs.RECEIVE_API_RESPONSE); + diagnosticLogBuilder.inputParam(Constants.LogConstants.InputKeys.TOKEN_ENDPOINT, getTokenEndpoint()) + .resultMessage("Received an error while invoking the token endpoint.") + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED); + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); + } + LOG.error("Error while calling token endpoint. ", e); + } + } + return Pair.of(isRetry, null); + } + + /** + * This method is used to process the successful response from the token endpoint. + * + * @param response {@link CloseableHttpResponse} + * @return Access token + * @throws IOException {@link IOException} + */ + private Pair processSuccessfulResponse(CloseableHttpResponse response) throws IOException { + + Type responseBodyType = new TypeToken>(){}.getType(); + Map responseBody = GSON.fromJson(EntityUtils.toString(response.getEntity()), responseBodyType); + String accessToken = responseBody.get(ACCESS_TOKEN_KEY); + + if (accessToken != null) { + + if (LoggerUtils.isDiagnosticLogsEnabled()) { + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new + DiagnosticLog.DiagnosticLogBuilder(Constants.LogConstants.ADAPTIVE_AUTH_SERVICE, + Constants.LogConstants.ActionIDs.RECEIVE_API_RESPONSE); + diagnosticLogBuilder.inputParam(Constants.LogConstants.InputKeys.TOKEN_ENDPOINT, getTokenEndpoint()) + .resultMessage("Received access token from the token endpoint.") + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.SUCCESS); + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); + } + LOG.info("Received access token from the token endpoint. Session data key: " + + authenticationContext.getContextIdentifier()); + apiAccessTokenCache.addToCache(getConsumerKey(), accessToken, + this.authenticationContext.getTenantDomain()); + return Pair.of(RetryDecision.NO_RETRY, accessToken); + } + LOG.error("Token response does not contain an access token. Session data key: " + + authenticationContext.getContextIdentifier()); + return Pair.of(RetryDecision.NO_RETRY, null); + } +} diff --git a/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/test/java/org/wso2/carbon/identity/conditional/auth/functions/http/HTTPGetFunctionImplTest.java b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/test/java/org/wso2/carbon/identity/conditional/auth/functions/http/HTTPGetFunctionImplTest.java index 32929d19..0087c4b0 100644 --- a/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/test/java/org/wso2/carbon/identity/conditional/auth/functions/http/HTTPGetFunctionImplTest.java +++ b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/test/java/org/wso2/carbon/identity/conditional/auth/functions/http/HTTPGetFunctionImplTest.java @@ -66,6 +66,7 @@ public class HTTPGetFunctionImplTest extends JsSequenceHandlerAbstractTest { private static final String TEST_SP_CONFIG = "http-get-test-sp.xml"; private static final String TEST_HEADERS = "http-get-test-headers.xml"; + private static final String TEST_AUTH_CONFIG = "http-get-test-auth-config.xml"; private static final String TENANT_DOMAIN = "carbon.super"; private static final String STATUS = "status"; private static final String SUCCESS = "SUCCESS"; @@ -92,7 +93,7 @@ protected void initClass() throws Exception { // Mocking the executeHttpMethod method to avoid actual http calls. httpGetFunction = spy(new HTTPGetFunctionImpl()); - doNothing().when(httpGetFunction).executeHttpMethod(any(), any()); + doNothing().when(httpGetFunction).executeHttpMethod(any(), any(), any()); } @AfterClass @@ -143,6 +144,21 @@ public void testHttpGetMethodWithHeaders() throws JsTestException { assertEquals(result, SUCCESS, "The http get request was not successful. Result from request: " + result); } + /** + * Test http get method with auth config. + * Check if the auth config is applied to the request. + * + * @throws JsTestException + */ + @Test + public void testHttpGetMethodWithAuthConfig() throws JsTestException { + + String requestUrl = getRequestUrl("dummy-get-with-auth-config"); + String result = executeHttpGetFunction(requestUrl, TEST_AUTH_CONFIG); + + assertEquals(result, SUCCESS, "The http get request was not successful. Result from request: " + result); + } + /** * Tests the behavior of the httpGet function when provided with null headers. * @@ -162,7 +178,8 @@ public void testHttpGetWithNullHeaders() { @Test(expectedExceptions = IllegalArgumentException.class) public void testHttpGetWithInvalidNumberOfArguments() { Map eventHandlers = new HashMap<>(); - httpGetFunction.httpGet(getRequestUrl("dummy-get"), eventHandlers, eventHandlers, eventHandlers); + httpGetFunction.httpGet(getRequestUrl("dummy-get"), + eventHandlers, eventHandlers, eventHandlers, eventHandlers); } private void setAllowedDomain(String domain) { @@ -235,4 +252,25 @@ public Map dummyGetWithHeaders(@HeaderParam(AUTHORIZATION) Strin } return response; } + + /** + * Dummy endpoint to test the http get function with auth config. + * + * @param authorization Authorization header value. + * @return Response. + */ + @GET + @Path("/dummy-get-with-auth-config") + @Produces("application/json") + public Map dummyGetWithAuthConfig(@HeaderParam(AUTHORIZATION) String authorization) { + + System.out.println("Authorization123: " + authorization); + Map response = new HashMap<>(); + if (authorization != null) { + response.put(STATUS, SUCCESS); + } else { + response.put(STATUS, FAILED); + } + return response; + } } diff --git a/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/test/java/org/wso2/carbon/identity/conditional/auth/functions/http/HTTPPostFunctionImplTest.java b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/test/java/org/wso2/carbon/identity/conditional/auth/functions/http/HTTPPostFunctionImplTest.java index 6388b46e..b8c2382f 100644 --- a/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/test/java/org/wso2/carbon/identity/conditional/auth/functions/http/HTTPPostFunctionImplTest.java +++ b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/test/java/org/wso2/carbon/identity/conditional/auth/functions/http/HTTPPostFunctionImplTest.java @@ -67,6 +67,7 @@ public class HTTPPostFunctionImplTest extends JsSequenceHandlerAbstractTest { private static final String TEST_SP_CONFIG = "http-post-test-sp.xml"; private static final String TEST_HEADERS = "http-post-test-headers.xml"; + private static final String TEST_AUTH_CONFIG = "http-post-test-auth-config.xml"; private static final String TENANT_DOMAIN = "carbon.super"; private static final String STATUS = "status"; private static final String SUCCESS = "SUCCESS"; @@ -74,6 +75,7 @@ public class HTTPPostFunctionImplTest extends JsSequenceHandlerAbstractTest { private static final String EMAIL = "email"; private static final String ALLOWED_DOMAIN = "abc"; private static final String AUTHORIZATION = "Authorization"; + private static final String API_KEY_HEADER = "X-API-KEY"; private HTTPPostFunctionImpl httpPostFunction; @InjectMicroservicePort @@ -94,7 +96,7 @@ protected void initClass() throws Exception { // Mocking the executeHttpMethod method to avoid actual http calls. httpPostFunction = spy(new HTTPPostFunctionImpl()); - doNothing().when(httpPostFunction).executeHttpMethod(any(), any()); + doNothing().when(httpPostFunction).executeHttpMethod(any(), any(), any()); } @AfterClass @@ -144,6 +146,21 @@ public void testHttpPostWithHeaders() throws JsTestException { + result); } + /** + * Test httpPost with auth config. + * Check if the auth config is sent with the request. + * + * @throws JsTestException + */ + @Test + public void testHttpPostWithAuthConfig() throws JsTestException { + + String requestUrl = getRequestUrl("dummy-post-auth-config"); + String result = executeHttpPostFunction(requestUrl, TEST_AUTH_CONFIG); + assertEquals(result, SUCCESS, "The http post request was not successful. Result from request: " + + result); + } + /** * Tests the behavior of the httpPost function when provided with null headers. * @@ -254,4 +271,27 @@ public Map dummyPostWithHeaders(@HeaderParam(AUTHORIZATION) Stri } return response; } + + /** + * Dummy post method to test auth config. + * Check if the auth config is sent with the request. + * @param apikeyHeader + * @param data + * @return + */ + @POST + @Path("/dummy-post-auth-config") + @Produces("application/json") + @Consumes("application/json") + public Map dummyPostWithAuthConfig(@HeaderParam(API_KEY_HEADER) String apikeyHeader, + Map data) { + + Map response = new HashMap<>(); + if (data.containsKey(EMAIL) && apikeyHeader != null) { + response.put(STATUS, SUCCESS); + } else { + response.put(STATUS, FAILED); + } + return response; + } } diff --git a/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/test/resources/org/wso2/carbon/identity/conditional/auth/functions/http/http-get-test-auth-config.xml b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/test/resources/org/wso2/carbon/identity/conditional/auth/functions/http/http-get-test-auth-config.xml new file mode 100644 index 00000000..a61ddc9f --- /dev/null +++ b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/test/resources/org/wso2/carbon/identity/conditional/auth/functions/http/http-get-test-auth-config.xml @@ -0,0 +1,86 @@ + + + + 1 + default + Default Service Provider + + + + default + + + + + + + + + 1 + + + BasicMockAuthenticator + basicauth + true + + + true + true + + + + flow + + + + + + true + + + diff --git a/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/test/resources/org/wso2/carbon/identity/conditional/auth/functions/http/http-post-test-auth-config.xml b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/test/resources/org/wso2/carbon/identity/conditional/auth/functions/http/http-post-test-auth-config.xml new file mode 100644 index 00000000..b6383279 --- /dev/null +++ b/components/org.wso2.carbon.identity.conditional.auth.functions.http/src/test/resources/org/wso2/carbon/identity/conditional/auth/functions/http/http-post-test-auth-config.xml @@ -0,0 +1,89 @@ + + + + 1 + default + Default Service Provider + + + + default + + + + + + + + + 1 + + + BasicMockAuthenticator + basicauth + true + + + true + true + + + + flow + + + + + + true + + + diff --git a/pom.xml b/pom.xml index 1ae4d143..b8e3424a 100644 --- a/pom.xml +++ b/pom.xml @@ -103,6 +103,11 @@ org.wso2.carbon.identity.conditional.auth.functions.jwt.decode ${project.version} + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.central.log.mgt + ${carbon.identity.framework.version} + org.wso2.carbon.identity.conditional.auth.functions org.wso2.carbon.identity.conditional.auth.functions.utils