diff --git a/addOns/authhelper/CHANGELOG.md b/addOns/authhelper/CHANGELOG.md index b4e3f14958f..49231d32d91 100644 --- a/addOns/authhelper/CHANGELOG.md +++ b/addOns/authhelper/CHANGELOG.md @@ -7,6 +7,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed - Ignore non-displayed fields when selecting the user name and password. +### Added +- Added support for Client Script Authentication when used in conjunction with the Ajax Spider add-on. + ## [0.17.0] - 2025-01-09 ### Changed - Update minimum ZAP version to 2.16.0. diff --git a/addOns/authhelper/authhelper.gradle.kts b/addOns/authhelper/authhelper.gradle.kts index 660c09253d5..6da3cd4b2e9 100644 --- a/addOns/authhelper/authhelper.gradle.kts +++ b/addOns/authhelper/authhelper.gradle.kts @@ -17,7 +17,7 @@ zapAddOn { dependencies { addOns { register("spiderAjax") { - version.set(">=23.15.0") + version.set(">=23.23.0") } } } @@ -29,7 +29,16 @@ zapAddOn { dependencies { addOns { register("client") { - version.set(">=0.10.0") + version.set(">=0.12.0") + } + register("spiderAjax") { + version.set(">=23.23.0") + } + register("scripts") { + version.set(">=45.9.0") + } + register("zest") { + version.set(">=48.1.0") } } } @@ -69,6 +78,16 @@ dependencies { zapAddOn("selenium") zapAddOn("spiderAjax") zapAddOn("client") + zapAddOn("zest") + + implementation("org.zaproxy:zest:0.22.0") { + // Provided by commonlib add-on. + exclude(group = "com.fasterxml.jackson") + // Provided by Selenium add-on. + exclude(group = "org.seleniumhq.selenium") + // Provided by ZAP. + exclude(group = "net.htmlparser.jericho", module = "jericho-html") + } testImplementation(project(":testutils")) } diff --git a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/AuthUtils.java b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/AuthUtils.java index 96916a214f5..cd1029d78a8 100644 --- a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/AuthUtils.java +++ b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/AuthUtils.java @@ -591,7 +591,8 @@ static SessionManagementRequestDetails findSessionTokenSource(String token) { return findSessionTokenSource(token, -1); } - static SessionManagementRequestDetails findSessionTokenSource(String token, int firstId) { + public static SessionManagementRequestDetails findSessionTokenSource( + String token, int firstId) { ExtensionHistory extHist = AuthUtils.getExtension(ExtensionHistory.class); int lastId = extHist.getLastHistoryId(); if (firstId == -1) { diff --git a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/client/ClientScriptBasedAuthHandler.java b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/client/ClientScriptBasedAuthHandler.java new file mode 100644 index 00000000000..e6572a0e101 --- /dev/null +++ b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/client/ClientScriptBasedAuthHandler.java @@ -0,0 +1,112 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2025 The ZAP Development Team + * + * Licensed 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.zaproxy.addon.authhelper.client; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.zaproxy.addon.authhelper.AuthUtils; +import org.zaproxy.addon.authhelper.client.ClientScriptBasedAuthenticationMethodType.ClientScriptBasedAuthenticationMethod; +import org.zaproxy.addon.network.ExtensionNetwork; +import org.zaproxy.addon.network.server.ServerInfo; +import org.zaproxy.zap.authentication.AuthenticationMethod; +import org.zaproxy.zap.extension.selenium.BrowserHook; +import org.zaproxy.zap.extension.selenium.ExtensionSelenium; +import org.zaproxy.zap.extension.selenium.SeleniumScriptUtils; +import org.zaproxy.zap.extension.spiderAjax.AuthenticationHandler; +import org.zaproxy.zap.model.Context; +import org.zaproxy.zap.users.User; +import org.zaproxy.zest.impl.ZestBasicRunner; + +public class ClientScriptBasedAuthHandler implements AuthenticationHandler { + + private static final Logger LOGGER = LogManager.getLogger(ClientScriptBasedAuthHandler.class); + + private BrowserHook browserHook; + private static ZestBasicRunner zestRunner; + + @Override + public boolean enableAuthentication(User user) { + Context context = user.getContext(); + if (context.getAuthenticationMethod() + instanceof + ClientScriptBasedAuthenticationMethodType.ClientScriptBasedAuthenticationMethod) { + + if (browserHook != null) { + throw new IllegalStateException("BrowserHook already enabled"); + } + browserHook = new AuthenticationBrowserHook(context, user); + + AuthUtils.getExtension(ExtensionSelenium.class).registerBrowserHook(browserHook); + + return true; + } + return false; + } + + @Override + public boolean disableAuthentication(User user) { + if (browserHook != null) { + AuthUtils.getExtension(ExtensionSelenium.class).deregisterBrowserHook(browserHook); + browserHook = null; + return true; + } + return false; + } + + static class AuthenticationBrowserHook implements BrowserHook { + + private ClientScriptBasedAuthenticationMethod csaMethod; + private Context context; + + AuthenticationBrowserHook(Context context, User user) { + this.context = context; + AuthenticationMethod method = context.getAuthenticationMethod(); + if (!(method instanceof ClientScriptBasedAuthenticationMethod)) { + throw new IllegalStateException("Unsupported method " + method.getType().getName()); + } + csaMethod = (ClientScriptBasedAuthenticationMethod) method; + } + + private static ZestBasicRunner getZestRunner() { + if (zestRunner == null) { + zestRunner = new ZestBasicRunner(); + // Always proxy via ZAP + ServerInfo mainProxyInfo = + AuthUtils.getExtension(ExtensionNetwork.class).getMainProxyServerInfo(); + zestRunner.setProxy(mainProxyInfo.getAddress(), mainProxyInfo.getPort()); + } + return zestRunner; + } + + @Override + public void browserLaunched(SeleniumScriptUtils ssUtils) { + ZestBasicRunner runner = getZestRunner(); + runner.addWebDriver("ClientScriptBasedAuthHandler", ssUtils.getWebDriver()); + try { + runner.run(csaMethod.getZestScript(), null); + } catch (Exception e) { + LOGGER.warn( + "An error occurred while trying to execute the Client Script Authentication script: {}", + e.getMessage()); + LOGGER.debug(e); + } + } + } +} diff --git a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/client/ClientScriptBasedAuthenticationMethodType.java b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/client/ClientScriptBasedAuthenticationMethodType.java new file mode 100644 index 00000000000..5962cbc3ac0 --- /dev/null +++ b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/client/ClientScriptBasedAuthenticationMethodType.java @@ -0,0 +1,750 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2025 The ZAP Development Team + * + * Licensed 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.zaproxy.addon.authhelper.client; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.GridBagLayout; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import org.apache.commons.configuration.Configuration; +import org.apache.commons.configuration.ConfigurationException; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jdesktop.swingx.JXComboBox; +import org.jdesktop.swingx.decorator.FontHighlighter; +import org.jdesktop.swingx.renderer.DefaultListRenderer; +import org.parosproxy.paros.Constant; +import org.parosproxy.paros.control.Control; +import org.parosproxy.paros.core.scanner.Alert; +import org.parosproxy.paros.network.HttpMessage; +import org.parosproxy.paros.network.HttpRequestHeader; +import org.parosproxy.paros.view.View; +import org.zaproxy.addon.authhelper.AuthUtils; +import org.zaproxy.addon.authhelper.SessionManagementRequestDetails; +import org.zaproxy.addon.authhelper.SessionToken; +import org.zaproxy.addon.network.server.HttpMessageHandler; +import org.zaproxy.zap.authentication.AbstractAuthenticationMethodOptionsPanel; +import org.zaproxy.zap.authentication.AuthenticationCredentials; +import org.zaproxy.zap.authentication.AuthenticationHelper; +import org.zaproxy.zap.authentication.AuthenticationIndicatorsPanel; +import org.zaproxy.zap.authentication.AuthenticationMethod; +import org.zaproxy.zap.authentication.AuthenticationMethodType; +import org.zaproxy.zap.authentication.GenericAuthenticationCredentials; +import org.zaproxy.zap.authentication.ScriptBasedAuthenticationMethodType; +import org.zaproxy.zap.extension.script.ExtensionScript; +import org.zaproxy.zap.extension.script.ScriptWrapper; +import org.zaproxy.zap.extension.zest.ZestAuthenticationRunner; +import org.zaproxy.zap.model.Context; +import org.zaproxy.zap.session.SessionManagementMethod; +import org.zaproxy.zap.session.WebSession; +import org.zaproxy.zap.users.User; +import org.zaproxy.zap.utils.EncodingUtils; +import org.zaproxy.zap.utils.ZapHtmlLabel; +import org.zaproxy.zap.view.DynamicFieldsPanel; +import org.zaproxy.zap.view.LayoutHelper; +import org.zaproxy.zest.core.v1.ZestScript; + +public class ClientScriptBasedAuthenticationMethodType extends ScriptBasedAuthenticationMethodType { + + public static final int METHOD_IDENTIFIER = 8; + + private static final Logger LOGGER = + LogManager.getLogger(ClientScriptBasedAuthenticationMethodType.class); + + private ExtensionScript extensionScript; + + private HttpMessageHandler handler; + private HttpMessage authMsg; + private HttpMessage fallbackMsg; + private int firstHrefId; + + public ClientScriptBasedAuthenticationMethodType() {} + + private HttpMessageHandler getHandler(Context context) { + if (handler == null) { + handler = + (ctx, msg) -> { + if (ctx.isFromClient()) { + return; + } + + AuthenticationHelper.addAuthMessageToHistory(msg); + + if (HttpRequestHeader.POST.equals(msg.getRequestHeader().getMethod()) + && context.isIncluded(msg.getRequestHeader().getURI().toString())) { + // Record the last in scope POST as a fallback + fallbackMsg = msg; + } + + SessionManagementRequestDetails smReqDetails = null; + Map sessionTokens = + AuthUtils.getResponseSessionTokens(msg); + if (!sessionTokens.isEmpty()) { + authMsg = msg; + smReqDetails = + new SessionManagementRequestDetails( + authMsg, + new ArrayList<>(sessionTokens.values()), + Alert.CONFIDENCE_HIGH); + } else { + Set reqSessionTokens = + AuthUtils.getRequestSessionTokens(msg); + if (!reqSessionTokens.isEmpty()) { + // The request has at least one auth token we missed - try + // to find one of them + for (SessionToken st : reqSessionTokens) { + smReqDetails = + AuthUtils.findSessionTokenSource( + st.getValue(), firstHrefId); + if (smReqDetails != null) { + authMsg = smReqDetails.getMsg(); + LOGGER.debug( + "Session token found in href {}", + authMsg.getHistoryRef().getHistoryId()); + break; + } + } + } + + if (authMsg != null && View.isInitialised()) { + String hrefId = "?"; + if (msg.getHistoryRef() != null) { + hrefId = "" + msg.getHistoryRef().getHistoryId(); + } + AuthUtils.logUserMessage( + Level.INFO, + Constant.messages.getString( + "authhelper.auth.method.browser.output.sessionid", + hrefId)); + } + } + if (firstHrefId == 0 && msg.getHistoryRef() != null) { + firstHrefId = msg.getHistoryRef().getHistoryId(); + } + }; + } + return handler; + } + + @Override + public String getName() { + return Constant.messages.getString("authhelper.auth.method.clientscript.name"); + } + + @Override + public int getUniqueIdentifier() { + return METHOD_IDENTIFIER; + } + + @Override + public ClientScriptBasedAuthenticationMethod createAuthenticationMethod(int contextId) { + return new ClientScriptBasedAuthenticationMethod(); + } + + @Override + public AbstractAuthenticationMethodOptionsPanel buildOptionsPanel(Context uiSharedContext) { + return new ClientScriptBasedAuthenticationMethodOptionsPanel(); + } + + public class ClientScriptBasedAuthenticationMethod extends ScriptBasedAuthenticationMethod { + private ScriptWrapper script; + + private String[] credentialsParamNames; + + private Map paramValues; + + /** + * Load a script and fills in the method's parameters according to the values specified by + * the script. + * + *

