From 8d1964e67c9036002dfd5070325a1f26e1240a9d Mon Sep 17 00:00:00 2001 From: kingthorin Date: Tue, 2 Jul 2024 11:49:52 +0100 Subject: [PATCH] Client Script Based Auth (WIP) Signed-off-by: kingthorin --- addOns/authhelper/CHANGELOG.md | 3 + addOns/authhelper/authhelper.gradle.kts | 11 +- .../zaproxy/addon/authhelper/AuthUtils.java | 3 +- ...ntScriptBasedAuthenticationMethodType.java | 748 ++++++++++++++++++ .../client/ExtensionAuthhelperClient.java | 31 +- .../ClientScriptBasedAuthHandler.java | 113 +++ .../spiderajax/ExtensionAuthhelperAjax.java | 5 + .../authhelper/resources/Messages.properties | 2 + addOns/automation/CHANGELOG.md | 3 + .../addon/automation/AuthenticationData.java | 108 ++- .../zest/ZestAuthenticationRunner.java | 22 +- 11 files changed, 1028 insertions(+), 21 deletions(-) create mode 100644 addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/ClientScriptBasedAuthenticationMethodType.java create mode 100644 addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/spiderajax/ClientScriptBasedAuthHandler.java 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..cbf8716c796 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,13 @@ zapAddOn { dependencies { addOns { register("client") { - version.set(">=0.10.0") + version.set(">=0.11.0") + } + register("scripts") { + version.set(">=45.8.0") + } + register("zest") { + version.set(">=48.1.0") } } } @@ -69,6 +75,7 @@ dependencies { zapAddOn("selenium") zapAddOn("spiderAjax") zapAddOn("client") + zapAddOn("zest") 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/ClientScriptBasedAuthenticationMethodType.java b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/ClientScriptBasedAuthenticationMethodType.java new file mode 100644 index 00000000000..8255b4694cc --- /dev/null +++ b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/ClientScriptBasedAuthenticationMethodType.java @@ -0,0 +1,748 @@ +/* + * 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; + +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.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 { + LOGGER.warn("Expected authScript to be a Zest script"); + return null; + } + + 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..fa2d5eccab4 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,12 +20,17 @@ 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.authhelper.ClientScriptBasedAuthenticationMethodType; import org.zaproxy.addon.client.ExtensionClientIntegration; +import org.zaproxy.zap.extension.authentication.ExtensionAuthentication; public class ExtensionAuthhelperClient extends ExtensionAdaptor { @@ -33,9 +38,13 @@ public class ExtensionAuthhelperClient extends ExtensionAdaptor { private static final List> DEPENDENCIES = List.of(ExtensionClientIntegration.class); + private static final Logger LOGGER = LogManager.getLogger(ExtensionAuthhelperClient.class); private BrowserBasedAuthHandler authHandler; + protected static final ClientScriptBasedAuthenticationMethodType CLIENT_SCRIPT_BASED_AUTH_TYPE = + new ClientScriptBasedAuthenticationMethodType(); + public ExtensionAuthhelperClient() { super(NAME); } @@ -45,6 +54,11 @@ public boolean supportsDb(String type) { return true; } + @Override + public List> getDependencies() { + return DEPENDENCIES; + } + @Override public void hook(ExtensionHook extensionHook) { super.hook(extensionHook); @@ -59,18 +73,27 @@ 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); + } } @Override - public List> getDependencies() { - return DEPENDENCIES; + public boolean canUnload() { + return true; } @Override diff --git a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/spiderajax/ClientScriptBasedAuthHandler.java b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/spiderajax/ClientScriptBasedAuthHandler.java new file mode 100644 index 00000000000..660e6fb7db3 --- /dev/null +++ b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/spiderajax/ClientScriptBasedAuthHandler.java @@ -0,0 +1,113 @@ +/* + * 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.spiderajax; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.zaproxy.addon.authhelper.AuthUtils; +import org.zaproxy.addon.authhelper.ClientScriptBasedAuthenticationMethodType; +import org.zaproxy.addon.authhelper.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/spiderajax/ExtensionAuthhelperAjax.java b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/spiderajax/ExtensionAuthhelperAjax.java index 294790664b7..980de9cfe69 100644 --- a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/spiderajax/ExtensionAuthhelperAjax.java +++ b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/spiderajax/ExtensionAuthhelperAjax.java @@ -35,6 +35,7 @@ public class ExtensionAuthhelperAjax extends ExtensionAdaptor { List.of(ExtensionAjax.class); private BrowserBasedAuthHandler authHandler; + private ClientScriptBasedAuthHandler scriptAuthHandler; public ExtensionAuthhelperAjax() { super(NAME); @@ -52,6 +53,9 @@ public void hook(ExtensionHook extensionHook) { Control.getSingleton().getExtensionLoader().getExtension(ExtensionAjax.class); authHandler = new BrowserBasedAuthHandler(); extAjax.addAuthenticationHandler(authHandler); + + scriptAuthHandler = new ClientScriptBasedAuthHandler(); + extAjax.addAuthenticationHandler(scriptAuthHandler); } @Override @@ -64,6 +68,7 @@ public void unload() { ExtensionAjax extAjax = Control.getSingleton().getExtensionLoader().getExtension(ExtensionAjax.class); extAjax.removeAuthenticationHandler(authHandler); + extAjax.removeAuthenticationHandler(scriptAuthHandler); } @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..699787358c5 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,12 @@ import org.zaproxy.zap.utils.ZapXmlConfiguration; public class AuthenticationData extends AutomationData { + // TODO: Plan to change once the core supports dynamic methods better + protected static final String CLIENT_SCRIPT_BASED_AUTH_METHOD_CLASSNAME = + "org.zaproxy.addon.authhelper.client.ClientScriptBasedAuthenticationMethodType.ClientScriptBasedAuthenticationMethod"; + protected static final String BROWSER_BASED_AUTH_METHOD_CLASSNAME = + "org.zaproxy.addon.authhelper.BrowserBasedAuthenticationMethodType.BrowserBasedAuthenticationMethod"; + public static final String METHOD_HTTP = "http"; public static final String METHOD_FORM = "form"; public static final String METHOD_JSON = "json"; @@ -57,6 +64,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"; @@ -69,13 +77,11 @@ public class AuthenticationData extends AutomationData { public static final String PARAM_SCRIPT = "script"; public static final String PARAM_SCRIPT_ENGINE = "scriptEngine"; - protected static final String BROWSER_BASED_AUTH_METHOD_CLASSNAME = - "org.zaproxy.addon.authhelper.BrowserBasedAuthenticationMethodType.BrowserBasedAuthenticationMethod"; - /** 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 PRIVATE_FIELD_SCRIPT = "script"; public static final String VERIFICATION_ELEMENT = "verification"; @@ -87,7 +93,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<>(); @@ -121,10 +128,29 @@ public AuthenticationData(Context context) { 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)) { + 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, "paramValues"); + 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()); @@ -291,6 +317,49 @@ public void initContextAuthentication( .get(AuthenticationData.PARAM_LOGIN_REQUEST_BODY))); context.setAuthenticationMethod(jsonAuthMethod); break; + case AuthenticationData.METHOD_CLIENT: + File clientScript = JobUtils.getFile( + parameters.getOrDefault(PARAM_SCRIPT, "").toString(), + env.getPlan()); + if (!clientScript.exists() || !clientScript.canRead()) { + progress.error( + Constant.messages.getString( + "automation.error.env.sessionmgmt.script.bad", + clientScript.getAbsolutePath())); + } else { + ScriptWrapper sw = JobUtils.getScriptWrapper( + clientScript, + ScriptBasedAuthenticationMethodType.SCRIPT_TYPE_AUTH, + parameters.getOrDefault(PARAM_SCRIPT_ENGINE, "").toString(), + progress); + + AuthenticationMethodType clientScriptType = extAuth + .getAuthenticationMethodTypeForIdentifier(8); + LOGGER.info("Loaded client script auth method type {}.", clientScriptType); + AuthenticationMethod scriptMethod = clientScriptType + .createAuthenticationMethod(context.getId()); + + if (sw == null) { + LOGGER.error( + "Error setting script authentication - failed to find script wrapper"); + progress.error( + Constant.messages.getString( + "automation.error.env.auth.script.bad", + clientScript.getAbsolutePath())); + } else { + try { + MethodUtils.invokeMethod(scriptMethod, "loadScript", sw); + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + } + JobUtils.setPrivateField( + scriptMethod, "paramValues", getScriptParameters(env)); + + reloadAuthenticationMethod(scriptMethod, progress); + context.setAuthenticationMethod(scriptMethod); + } + } + break; case AuthenticationData.METHOD_SCRIPT: File f = JobUtils.getFile( @@ -308,9 +377,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,7 +401,11 @@ 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)); 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; + } }