If the method already had a loaded script and a set of values for the parameters, it + * tries to provide new values for the new parameters if they match any previous parameter + * names. + * + * @param scriptW the script wrapper + * @throws IllegalArgumentException if an error occurs while loading the script. + */ + @Override + public void loadScript(ScriptWrapper scriptW) { + AuthenticationScript authScript = getAuthScriptInterfaceV2(scriptW); + if (authScript == null) { + authScript = getAuthScriptInterface(scriptW); + } + if (authScript == null) { + LOGGER.warn( + "The script {} does not properly implement the Authentication Script interface.", + scriptW.getName()); + throw new IllegalArgumentException( + Constant.messages.getString( + "authentication.method.script.dialog.error.text.interface", + scriptW.getName())); + } + + try { + if (authScript instanceof AuthenticationScriptV2 scriptV2) { + setLoggedInIndicatorPattern(scriptV2.getLoggedInIndicator()); + setLoggedOutIndicatorPattern(scriptV2.getLoggedOutIndicator()); + } + String[] requiredParams = authScript.getRequiredParamsNames(); + String[] optionalParams = authScript.getOptionalParamsNames(); + this.credentialsParamNames = authScript.getCredentialsParamsNames(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "Loaded authentication script - required parameters: {} - optional parameters: {}", + Arrays.toString(requiredParams), + Arrays.toString(optionalParams)); + } + // If there's an already loaded script, make sure we save its values and _try_ + // to use them + Map oldValues = + this.paramValues != null + ? this.paramValues + : Collections.emptyMap(); + this.paramValues = new HashMap<>(requiredParams.length + optionalParams.length); + for (String param : requiredParams) + this.paramValues.put(param, oldValues.get(param)); + for (String param : optionalParams) + this.paramValues.put(param, oldValues.get(param)); + + this.script = scriptW; + LOGGER.info( + "Successfully loaded new script for ClientScriptBasedAuthentication: {}", + this); + } catch (Exception e) { + LOGGER.error("Error while loading authentication script", e); + getExtensionScript().handleScriptException(this.script, e); + throw new IllegalArgumentException( + Constant.messages.getString( + "authentication.method.script.dialog.error.text.loading", + e.getMessage())); + } + } + + @Override + public String toString() { + return "ClientScriptBasedAuthenticationMethod [script=" + + script + + ", paramValues=" + + paramValues + + ", credentialsParamNames=" + + Arrays.toString(credentialsParamNames) + + "]"; + } + + @Override + public boolean isConfigured() { + return true; + } + + @Override + public AuthenticationMethod duplicate() { + ClientScriptBasedAuthenticationMethod method = + new ClientScriptBasedAuthenticationMethod(); + method.script = script; + method.paramValues = this.paramValues != null ? new HashMap<>(this.paramValues) : null; + method.credentialsParamNames = this.credentialsParamNames; + return method; + } + + @Override + public boolean validateCreationOfAuthenticationCredentials() { + if (credentialsParamNames != null) { + return true; + } + + if (View.isInitialised()) { + View.getSingleton() + .showMessageDialog( + Constant.messages.getString( + "authentication.method.script.dialog.error.text.notLoaded")); + } + + return false; + } + + @Override + public AuthenticationCredentials createAuthenticationCredentials() { + return new GenericAuthenticationCredentials(this.credentialsParamNames); + } + + @Override + public AuthenticationMethodType getType() { + return new ClientScriptBasedAuthenticationMethodType(); + } + + public ScriptWrapper getScriptWrapper() { + return this.script; + } + + public ZestScript getZestScript() { + AuthenticationScript authScript = getAuthScriptInterfaceV2(this.script); + if (authScript == null) { + authScript = getAuthScriptInterface(this.script); + } + + if (authScript == null) { + LOGGER.debug("Failed to get ZestScript - no suitable interface"); + return null; + } + + if (authScript instanceof ZestAuthenticationRunner zestScript) { + return zestScript.getScript().getZestScript(); + } + LOGGER.debug( + "Failed to get ZestScript - authScript of right type {}", + authScript.getClass().getCanonicalName()); + return null; + } + + @Override + public WebSession authenticate( + SessionManagementMethod sessionManagementMethod, + AuthenticationCredentials credentials, + User user) + throws UnsupportedAuthenticationCredentialsException { + if (!(credentials instanceof GenericAuthenticationCredentials)) { + user.getAuthenticationState() + .setLastAuthFailure("Credentials not GenericAuthenticationCredentials"); + throw new UnsupportedAuthenticationCredentialsException( + "Script based Authentication method only supports " + + GenericAuthenticationCredentials.class.getSimpleName() + + ". Received: " + + credentials.getClass()); + } + GenericAuthenticationCredentials cred = (GenericAuthenticationCredentials) credentials; + + // Call the script to get an authenticated message from which we can then extract the + // session + AuthenticationScript authScript = getAuthScriptInterfaceV2(this.script); + if (authScript == null) { + authScript = getAuthScriptInterface(this.script); + } + + if (authScript == null) { + return null; + } + LOGGER.debug("Script class: {}", authScript.getClass().getCanonicalName()); + ExtensionScript.recordScriptCalledStats(this.script); + + try { + if (authScript instanceof AuthenticationScriptV2 scriptV2) { + setLoggedInIndicatorPattern(scriptV2.getLoggedInIndicator()); + setLoggedOutIndicatorPattern(scriptV2.getLoggedOutIndicator()); + } + + if (authScript instanceof ZestAuthenticationRunner zestScript) { + zestScript.registerHandler(getHandler(user.getContext())); + } else { + // TODO fail in some way? + } + + authScript.authenticate( + new AuthenticationHelper(getHttpSender(), sessionManagementMethod, user), + this.paramValues, + cred); + } catch (Exception e) { + // Catch Exception instead of ScriptException and IOException because script engine + // implementations might throw other exceptions on script errors (e.g. + // jdk.nashorn.internal.runtime.ECMAException) + user.getAuthenticationState() + .setLastAuthFailure( + "Error running authentication script " + e.getMessage()); + LOGGER.error( + "An error occurred while trying to authenticate using the Authentication Script: {}", + this.script.getName(), + e); + getExtensionScript().handleScriptException(this.script, e); + return null; + } + + // Wait until the authentication request is identified + for (int i = 0; i < AuthUtils.getWaitLoopCount(); i++) { + if (authMsg != null) { + break; + } + AuthUtils.sleep(AuthUtils.TIME_TO_SLEEP_IN_MSECS); + } + + if (authMsg != null) { + // Update the session as it may have changed + WebSession session = sessionManagementMethod.extractWebSession(authMsg); + user.setAuthenticatedSession(session); + + if (this.isAuthenticated(authMsg, user, true)) { + // Let the user know it worked + AuthenticationHelper.notifyOutputAuthSuccessful(authMsg); + user.getAuthenticationState().setLastAuthFailure(""); + } else { + // Let the user know it failed + AuthenticationHelper.notifyOutputAuthFailure(authMsg); + } + return session; + } + + // We don't expect this to work, but it will prevent some NPEs + return sessionManagementMethod.extractWebSession(fallbackMsg); + } + + @Override + public void replaceUserDataInPollRequest(HttpMessage msg, User user) { + AuthenticationHelper.replaceUserDataInRequest( + msg, wrapKeys(this.paramValues), NULL_ENCODER); + } + } + + private static Map wrapKeys(Map kvPairs) { + Map map = new HashMap<>(); + for (Entry kv : kvPairs.entrySet()) { + map.put( + AuthenticationMethod.TOKEN_PREFIX + + kv.getKey() + + AuthenticationMethod.TOKEN_POSTFIX, + kv.getValue()); + } + return map; + } + + @SuppressWarnings("serial") + public class ClientScriptBasedAuthenticationMethodOptionsPanel + extends AbstractAuthenticationMethodOptionsPanel { + + private static final long serialVersionUID = 7812841049435409987L; + + private static final String SCRIPT_NAME_LABEL = + Constant.messages.getString("authentication.method.script.field.label.scriptName"); + private static final String LABEL_NOT_LOADED = + Constant.messages.getString("authentication.method.script.field.label.notLoaded"); + private JXComboBox scriptsComboBox; + private JButton loadScriptButton; + + private ClientScriptBasedAuthenticationMethod method; + private AuthenticationIndicatorsPanel indicatorsPanel; + + private ScriptWrapper loadedScript; + + private JPanel dynamicContentPanel; + + private DynamicFieldsPanel dynamicFieldsPanel; + + private String[] loadedCredentialParams; + + public ClientScriptBasedAuthenticationMethodOptionsPanel() { + super(); + initialize(); + } + + private void initialize() { + this.setLayout(new GridBagLayout()); + + this.add(new JLabel(SCRIPT_NAME_LABEL), LayoutHelper.getGBC(0, 0, 1, 0.0d, 0.0d)); + + scriptsComboBox = new JXComboBox(); + scriptsComboBox.addHighlighter( + new FontHighlighter( + (renderer, adapter) -> loadedScript == adapter.getValue(), + scriptsComboBox.getFont().deriveFont(Font.BOLD))); + scriptsComboBox.setRenderer( + new DefaultListRenderer( + sw -> { + if (sw == null) { + return null; + } + + String name = ((ScriptWrapper) sw).getName(); + if (loadedScript == sw) { + return Constant.messages.getString( + "authentication.method.script.loaded", name); + } + return name; + })); + this.add(this.scriptsComboBox, LayoutHelper.getGBC(1, 0, 1, 1.0d, 0.0d)); + + this.loadScriptButton = + new JButton( + Constant.messages.getString( + "authentication.method.script.load.button")); + this.add(this.loadScriptButton, LayoutHelper.getGBC(2, 0, 1, 0.0d, 0.0d)); + this.loadScriptButton.addActionListener( + e -> loadScript((ScriptWrapper) scriptsComboBox.getSelectedItem(), true)); + + // Make sure the 'Load' button is disabled when nothing is selected + this.loadScriptButton.setEnabled(false); + this.scriptsComboBox.addActionListener( + e -> loadScriptButton.setEnabled(scriptsComboBox.getSelectedIndex() >= 0)); + + this.dynamicContentPanel = new JPanel(new BorderLayout()); + this.add(this.dynamicContentPanel, LayoutHelper.getGBC(0, 1, 3, 1.0d, 0.0d)); + this.dynamicContentPanel.add(new ZapHtmlLabel(LABEL_NOT_LOADED)); + } + + @Override + public void validateFields() throws IllegalStateException { + if (this.loadedScript == null) { + this.scriptsComboBox.requestFocusInWindow(); + throw new IllegalStateException( + Constant.messages.getString( + "authentication.method.script.dialog.error.text.notLoadedNorConfigured")); + } + this.dynamicFieldsPanel.validateFields(); + } + + @Override + public void saveMethod() { + this.method.script = (ScriptWrapper) this.scriptsComboBox.getSelectedItem(); + // This method will also be called when switching panels to save a temporary state so + // the state of the authentication method might not be valid + if (this.dynamicFieldsPanel != null) + this.method.paramValues = this.dynamicFieldsPanel.getFieldValues(); + else this.method.paramValues = Collections.emptyMap(); + if (this.loadedScript != null) + this.method.credentialsParamNames = this.loadedCredentialParams; + } + + @Override + @SuppressWarnings("unchecked") + public void bindMethod(AuthenticationMethod method) + throws UnsupportedAuthenticationMethodException { + this.method = (ClientScriptBasedAuthenticationMethod) method; + + // Make sure the list of scripts is refreshed with just Zest scripts + List scripts = + getExtensionScript().getScripts(SCRIPT_TYPE_AUTH).stream() + .filter(sc -> sc.getEngineName().contains("Zest")) + .toList(); + DefaultComboBoxModel model = + new DefaultComboBoxModel<>(scripts.toArray(new ScriptWrapper[scripts.size()])); + this.scriptsComboBox.setModel(model); + this.scriptsComboBox.setSelectedItem(this.method.script); + this.loadScriptButton.setEnabled(this.method.script != null); + + // Load the selected script, if any + if (this.method.script != null) { + loadScript(this.method.script, false); + if (this.dynamicFieldsPanel != null) + this.dynamicFieldsPanel.bindFieldValues(this.method.paramValues); + } + } + + @Override + public void bindMethod( + AuthenticationMethod method, AuthenticationIndicatorsPanel indicatorsPanel) + throws UnsupportedAuthenticationMethodException { + this.indicatorsPanel = indicatorsPanel; + bindMethod(method); + } + + @Override + public AuthenticationMethod getMethod() { + return this.method; + } + + private void loadScript(ScriptWrapper scriptW, boolean adaptOldValues) { + AuthenticationScript script = getAuthScriptInterfaceV2(scriptW); + if (script == null) { + script = getAuthScriptInterface(scriptW); + } + + if (script == null) { + LOGGER.warn( + "The script {} does not properly implement the Authentication Script interface.", + scriptW.getName()); + warnAndResetPanel( + Constant.messages.getString( + "authentication.method.script.dialog.error.text.interface", + scriptW.getName())); + return; + } + + try { + if (script instanceof AuthenticationScriptV2 scriptV2) { + String toolTip = + Constant.messages.getString( + "authentication.method.script.dialog.loggedInOutIndicatorsInScript.toolTip"); + String loggedInIndicator = scriptV2.getLoggedInIndicator(); + this.method.setLoggedInIndicatorPattern(loggedInIndicator); + this.indicatorsPanel.setLoggedInIndicatorPattern(loggedInIndicator); + this.indicatorsPanel.setLoggedInIndicatorEnabled(false); + this.indicatorsPanel.setLoggedInIndicatorToolTip(toolTip); + + String loggedOutIndicator = scriptV2.getLoggedOutIndicator(); + this.method.setLoggedOutIndicatorPattern(loggedOutIndicator); + this.indicatorsPanel.setLoggedOutIndicatorPattern(loggedOutIndicator); + this.indicatorsPanel.setLoggedOutIndicatorEnabled(false); + this.indicatorsPanel.setLoggedOutIndicatorToolTip(toolTip); + } else { + this.indicatorsPanel.setLoggedInIndicatorEnabled(true); + this.indicatorsPanel.setLoggedInIndicatorToolTip(null); + this.indicatorsPanel.setLoggedOutIndicatorEnabled(true); + this.indicatorsPanel.setLoggedOutIndicatorToolTip(null); + } + String[] requiredParams = script.getRequiredParamsNames(); + String[] optionalParams = script.getOptionalParamsNames(); + this.loadedCredentialParams = script.getCredentialsParamsNames(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "Loaded authentication script - required parameters: {} - optional parameters: {}", + Arrays.toString(requiredParams), + Arrays.toString(optionalParams)); + } + // If there's an already loaded script, make sure we save its values and _try_ + // to place them in the new panel + Map oldValues = null; + if (adaptOldValues && dynamicFieldsPanel != null) { + oldValues = dynamicFieldsPanel.getFieldValues(); + LOGGER.debug("Trying to adapt old values: {}", oldValues); + } + + this.dynamicFieldsPanel = new DynamicFieldsPanel(requiredParams, optionalParams); + this.loadedScript = scriptW; + if (adaptOldValues && oldValues != null) { + this.dynamicFieldsPanel.bindFieldValues(oldValues); + } + + this.dynamicContentPanel.removeAll(); + this.dynamicContentPanel.add(dynamicFieldsPanel, BorderLayout.CENTER); + this.dynamicContentPanel.revalidate(); + + } catch (Exception e) { + getExtensionScript().handleScriptException(scriptW, e); + LOGGER.error("Error while calling authentication script", e); + warnAndResetPanel( + Constant.messages.getString( + "authentication.method.script.dialog.error.text.loading", + ExceptionUtils.getRootCauseMessage(e))); + } + } + + private void warnAndResetPanel(String errorMessage) { + JOptionPane.showMessageDialog( + this, + errorMessage, + Constant.messages.getString("authentication.method.script.dialog.error.title"), + JOptionPane.ERROR_MESSAGE); + this.loadedScript = null; + this.scriptsComboBox.setSelectedItem(null); + this.dynamicFieldsPanel = null; + this.dynamicContentPanel.removeAll(); + this.dynamicContentPanel.add(new JLabel(LABEL_NOT_LOADED), BorderLayout.CENTER); + this.dynamicContentPanel.revalidate(); + } + } + + private ExtensionScript getExtensionScript() { + if (extensionScript == null) + extensionScript = + Control.getSingleton().getExtensionLoader().getExtension(ExtensionScript.class); + return extensionScript; + } + + private AuthenticationScript getAuthScriptInterface(ScriptWrapper script) { + try { + return getExtensionScript().getInterface(script, AuthenticationScript.class); + } catch (Exception e) { + getExtensionScript() + .handleFailedScriptInterface( + script, + Constant.messages.getString( + "authentication.method.script.dialog.error.text.interface", + script.getName())); + } + return null; + } + + private AuthenticationScriptV2 getAuthScriptInterfaceV2(ScriptWrapper script) { + try { + AuthenticationScriptV2 authScript = + getExtensionScript().getInterface(script, AuthenticationScriptV2.class); + if (authScript == null) { + LOGGER.debug( + "Script '{}' is not a AuthenticationScriptV2 interface.", script::getName); + return null; + } + + // Some ScriptEngines do not verify if all Interface Methods are contained in the + // script. + // So we must invoke them to ensure that they are defined in the loaded script! + // Otherwise some ScriptEngines loads successfully AuthenticationScriptV2 without the + // methods getLoggedInIndicator() / getLoggedOutIndicator(). + // Though it should fallback to interface AuthenticationScript. + authScript.getLoggedInIndicator(); + authScript.getLoggedOutIndicator(); + return authScript; + } catch (Exception ignore) { + // The interface is optional, the AuthenticationScript will be checked after this one. + LOGGER.debug( + "Script '{}' is not a AuthenticationScriptV2 interface!", + script.getName(), + ignore); + } + return null; + } + + @Override + public void exportData(Configuration config, AuthenticationMethod authMethod) { + if (!(authMethod instanceof ClientScriptBasedAuthenticationMethod)) { + throw new UnsupportedAuthenticationMethodException( + "Client script based authentication type only supports: " + + ClientScriptBasedAuthenticationMethod.class.getName()); + } + ClientScriptBasedAuthenticationMethod method = + (ClientScriptBasedAuthenticationMethod) authMethod; + config.setProperty(CONTEXT_CONFIG_AUTH_SCRIPT_NAME, method.script.getName()); + config.setProperty( + CONTEXT_CONFIG_AUTH_SCRIPT_PARAMS, EncodingUtils.mapToString(method.paramValues)); + } + + @Override + public void importData(Configuration config, AuthenticationMethod authMethod) + throws ConfigurationException { + if (!(authMethod instanceof ClientScriptBasedAuthenticationMethod)) { + throw new UnsupportedAuthenticationMethodException( + "Client script based authentication type only supports: " + + ClientScriptBasedAuthenticationMethod.class.getName()); + } + ClientScriptBasedAuthenticationMethod method = + (ClientScriptBasedAuthenticationMethod) authMethod; + this.loadMethod( + method, + objListToStrList(config.getList(CONTEXT_CONFIG_AUTH_SCRIPT_NAME)), + objListToStrList(config.getList(CONTEXT_CONFIG_AUTH_SCRIPT_PARAMS))); + } + + private static List objListToStrList(List oList) { + List sList = new ArrayList<>(oList.size()); + for (Object o : oList) { + sList.add(o.toString()); + } + return sList; + } +} diff --git a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/client/ExtensionAuthhelperClient.java b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/client/ExtensionAuthhelperClient.java index a7d0f964fcb..e4885f5af2d 100644 --- a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/client/ExtensionAuthhelperClient.java +++ b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/client/ExtensionAuthhelperClient.java @@ -20,21 +20,31 @@ package org.zaproxy.addon.authhelper.client; import java.util.List; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.parosproxy.paros.Constant; import org.parosproxy.paros.control.Control; import org.parosproxy.paros.extension.Extension; import org.parosproxy.paros.extension.ExtensionAdaptor; import org.parosproxy.paros.extension.ExtensionHook; +import org.zaproxy.addon.authhelper.AuthUtils; import org.zaproxy.addon.client.ExtensionClientIntegration; +import org.zaproxy.zap.extension.authentication.ExtensionAuthentication; +import org.zaproxy.zap.extension.spiderAjax.ExtensionAjax; public class ExtensionAuthhelperClient extends ExtensionAdaptor { public static final String NAME = "ExtensionAuthhelperClient"; private static final List> DEPENDENCIES = - List.of(ExtensionClientIntegration.class); + List.of(ExtensionClientIntegration.class, ExtensionAjax.class); + private static final Logger LOGGER = LogManager.getLogger(ExtensionAuthhelperClient.class); private BrowserBasedAuthHandler authHandler; + private ClientScriptBasedAuthHandler scriptAuthHandler; + + protected static final ClientScriptBasedAuthenticationMethodType CLIENT_SCRIPT_BASED_AUTH_TYPE = + new ClientScriptBasedAuthenticationMethodType(); public ExtensionAuthhelperClient() { super(NAME); @@ -45,11 +55,20 @@ public boolean supportsDb(String type) { return true; } + @Override + public List> getDependencies() { + return DEPENDENCIES; + } + @Override public void hook(ExtensionHook extensionHook) { super.hook(extensionHook); authHandler = new BrowserBasedAuthHandler(); getClientExtension().addAuthenticationHandler(authHandler); + ExtensionAjax extAjax = + Control.getSingleton().getExtensionLoader().getExtension(ExtensionAjax.class); + scriptAuthHandler = new ClientScriptBasedAuthHandler(); + extAjax.addAuthenticationHandler(scriptAuthHandler); } private static ExtensionClientIntegration getClientExtension() { @@ -59,18 +78,32 @@ private static ExtensionClientIntegration getClientExtension() { } @Override - public boolean canUnload() { - return true; + public void optionsLoaded() { + ExtensionAuthentication extAuth = AuthUtils.getExtension(ExtensionAuthentication.class); + if (extAuth != null) { + extAuth.getAuthenticationMethodTypes().add(CLIENT_SCRIPT_BASED_AUTH_TYPE); + LOGGER.debug("Loaded client script based auth type."); + } } @Override public void unload() { getClientExtension().removeAuthenticationHandler(authHandler); + ExtensionAuthentication extAuth = AuthUtils.getExtension(ExtensionAuthentication.class); + + if (extAuth != null) { + extAuth.getAuthenticationMethodTypes().remove(CLIENT_SCRIPT_BASED_AUTH_TYPE); + } + ExtensionAjax extAjax = + Control.getSingleton().getExtensionLoader().getExtension(ExtensionAjax.class); + if (extAjax != null) { + extAjax.removeAuthenticationHandler(scriptAuthHandler); + } } @Override - public List> getDependencies() { - return DEPENDENCIES; + public boolean canUnload() { + return true; } @Override diff --git a/addOns/authhelper/src/main/resources/org/zaproxy/addon/authhelper/resources/Messages.properties b/addOns/authhelper/src/main/resources/org/zaproxy/addon/authhelper/resources/Messages.properties index e49a1235cd9..ab440ec5434 100644 --- a/addOns/authhelper/src/main/resources/org/zaproxy/addon/authhelper/resources/Messages.properties +++ b/addOns/authhelper/src/main/resources/org/zaproxy/addon/authhelper/resources/Messages.properties @@ -9,6 +9,8 @@ authhelper.auth.method.browser.label.loginWait = Login Wait in Seconds: authhelper.auth.method.browser.name = Browser-based Authentication authhelper.auth.method.browser.output.sessionid = Session token identified in History ID: {0} +authhelper.auth.method.clientscript.name = Client Script Authentication + authhelper.auth.test.dialog.button.copy = Copy authhelper.auth.test.dialog.button.save = Test diff --git a/addOns/automation/CHANGELOG.md b/addOns/automation/CHANGELOG.md index 89eb754fd29..3c878792d8d 100644 --- a/addOns/automation/CHANGELOG.md +++ b/addOns/automation/CHANGELOG.md @@ -8,6 +8,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Address malformed HTML in the help. - Correct default value of `threadPerHost` property of the `activeScan-config` job's help. +### Added +- Added support for Client Script Authentication when the Ajax Spider is used in conjunction with the Auth Helper add-on. + ## [0.44.0] - 2025-01-09 ### Added - Active scan policy job. diff --git a/addOns/automation/src/main/java/org/zaproxy/addon/automation/AuthenticationData.java b/addOns/automation/src/main/java/org/zaproxy/addon/automation/AuthenticationData.java index 81231295138..7f45d2ed059 100644 --- a/addOns/automation/src/main/java/org/zaproxy/addon/automation/AuthenticationData.java +++ b/addOns/automation/src/main/java/org/zaproxy/addon/automation/AuthenticationData.java @@ -30,6 +30,7 @@ import org.apache.commons.configuration.Configuration; import org.apache.commons.configuration.ConfigurationException; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.reflect.MethodUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.parosproxy.paros.Constant; @@ -50,6 +51,8 @@ import org.zaproxy.zap.utils.ZapXmlConfiguration; public class AuthenticationData extends AutomationData { + private static final String AUTOMATION_ERROR_ENV_AUTH_TYPE_BAD = + "automation.error.env.auth.type.bad"; public static final String METHOD_HTTP = "http"; public static final String METHOD_FORM = "form"; public static final String METHOD_JSON = "json"; @@ -57,6 +60,7 @@ public class AuthenticationData extends AutomationData { public static final String METHOD_SCRIPT = "script"; public static final String METHOD_BROWSER = "browser"; public static final String METHOD_AUTO = "autodetect"; + public static final String METHOD_CLIENT = "client"; public static final String PARAM_HOSTNAME = "hostname"; public static final String PARAM_REALM = "realm"; @@ -72,10 +76,15 @@ public class AuthenticationData extends AutomationData { protected static final String BROWSER_BASED_AUTH_METHOD_CLASSNAME = "org.zaproxy.addon.authhelper.BrowserBasedAuthenticationMethodType.BrowserBasedAuthenticationMethod"; + protected static final String CLIENT_SCRIPT_BASED_AUTH_METHOD_CLASSNAME = + "org.zaproxy.addon.authhelper.client.ClientScriptBasedAuthenticationMethodType.ClientScriptBasedAuthenticationMethod"; + /** Field name in the underlying PostBasedAuthenticationMethod class * */ protected static final String FIELD_LOGIN_REQUEST_URL = "loginRequestURL"; private static final String BAD_FIELD_ERROR_MSG = "automation.error.env.auth.field.bad"; + private static final String PARAM_VALUES = "paramValues"; + private static final String PRIVATE_FIELD_SCRIPT = "script"; public static final String VERIFICATION_ELEMENT = "verification"; @@ -87,7 +96,8 @@ public class AuthenticationData extends AutomationData { METHOD_JSON, METHOD_SCRIPT, METHOD_BROWSER, - METHOD_AUTO); + METHOD_AUTO, + METHOD_CLIENT); private String method; private Map parameters = new LinkedHashMap<>(); @@ -99,32 +109,47 @@ public AuthenticationData() {} public AuthenticationData(Context context) { AuthenticationMethod authMethod = context.getAuthenticationMethod(); - if (authMethod instanceof HttpAuthenticationMethod) { - HttpAuthenticationMethod httpAuthMethod = (HttpAuthenticationMethod) authMethod; + if (authMethod instanceof HttpAuthenticationMethod httpAuthMethod) { setMethod(AuthenticationData.METHOD_HTTP); JobUtils.addPrivateField(parameters, PARAM_REALM, httpAuthMethod); JobUtils.addPrivateField(parameters, PARAM_HOSTNAME, httpAuthMethod); JobUtils.addPrivateField(parameters, PARAM_PORT, httpAuthMethod); - } else if (authMethod instanceof FormBasedAuthenticationMethod) { - FormBasedAuthenticationMethod formAuthMethod = - (FormBasedAuthenticationMethod) authMethod; + } else if (authMethod instanceof FormBasedAuthenticationMethod formAuthMethod) { setMethod(AuthenticationData.METHOD_FORM); JobUtils.addPrivateField(parameters, PARAM_LOGIN_PAGE_URL, formAuthMethod); JobUtils.addPrivateField( parameters, PARAM_LOGIN_REQUEST_URL, FIELD_LOGIN_REQUEST_URL, formAuthMethod); JobUtils.addPrivateField(parameters, PARAM_LOGIN_REQUEST_BODY, formAuthMethod); - } else if (authMethod instanceof JsonBasedAuthenticationMethod) { - JsonBasedAuthenticationMethod jsonAuthMethod = - (JsonBasedAuthenticationMethod) authMethod; + } else if (authMethod instanceof JsonBasedAuthenticationMethod jsonAuthMethod) { setMethod(AuthenticationData.METHOD_JSON); JobUtils.addPrivateField(parameters, PARAM_LOGIN_PAGE_URL, jsonAuthMethod); JobUtils.addPrivateField( parameters, PARAM_LOGIN_REQUEST_URL, FIELD_LOGIN_REQUEST_URL, jsonAuthMethod); JobUtils.addPrivateField(parameters, PARAM_LOGIN_REQUEST_BODY, jsonAuthMethod); - } else if (authMethod instanceof ScriptBasedAuthenticationMethod) { - ScriptBasedAuthenticationMethod scriptAuthMethod = - (ScriptBasedAuthenticationMethod) authMethod; - ScriptWrapper sw = (ScriptWrapper) JobUtils.getPrivateField(scriptAuthMethod, "script"); + } else if (authMethod != null + && authMethod + .getClass() + .getCanonicalName() + .equals(CLIENT_SCRIPT_BASED_AUTH_METHOD_CLASSNAME)) { + // TODO: Plan to change once the core supports dynamic methods better + ScriptWrapper sw = + (ScriptWrapper) JobUtils.getPrivateField(authMethod, PRIVATE_FIELD_SCRIPT); + LOGGER.debug("Matched client script class"); + if (sw != null) { + setMethod(METHOD_CLIENT); + parameters.put(PARAM_SCRIPT, sw.getFile().getAbsolutePath()); + parameters.put(PARAM_SCRIPT_ENGINE, sw.getEngineName()); + @SuppressWarnings("unchecked") + Map paramValues = + (Map) JobUtils.getPrivateField(authMethod, PARAM_VALUES); + for (Entry entry : paramValues.entrySet()) { + parameters.put(entry.getKey(), entry.getValue()); + } + } + } else if (authMethod instanceof ScriptBasedAuthenticationMethod scriptAuthMethod) { + ScriptWrapper sw = + (ScriptWrapper) + JobUtils.getPrivateField(scriptAuthMethod, PRIVATE_FIELD_SCRIPT); if (sw != null) { setMethod(AuthenticationData.METHOD_SCRIPT); parameters.put(PARAM_SCRIPT, sw.getFile().getAbsolutePath()); @@ -132,7 +157,7 @@ public AuthenticationData(Context context) { @SuppressWarnings("unchecked") Map paramValues = (Map) - JobUtils.getPrivateField(scriptAuthMethod, "paramValues"); + JobUtils.getPrivateField(scriptAuthMethod, PARAM_VALUES); for (Entry entry : paramValues.entrySet()) { parameters.put(entry.getKey(), entry.getValue()); } @@ -172,12 +197,11 @@ public AuthenticationData(Object data, AutomationProgress progress) { if (!StringUtils.isEmpty(method) && !validMethods.contains(method.toLowerCase(Locale.ROOT))) { progress.error( - Constant.messages.getString("automation.error.env.auth.type.bad", data)); + Constant.messages.getString(AUTOMATION_ERROR_ENV_AUTH_TYPE_BAD, data)); } for (Entry entry : parameters.entrySet()) { switch (entry.getKey()) { - case PARAM_PORT: - case PARAM_LOGIN_PAGE_WAIT: + case PARAM_PORT, PARAM_LOGIN_PAGE_WAIT: try { Integer.parseInt(entry.getValue().toString()); } catch (NumberFormatException e) { @@ -291,6 +315,9 @@ public void initContextAuthentication( .get(AuthenticationData.PARAM_LOGIN_REQUEST_BODY))); context.setAuthenticationMethod(jsonAuthMethod); break; + case AuthenticationData.METHOD_CLIENT: + // Nothing to do + break; case AuthenticationData.METHOD_SCRIPT: File f = JobUtils.getFile( @@ -308,9 +335,20 @@ public void initContextAuthentication( ScriptBasedAuthenticationMethodType.SCRIPT_TYPE_AUTH, parameters.getOrDefault(PARAM_SCRIPT_ENGINE, "").toString(), progress); - ScriptBasedAuthenticationMethodType scriptType = - new ScriptBasedAuthenticationMethodType(); - ScriptBasedAuthenticationMethod scriptMethod = + + AuthenticationMethodType scriptType; + + if (getMethod() + .toLowerCase(Locale.ROOT) + .equals(AuthenticationData.METHOD_SCRIPT)) { + scriptType = new ScriptBasedAuthenticationMethodType(); + LOGGER.debug("Loaded script auth method type"); + + } else { + scriptType = extAuth.getAuthenticationMethodTypeForIdentifier(8); + LOGGER.info("Loaded client script auth method type {}.", scriptType); + } + AuthenticationMethod scriptMethod = scriptType.createAuthenticationMethod(context.getId()); if (sw == null) { @@ -321,9 +359,13 @@ public void initContextAuthentication( "automation.error.env.auth.script.bad", f.getAbsolutePath())); } else { - scriptMethod.loadScript(sw); + try { + MethodUtils.invokeMethod(scriptMethod, "loadScript", sw); + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + } JobUtils.setPrivateField( - scriptMethod, "paramValues", getScriptParameters(env)); + scriptMethod, PARAM_VALUES, getScriptParameters(env)); reloadAuthenticationMethod(scriptMethod, progress); context.setAuthenticationMethod(scriptMethod); @@ -348,14 +390,14 @@ public void initContextAuthentication( Object browserIdObj = getParameters().get(AuthenticationData.PARAM_BROWSER_ID); - if (browserIdObj != null && browserIdObj instanceof String) { + if (browserIdObj != null && browserIdObj instanceof String browserIdStr) { JobUtils.setPrivateField( - am, AuthenticationData.PARAM_BROWSER_ID, (String) browserIdObj); + am, AuthenticationData.PARAM_BROWSER_ID, browserIdStr); } Object loginPageWaitObj = getParameters().get(AuthenticationData.PARAM_LOGIN_PAGE_WAIT); - if (loginPageWaitObj instanceof Integer) { - int loginPageWait = JobUtils.unBox((Integer) loginPageWaitObj); + if (loginPageWaitObj instanceof Integer loginPageWaitIgr) { + int loginPageWait = JobUtils.unBox(loginPageWaitIgr); if (loginPageWait > 0) { JobUtils.setPrivateField( am, @@ -370,7 +412,7 @@ public void initContextAuthentication( } else { progress.error( Constant.messages.getString( - "automation.error.env.auth.type.bad", getMethod())); + AUTOMATION_ERROR_ENV_AUTH_TYPE_BAD, getMethod())); } break; @@ -389,13 +431,13 @@ public void initContextAuthentication( } else { progress.error( Constant.messages.getString( - "automation.error.env.auth.type.bad", getMethod())); + AUTOMATION_ERROR_ENV_AUTH_TYPE_BAD, getMethod())); } break; default: progress.error( Constant.messages.getString( - "automation.error.env.auth.type.bad", getMethod())); + AUTOMATION_ERROR_ENV_AUTH_TYPE_BAD, getMethod())); break; } } diff --git a/addOns/zest/CHANGELOG.md b/addOns/zest/CHANGELOG.md index 57537bc98ac..99151eb0c4b 100644 --- a/addOns/zest/CHANGELOG.md +++ b/addOns/zest/CHANGELOG.md @@ -5,7 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased - +### Added +- Added support for Client Script Authentication. ## [48.1.0] - 2025-01-23 ### Changed diff --git a/addOns/zest/src/main/java/org/zaproxy/zap/extension/zest/ZestAuthenticationRunner.java b/addOns/zest/src/main/java/org/zaproxy/zap/extension/zest/ZestAuthenticationRunner.java index 3d5279b3b43..53f3c738a04 100644 --- a/addOns/zest/src/main/java/org/zaproxy/zap/extension/zest/ZestAuthenticationRunner.java +++ b/addOns/zest/src/main/java/org/zaproxy/zap/extension/zest/ZestAuthenticationRunner.java @@ -94,6 +94,13 @@ public String[] getCredentialsParamsNames() { return new String[] {USERNAME, PASSWORD}; } + HttpMessageHandler handler; + + public void registerHandler(HttpMessageHandler handler) { + LOGGER.debug("ZestAuthRunner register handler: {}", handler.getClass().getCanonicalName()); + this.handler = handler; + } + @Override public HttpMessage authenticate( AuthenticationHelper helper, @@ -110,7 +117,7 @@ public HttpMessage authenticate( getExtensionNetwork() .createHttpProxy( helper.getHttpSender(), - new ZestMessageHandler(this, helper)); + new ZestMessageHandler(this, helper, handler)); int port = proxyServer.start(PROXY_ADDRESS, Server.ANY_PORT); this.setProxy(PROXY_ADDRESS, port); } @@ -175,10 +182,13 @@ private static class ZestMessageHandler implements HttpMessageHandler { private final ZestBasicRunner runner; private final AuthenticationHelper helper; + private final HttpMessageHandler handler; - private ZestMessageHandler(ZestBasicRunner runner, AuthenticationHelper helper) { + private ZestMessageHandler( + ZestBasicRunner runner, AuthenticationHelper helper, HttpMessageHandler handler) { this.runner = runner; this.helper = helper; + this.handler = handler; } @Override @@ -199,6 +209,14 @@ public void handleMessage(HttpMessageHandlerContext ctx, HttpMessage msg) { ZestVariables.RESPONSE_URL, msg.getRequestHeader().getURI().toString()); runner.setVariable(ZestVariables.RESPONSE_HEADER, msg.getResponseHeader().toString()); runner.setVariable(ZestVariables.RESPONSE_BODY, msg.getResponseBody().toString()); + + if (handler != null) { + handler.handleMessage(ctx, msg); + } } } + + public ZestScriptWrapper getScript() { + return script; + } } diff --git a/addOns/zest/src/main/java/org/zaproxy/zap/extension/zest/dialogs/ZestRecordScriptDialog.java b/addOns/zest/src/main/java/org/zaproxy/zap/extension/zest/dialogs/ZestRecordScriptDialog.java index 2f531c867f5..1298ce323b1 100644 --- a/addOns/zest/src/main/java/org/zaproxy/zap/extension/zest/dialogs/ZestRecordScriptDialog.java +++ b/addOns/zest/src/main/java/org/zaproxy/zap/extension/zest/dialogs/ZestRecordScriptDialog.java @@ -24,6 +24,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Locale; import org.apache.logging.log4j.LogManager; @@ -146,6 +147,8 @@ private List getScriptTypes() { list.add(Constant.messages.getString(st.getI18nKey())); } } + // Alphabetic order best, and it just so happens Auth scripts will appear at the top + Collections.sort(list); return list; } @@ -173,13 +176,9 @@ private boolean isServerSide() { } private List getBrowsers() { - List browsers = new ArrayList<>(); - // String firefox = Browser.FIREFOX.getId(); - String chrome = Browser.CHROME.getId(); - - // browsers.add(Character.toUpperCase(firefox.charAt(0)) + firefox.substring(1)); - browsers.add(Character.toUpperCase(chrome.charAt(0)) + chrome.substring(1)); - return browsers; + return List.of( + ExtensionSelenium.getName(Browser.FIREFOX), + ExtensionSelenium.getName(Browser.CHROME)); } private List getSites() {