diff --git a/addOns/authhelper/CHANGELOG.md b/addOns/authhelper/CHANGELOG.md index 623057ce252..e49915e4bc0 100644 --- a/addOns/authhelper/CHANGELOG.md +++ b/addOns/authhelper/CHANGELOG.md @@ -4,7 +4,8 @@ All notable changes to this add-on will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased - +### Changed +- Use TOTP data defined under user credentials for Client Script and Browser Based Authentication, when available. ## [0.24.0] - 2025-03-21 ### Added diff --git a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/AuthTestDialog.java b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/AuthTestDialog.java index 0a5449f2ee2..6ab50b1e950 100644 --- a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/AuthTestDialog.java +++ b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/AuthTestDialog.java @@ -24,6 +24,8 @@ import java.awt.Insets; import java.awt.Toolkit; import java.awt.datatransfer.StringSelection; +import java.util.List; +import java.util.Optional; import javax.swing.BorderFactory; import javax.swing.Icon; import javax.swing.ImageIcon; @@ -46,6 +48,7 @@ import org.zaproxy.addon.authhelper.BrowserBasedAuthenticationMethodType.BrowserBasedAuthenticationMethod; import org.zaproxy.addon.authhelper.internal.AuthenticationStep; import org.zaproxy.addon.authhelper.internal.StepsPanel; +import org.zaproxy.addon.commonlib.internal.TotpSupport; import org.zaproxy.addon.pscan.ExtensionPassiveScan2; import org.zaproxy.zap.ZAP; import org.zaproxy.zap.authentication.AuthenticationMethod; @@ -297,7 +300,9 @@ private void authenticate() { // Set up user User user = new User(context.getId(), username); UsernamePasswordAuthenticationCredentials upCreds = - new UsernamePasswordAuthenticationCredentials(username, password); + TotpSupport.createUsernamePasswordAuthenticationCredentials( + am, username, password); + setTotp(stepsPanel.getSteps(), upCreds); user.setAuthenticationCredentials(upCreds); user.setEnabled(true); Control.getSingleton() @@ -440,6 +445,30 @@ public void counterInc(String site, String key) { } } + private void setTotp( + List steps, UsernamePasswordAuthenticationCredentials credentials) { + if (!TotpSupport.isTotpInCore()) { + return; + } + + Optional optStep = + steps.stream() + .filter(e -> e.getType() == AuthenticationStep.Type.TOTP_FIELD) + .findFirst(); + if (optStep.isEmpty()) { + return; + } + + AuthenticationStep totpStep = optStep.get(); + TotpSupport.TotpData totpData = + new TotpSupport.TotpData( + totpStep.getTotpSecret(), + totpStep.getTotpPeriod(), + totpStep.getTotpDigits(), + totpStep.getTotpAlgorithm()); + TotpSupport.setTotpData(totpData, credentials); + } + private void reloadAuthenticationMethod(AuthenticationMethod am) throws ConfigurationException { // OK, this does look weird, but it is the easiest way to actually get // the session management data loaded :/ 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 d757a8ae427..24a6bba11cd 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 @@ -397,8 +397,6 @@ static boolean authenticateAsUserImpl( List steps) { UsernamePasswordAuthenticationCredentials credentials = getCredentials(user); - String username = credentials.getUsername(); - String password = credentials.getPassword(); Context context = user.getContext(); @@ -406,7 +404,7 @@ static boolean authenticateAsUserImpl( wd.get(loginPageUrl); boolean auth = internalAuthenticateAsUser( - diags, wd, context, loginPageUrl, username, password, waitInSecs, steps); + diags, wd, context, loginPageUrl, credentials, waitInSecs, steps); if (auth) { return true; @@ -430,14 +428,7 @@ static boolean authenticateAsUserImpl( sleep(AUTH_PAGE_SLEEP_IN_MSECS); auth = internalAuthenticateAsUser( - diags, - wd, - context, - loginPageUrl, - username, - password, - waitInSecs, - steps); + diags, wd, context, loginPageUrl, credentials, waitInSecs, steps); if (auth) { return true; } @@ -460,8 +451,7 @@ private static boolean internalAuthenticateAsUser( WebDriver wd, Context context, String loginPageUrl, - String username, - String password, + UsernamePasswordAuthenticationCredentials credentials, int waitInSecs, List steps) { @@ -472,6 +462,9 @@ private static boolean internalAuthenticateAsUser( sleep(DEMO_SLEEP_IN_MSECS); } + String username = credentials.getUsername(); + String password = credentials.getPassword(); + WebElement userField = null; WebElement pwdField = null; boolean userAdded = false; @@ -488,7 +481,7 @@ private static boolean internalAuthenticateAsUser( break; } - WebElement element = step.execute(wd, username, password); + WebElement element = step.execute(wd, credentials); diags.recordStep(wd, step.getDescription(), element); switch (step.getType()) { @@ -552,7 +545,7 @@ private static boolean internalAuthenticateAsUser( continue; } - step.execute(wd, username, password); + step.execute(wd, credentials); diags.recordStep(wd, step.getDescription()); sleep(demoMode ? DEMO_SLEEP_IN_MSECS : TIME_TO_SLEEP_IN_MSECS); diff --git a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/AutoDetectSessionManagementMethodType.java b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/AutoDetectSessionManagementMethodType.java index d862b73dd9d..71714a184b8 100644 --- a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/AutoDetectSessionManagementMethodType.java +++ b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/AutoDetectSessionManagementMethodType.java @@ -167,7 +167,7 @@ public void importData(Configuration config, SessionManagementMethod sessionMeth @Override public ApiDynamicActionImplementor getSetMethodForContextApiAction() { - return new ApiDynamicActionImplementor(API_METHOD_NAME, null, null) { + return new ApiDynamicActionImplementor(API_METHOD_NAME, (String[]) null, (String[]) null) { @Override public void handleAction(JSONObject params) throws ApiException { diff --git a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/BrowserBasedAuthenticationMethodType.java b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/BrowserBasedAuthenticationMethodType.java index 9dbeb1c358d..8ef46b7302e 100644 --- a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/BrowserBasedAuthenticationMethodType.java +++ b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/BrowserBasedAuthenticationMethodType.java @@ -60,6 +60,7 @@ import org.zaproxy.addon.authhelper.internal.AuthenticationStep.ValidationResult; import org.zaproxy.addon.authhelper.internal.ClientSideHandler; import org.zaproxy.addon.authhelper.internal.StepsPanel; +import org.zaproxy.addon.commonlib.internal.TotpSupport; import org.zaproxy.addon.network.ExtensionNetwork; import org.zaproxy.addon.network.internal.client.apachev5.HttpSenderContextApache; import org.zaproxy.addon.network.server.HttpServerConfig; @@ -224,7 +225,7 @@ protected AuthenticationMethod duplicate() { @Override public AuthenticationCredentials createAuthenticationCredentials() { - return new UsernamePasswordAuthenticationCredentials(); + return TotpSupport.createUsernamePasswordAuthenticationCredentials(); } @Override 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 index 84f30fc6764..6deb0c7ece7 100644 --- a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/ClientScriptBasedAuthenticationMethodType.java +++ b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/ClientScriptBasedAuthenticationMethodType.java @@ -50,6 +50,7 @@ import org.parosproxy.paros.network.HttpSender; import org.parosproxy.paros.view.View; import org.zaproxy.addon.authhelper.internal.ClientSideHandler; +import org.zaproxy.addon.commonlib.internal.TotpSupport; import org.zaproxy.addon.network.server.HttpMessageHandler; import org.zaproxy.zap.authentication.AbstractAuthenticationMethodOptionsPanel; import org.zaproxy.zap.authentication.AuthenticationCredentials; @@ -246,7 +247,7 @@ public boolean validateCreationOfAuthenticationCredentials() { @Override public AuthenticationCredentials createAuthenticationCredentials() { - return new GenericAuthenticationCredentials(this.credentialsParamNames); + return TotpSupport.createGenericAuthenticationCredentials(credentialsParamNames); } @Override diff --git a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/ExtensionAuthhelper.java b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/ExtensionAuthhelper.java index ffa6a8c0c45..e404602a8ae 100644 --- a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/ExtensionAuthhelper.java +++ b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/ExtensionAuthhelper.java @@ -19,14 +19,19 @@ */ package org.zaproxy.addon.authhelper; +import com.bastiaanjansen.otp.HMACAlgorithm; +import com.bastiaanjansen.otp.TOTPGenerator; import java.awt.EventQueue; import java.awt.event.KeyEvent; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; +import java.time.Duration; +import java.time.Instant; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Stream; import javax.swing.ImageIcon; import org.apache.commons.configuration.Configuration; import org.apache.commons.lang3.StringUtils; @@ -47,6 +52,9 @@ import org.parosproxy.paros.network.HttpMessage; import org.parosproxy.paros.view.View; import org.zaproxy.addon.authhelper.internal.db.TableJdo; +import org.zaproxy.addon.commonlib.internal.TotpSupport; +import org.zaproxy.addon.commonlib.internal.TotpSupport.TotpData; +import org.zaproxy.addon.commonlib.internal.TotpSupport.TotpGenerator; import org.zaproxy.addon.pscan.ExtensionPassiveScan2; import org.zaproxy.zap.authentication.FormBasedAuthenticationMethodType; import org.zaproxy.zap.authentication.JsonBasedAuthenticationMethodType; @@ -113,6 +121,35 @@ public List> getDependencies() { return EXTENSION_DEPENDENCIES; } + @Override + public void init() { + List supportedAlgorithms = + Stream.of(HMACAlgorithm.values()).map(HMACAlgorithm::name).toList(); + + TotpSupport.setTotpGenerator( + new TotpGenerator() { + + @Override + public List getSupportedAlgorithms() { + return supportedAlgorithms; + } + + @Override + public String generate(TotpData data, Instant when) { + return new TOTPGenerator.Builder(data.secret()) + .withHOTPGenerator( + builder -> + builder.withAlgorithm( + HMACAlgorithm.valueOf( + data.algorithm())) + .withPasswordLength(data.digits())) + .withPeriod(Duration.ofSeconds(data.period())) + .build() + .at(when); + } + }); + } + public AuthhelperParam getParam() { if (param == null) { param = new AuthhelperParam(); @@ -149,6 +186,8 @@ public void unload() { AuthUtils.disableBrowserAuthentication(); BrowserBasedAuthenticationMethodType.stopProxies(); AuthUtils.clean(); + + TotpSupport.setTotpGenerator(null); } @Override diff --git a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/internal/AuthenticationStep.java b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/internal/AuthenticationStep.java index b20997324c2..d5b2bc69173 100644 --- a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/internal/AuthenticationStep.java +++ b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/internal/AuthenticationStep.java @@ -43,6 +43,8 @@ import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; import org.parosproxy.paros.Constant; +import org.zaproxy.addon.commonlib.internal.TotpSupport; +import org.zaproxy.zap.authentication.UsernamePasswordAuthenticationCredentials; import org.zaproxy.zap.utils.EnableableInterface; import org.zaproxy.zap.utils.Orderable; @@ -177,7 +179,7 @@ public void setOrder(int order) { this.order = order; } - public WebElement execute(WebDriver wd, String username, String password) { + public WebElement execute(WebDriver wd, UsernamePasswordAuthenticationCredentials credentials) { By by = createtBy(); WebElement element = @@ -198,7 +200,7 @@ public WebElement execute(WebDriver wd, String username, String password) { break; case PASSWORD: - element.sendKeys(password); + element.sendKeys(credentials.getPassword()); break; case RETURN: @@ -206,11 +208,11 @@ public WebElement execute(WebDriver wd, String username, String password) { break; case TOTP_FIELD: - element.sendKeys(getTotpCode()); + element.sendKeys(getTotpCode(credentials)); break; case USERNAME: - element.sendKeys(username); + element.sendKeys(credentials.getUsername()); break; default: @@ -220,7 +222,13 @@ public WebElement execute(WebDriver wd, String username, String password) { return element; } - private CharSequence getTotpCode() { + private CharSequence getTotpCode(UsernamePasswordAuthenticationCredentials credentials) { + CharSequence code = TotpSupport.getCode(credentials); + if (code != null) { + return code; + } + + // Fallback to data from the step for now. return new TOTPGenerator.Builder(totpSecret) .withHOTPGenerator( builder -> diff --git a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/internal/DialogAddStep.java b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/internal/DialogAddStep.java index dfc86a21a6d..eae9d82489c 100644 --- a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/internal/DialogAddStep.java +++ b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/internal/DialogAddStep.java @@ -33,6 +33,7 @@ import javax.swing.JOptionPane; import javax.swing.JPanel; import org.parosproxy.paros.Constant; +import org.zaproxy.addon.commonlib.internal.TotpSupport; import org.zaproxy.zap.utils.ZapNumberSpinner; import org.zaproxy.zap.utils.ZapTextField; import org.zaproxy.zap.view.AbstractFormDialog; @@ -177,6 +178,17 @@ protected JPanel getFieldsPanel() { } public void setEnableable(boolean enableable) { + if (TotpSupport.isTotpInCore()) { + totpSecretLabel.setVisible(enableable); + getTotpSecretTextField().setVisible(enableable); + totpPeriodLabel.setVisible(enableable); + getTotpPeriodNumberSpinner().setVisible(enableable); + totpDigitsLabel.setVisible(enableable); + getTotpDigitsNumberSpinner().setVisible(enableable); + totpAlgorithmLabel.setVisible(enableable); + getTotpAlgorithmComboBox().setVisible(enableable); + } + getEnabledLabel().setVisible(enableable); getEnabledCheckBox().setVisible(enableable); if (!enableable) { @@ -278,6 +290,10 @@ private AuthenticationStep createStep() { newStep.setValue(getValueTextField().getText()); newStep.setTimeout(getTimeoutNumberSpinner().getValue()); + if (TotpSupport.isTotpInCore() && !getTotpSecretTextField().isVisible()) { + // Secret is read from the user credentials. + getTotpSecretTextField().setText("UserCredentials"); + } newStep.setTotpSecret(getTotpSecretTextField().getText()); newStep.setTotpPeriod(getTotpPeriodNumberSpinner().getValue()); newStep.setTotpDigits(getTotpDigitsNumberSpinner().getValue()); diff --git a/addOns/automation/CHANGELOG.md b/addOns/automation/CHANGELOG.md index 2ad5410319a..a8c1ddf5ce6 100644 --- a/addOns/automation/CHANGELOG.md +++ b/addOns/automation/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased ### Added - Document how the TOTP data is defined for a user. +- Use TOTP data defined under user credentials when creating or setting up a context. ### Changed - Progress and log messages with regard to setting scan rule threshold or strength no longer include commas in scan rule ID numbers. diff --git a/addOns/automation/src/main/java/org/zaproxy/addon/automation/ContextWrapper.java b/addOns/automation/src/main/java/org/zaproxy/addon/automation/ContextWrapper.java index 447c541405a..b53f5037b7a 100644 --- a/addOns/automation/src/main/java/org/zaproxy/addon/automation/ContextWrapper.java +++ b/addOns/automation/src/main/java/org/zaproxy/addon/automation/ContextWrapper.java @@ -44,6 +44,8 @@ import org.parosproxy.paros.control.Control; import org.parosproxy.paros.model.Session; import org.zaproxy.addon.automation.jobs.JobUtils; +import org.zaproxy.addon.commonlib.internal.TotpSupport; +import org.zaproxy.zap.authentication.AuthenticationCredentials; import org.zaproxy.zap.authentication.AuthenticationMethod; import org.zaproxy.zap.authentication.FormBasedAuthenticationMethodType.FormBasedAuthenticationMethod; import org.zaproxy.zap.authentication.GenericAuthenticationCredentials; @@ -104,9 +106,11 @@ public ContextWrapper(Context context) { UsernamePasswordAuthenticationCredentials upCreds = (UsernamePasswordAuthenticationCredentials) user.getAuthenticationCredentials(); - users.add( + UserData ud = new UserData( - user.getName(), upCreds.getUsername(), upCreds.getPassword())); + user.getName(), upCreds.getUsername(), upCreds.getPassword()); + setTotpData(upCreds, ud); + users.add(ud); } else if (user.getAuthenticationCredentials() instanceof GenericAuthenticationCredentials) { GenericAuthenticationCredentials genCreds = @@ -116,6 +120,8 @@ public ContextWrapper(Context context) { (Map) JobUtils.getPrivateField(genCreds, "paramValues"); UserData ud = new UserData(user.getName()); ud.setCredentials(paramValues); + setTotpData(genCreds, ud); + ud.getInternalCredentials().setTotp(null); users.add(ud); } else if (MANUAL_AUTH_CREDS_CANONICAL_NAME.equals( user.getAuthenticationCredentials().getClass().getCanonicalName())) { @@ -134,6 +140,20 @@ public ContextWrapper(Context context) { this.data.setAuthentication(new AuthenticationData(context, getData().getUsers())); } + private static void setTotpData(AuthenticationCredentials credentials, UserData ud) { + TotpSupport.TotpData coreData = TotpSupport.getTotpData(credentials); + if (coreData == null) { + return; + } + + UserData.TotpData totpData = new UserData.TotpData(); + totpData.setSecret(coreData.secret()); + totpData.setPeriod(String.valueOf(coreData.period())); + totpData.setDigits(String.valueOf(coreData.digits())); + totpData.setAlgorithm(coreData.algorithm()); + ud.getInternalCredentials().setTotp(totpData); + } + public ContextWrapper( Map contextData, AutomationEnvironment env, AutomationProgress progress) { this.data = new Data(); @@ -398,25 +418,25 @@ private void initContextUsers(Context context, AutomationEnvironment env) { .getCanonicalName() .equals(AuthenticationData.BROWSER_BASED_AUTH_METHOD_CLASSNAME)) { UsernamePasswordAuthenticationCredentials upCreds = - new UsernamePasswordAuthenticationCredentials( + TotpSupport.createUsernamePasswordAuthenticationCredentials( + authMethod, env.replaceVars(ud.getCredential(UserData.USERNAME_CREDENTIAL)), env.replaceVars( ud.getCredential(UserData.PASSWORD_CREDENTIAL))); + + setTotpData(ud, upCreds, env); user.setAuthenticationCredentials(upCreds); } else if (authMethod instanceof ManualAuthenticationMethod) { user.setAuthenticationCredentials( authMethod.getType().createAuthenticationCredentials()); } else if (authMethod instanceof ScriptBasedAuthenticationMethod) { - ScriptBasedAuthenticationMethod scriptMethod = - (ScriptBasedAuthenticationMethod) authMethod; - String[] credName = - (String[]) - JobUtils.getPrivateField(scriptMethod, "credentialsParamNames"); GenericAuthenticationCredentials genCreds = - new GenericAuthenticationCredentials(credName); + (GenericAuthenticationCredentials) + authMethod.createAuthenticationCredentials(); for (Entry cred : ud.getCredentials().entrySet()) { genCreds.setParam(cred.getKey(), env.replaceVars(cred.getValue())); } + setTotpData(ud, genCreds, env); user.setAuthenticationCredentials(genCreds); } else { LOGGER.error( @@ -428,6 +448,39 @@ private void initContextUsers(Context context, AutomationEnvironment env) { } } + private static void setTotpData( + UserData ud, AuthenticationCredentials credentials, AutomationEnvironment env) { + if (ud.getInternalCredentials().getTotp() == null) { + return; + } + + String algorithm = env.replaceVars(ud.getInternalCredentials().getTotp().getAlgorithm()); + TotpSupport.TotpData totpData = + new TotpSupport.TotpData( + env.replaceVars(ud.getInternalCredentials().getTotp().getSecret()), + getInt( + env.replaceVars(ud.getInternalCredentials().getTotp().getPeriod()), + 30), + getInt( + env.replaceVars(ud.getInternalCredentials().getTotp().getDigits()), + 6), + algorithm == null ? "SHA1" : algorithm); + TotpSupport.setTotpData(totpData, credentials); + } + + private static int getInt(String value, int defaultValue) { + if (value == null) { + return defaultValue; + } + + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + LOGGER.warn("An error occurred while parsing: {}", value, e); + } + return defaultValue; + } + public List getUserNames() { List userNames = new ArrayList<>(); if (this.getData().getUsers() != null) { diff --git a/addOns/commonlib/CHANGELOG.md b/addOns/commonlib/CHANGELOG.md index 1c98987e1f5..16ee3655f69 100644 --- a/addOns/commonlib/CHANGELOG.md +++ b/addOns/commonlib/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added - Replace the default Output panel with a tabbed version to allow multiple sources of output to be displayed in separate tabs. +- Add support functionality for usage of TOTP data defined under user credentials. ## [1.30.0] - 2025-01-09 ### Added diff --git a/addOns/commonlib/src/main/java/org/zaproxy/addon/commonlib/internal/TotpSupport.java b/addOns/commonlib/src/main/java/org/zaproxy/addon/commonlib/internal/TotpSupport.java new file mode 100644 index 00000000000..734e96c4f98 --- /dev/null +++ b/addOns/commonlib/src/main/java/org/zaproxy/addon/commonlib/internal/TotpSupport.java @@ -0,0 +1,282 @@ +/* + * 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.commonlib.internal; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.time.Instant; +import java.util.List; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.zaproxy.zap.authentication.AuthenticationCredentials; +import org.zaproxy.zap.authentication.AuthenticationMethod; +import org.zaproxy.zap.authentication.GenericAuthenticationCredentials; +import org.zaproxy.zap.authentication.UsernamePasswordAuthenticationCredentials; + +public final class TotpSupport { + + private static final Logger LOGGER = LogManager.getLogger(TotpSupport.class); + + private static Class tacClass; + private static Method isTotpEnabledMethod; + private static Method getTotpDataMethod; + + private static Class totpGeneratorClass; + private static Method setTotpDataMethod; + + private static Method getTotpCodeMethod; + private static Method setTotpGeneratorMethod; + private static Method secretMethod; + private static Method periodMethod; + private static Method digitsMethod; + private static Method algorithmMethod; + + private static Constructor totpDataConstructor; + private static Constructor upCredentialsConstructor; + private static Constructor genericCredentialsConstructor; + + static { + try { + tacClass = + Class.forName("org.zaproxy.zap.authentication.TotpAuthenticationCredentials"); + isTotpEnabledMethod = tacClass.getMethod("isTotpEnabled"); + getTotpDataMethod = tacClass.getMethod("getTotpData"); + + totpGeneratorClass = + Class.forName( + "org.zaproxy.zap.authentication.TotpAuthenticationCredentials$TotpGenerator"); + Class totpDataClass = + Class.forName( + "org.zaproxy.zap.authentication.TotpAuthenticationCredentials$TotpData"); + totpDataConstructor = + totpDataClass.getDeclaredConstructor( + String.class, int.class, int.class, String.class); + + setTotpDataMethod = tacClass.getMethod("setTotpData", totpDataClass); + + secretMethod = totpDataClass.getMethod("secret"); + periodMethod = totpDataClass.getMethod("period"); + digitsMethod = totpDataClass.getMethod("digits"); + algorithmMethod = totpDataClass.getMethod("algorithm"); + + Class acClass = + Class.forName("org.zaproxy.zap.authentication.AuthenticationCredentials"); + + getTotpCodeMethod = acClass.getMethod("getTotpCode", Instant.class); + setTotpGeneratorMethod = tacClass.getMethod("setTotpGenerator", totpGeneratorClass); + + upCredentialsConstructor = + UsernamePasswordAuthenticationCredentials.class.getConstructor( + String.class, String.class, boolean.class); + genericCredentialsConstructor = + GenericAuthenticationCredentials.class.getConstructor( + String[].class, boolean.class); + } catch (Exception e) { + LOGGER.debug("An error occurred while getting the method:", e); + } + } + + public static boolean isTotpInCore() { + return tacClass != null; + } + + public static String getCode(AuthenticationCredentials credentials) { + if (getTotpCodeMethod == null) { + return null; + } + + try { + if (hasTotpSecret(credentials)) { + return (String) getTotpCodeMethod.invoke(credentials, Instant.now()); + } + } catch (Exception e) { + LOGGER.warn("An error occurred while getting the TOTP code:", e); + } + + return null; + } + + private static boolean hasTotpSecret(AuthenticationCredentials credentials) { + if (!isTotpCredentials(credentials)) { + return false; + } + + try { + Object coreData = getTotpCoreData(credentials); + return StringUtils.isNotEmpty((String) secretMethod.invoke(coreData)); + } catch (Exception e) { + LOGGER.warn("An error occurred while checking the secret:", e); + } + return false; + } + + private static Object getTotpCoreData(AuthenticationCredentials credentials) throws Exception { + return getTotpDataMethod.invoke(credentials); + } + + public static void setTotpData(TotpData data, AuthenticationCredentials credentials) { + if (setTotpDataMethod == null) { + return; + } + + try { + if (!tacClass.isAssignableFrom(credentials.getClass())) { + return; + } + + Object coreTotpData = + totpDataConstructor.newInstance( + data.secret(), data.period(), data.digits(), data.algorithm()); + + setTotpDataMethod.invoke(credentials, coreTotpData); + } catch (Exception e) { + LOGGER.warn("An error occurred while creating TOTP enabled credentials:", e); + } + } + + public static UsernamePasswordAuthenticationCredentials + createUsernamePasswordAuthenticationCredentials() { + return createUsernamePasswordAuthenticationCredentials(null, null, null); + } + + public static UsernamePasswordAuthenticationCredentials + createUsernamePasswordAuthenticationCredentials( + AuthenticationMethod authMethod, String username, String password) { + if (upCredentialsConstructor != null && hasTotpEnabled(authMethod)) { + try { + return upCredentialsConstructor.newInstance(username, password, true); + } catch (Exception e) { + LOGGER.warn("An error occurred while creating TOTP enabled credentials:", e); + } + } + return new UsernamePasswordAuthenticationCredentials(username, password); + } + + private static boolean hasTotpEnabled(AuthenticationMethod authMethod) { + if (authMethod == null) { + return true; + } + + return isTotpCredentials(authMethod.createAuthenticationCredentials()); + } + + private static boolean isTotpCredentials(AuthenticationCredentials creds) { + if (tacClass == null) { + return false; + } + + try { + return tacClass.isAssignableFrom(creds.getClass()) + && (boolean) isTotpEnabledMethod.invoke(creds); + + } catch (Exception e) { + LOGGER.warn("An error occurred while checking if TOTP enabled credentials:", e); + } + return false; + } + + public static GenericAuthenticationCredentials createGenericAuthenticationCredentials( + String[] credentialsParamNames) { + if (genericCredentialsConstructor != null) { + try { + return genericCredentialsConstructor.newInstance(credentialsParamNames, true); + } catch (Exception e) { + LOGGER.warn("An error occurred while creating TOTP enabled credentials:", e); + } + } + return new GenericAuthenticationCredentials(credentialsParamNames); + } + + public static void setTotpGenerator(TotpGenerator generator) { + if (setTotpGeneratorMethod == null) { + return; + } + + try { + if (generator != null) { + InvocationHandler invocationHandler = + (o, method, args) -> { + switch (method.getName()) { + case "generate": + return generator.generate( + convertData(args[0]), (Instant) args[1]); + + case "getSupportedAlgorithms": + return generator.getSupportedAlgorithms(); + + default: + return null; + } + }; + + setTotpGeneratorMethod.invoke( + null, + Proxy.newProxyInstance( + TotpSupport.class.getClassLoader(), + new Class[] {totpGeneratorClass}, + invocationHandler)); + } else { + setTotpGeneratorMethod.invoke(null, (Object) null); + } + + } catch (Exception e) { + LOGGER.warn("An error occurred while setting the generator:", e); + } + } + + private static TotpData convertData(Object coreData) { + try { + return new TotpData( + (String) secretMethod.invoke(coreData), + (int) periodMethod.invoke(coreData), + (int) digitsMethod.invoke(coreData), + (String) algorithmMethod.invoke(coreData)); + + } catch (Exception e) { + LOGGER.warn("An error occurred while convert the TOTP data:", e); + } + return null; + } + + public static record TotpData(String secret, int period, int digits, String algorithm) {} + + public interface TotpGenerator { + + String generate(TotpData data, Instant when); + + List getSupportedAlgorithms(); + } + + public static TotpData getTotpData(AuthenticationCredentials credentials) { + if (!hasTotpSecret(credentials)) { + return null; + } + + try { + return convertData(getTotpCoreData(credentials)); + } catch (Exception e) { + LOGGER.warn("An error occurred while getting the TOTP data:", e); + } + return null; + } +} diff --git a/addOns/zest/CHANGELOG.md b/addOns/zest/CHANGELOG.md index f18b6fe1640..692459be682 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 - +### Changed +- Use TOTP data defined under user credentials during authentication when available. ## [48.4.0] - 2025-02-27 ### 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 e7bbc5345f1..a3e5be8bbd5 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 @@ -29,6 +29,7 @@ import org.apache.logging.log4j.Logger; import org.parosproxy.paros.network.HttpHeader; import org.parosproxy.paros.network.HttpMessage; +import org.zaproxy.addon.commonlib.internal.TotpSupport; import org.zaproxy.addon.network.ExtensionNetwork; import org.zaproxy.addon.network.server.HttpMessageHandler; import org.zaproxy.addon.network.server.HttpMessageHandlerContext; @@ -37,9 +38,16 @@ import org.zaproxy.zap.authentication.AuthenticationHelper; import org.zaproxy.zap.authentication.GenericAuthenticationCredentials; import org.zaproxy.zap.authentication.ScriptBasedAuthenticationMethodType.AuthenticationScript; +import org.zaproxy.zest.core.v1.ZestActionFailException; +import org.zaproxy.zest.core.v1.ZestAssertFailException; +import org.zaproxy.zest.core.v1.ZestAssignFailException; import org.zaproxy.zest.core.v1.ZestClient; +import org.zaproxy.zest.core.v1.ZestClientElementSendKeys; +import org.zaproxy.zest.core.v1.ZestClientFailException; +import org.zaproxy.zest.core.v1.ZestInvalidCommonTestException; import org.zaproxy.zest.core.v1.ZestRequest; import org.zaproxy.zest.core.v1.ZestResponse; +import org.zaproxy.zest.core.v1.ZestScript; import org.zaproxy.zest.core.v1.ZestStatement; import org.zaproxy.zest.core.v1.ZestVariables; import org.zaproxy.zest.impl.ZestBasicRunner; @@ -48,11 +56,15 @@ public class ZestAuthenticationRunner extends ZestZapRunner implements Authentic private static final Logger LOGGER = LogManager.getLogger(ZestAuthenticationRunner.class); + private static final String TOTP_VAR_NAME = "TOTP"; + private static final String PROXY_ADDRESS = "127.0.0.1"; private static final String USERNAME = "Username"; private static final String PASSWORD = "Password"; + private final String totpVar; + private ZestScriptWrapper script = null; private AuthenticationHelper helper; @@ -60,6 +72,10 @@ public ZestAuthenticationRunner( ExtensionZest extension, ExtensionNetwork extensionNetwork, ZestScriptWrapper script) { super(extension, extensionNetwork, script); this.script = script; + totpVar = + script.getZestScript().getParameters().getTokenStart() + + TOTP_VAR_NAME + + script.getZestScript().getParameters().getTokenEnd(); } @Override @@ -179,6 +195,24 @@ private boolean hasClientStatements() { return false; } + @Override + public ZestResponse runStatement( + ZestScript script, ZestStatement stmt, ZestResponse lastResponse) + throws ZestAssertFailException, + ZestActionFailException, + ZestInvalidCommonTestException, + IOException, + ZestAssignFailException, + ZestClientFailException { + if (stmt instanceof ZestClientElementSendKeys sendKeys + && totpVar.equals(sendKeys.getValue())) { + String code = + TotpSupport.getCode(helper.getRequestingUser().getAuthenticationCredentials()); + setVariable(TOTP_VAR_NAME, code); + } + return super.runStatement(script, stmt, lastResponse); + } + @Override public ZestResponse send(ZestRequest request) throws IOException { HttpMessage msg = ZestZapUtils.toHttpMessage(request, null); diff --git a/addOns/zest/src/main/javahelp/org/zaproxy/zap/extension/zest/resources/help/contents/zest.html b/addOns/zest/src/main/javahelp/org/zaproxy/zap/extension/zest/resources/help/contents/zest.html index 8ec968a39ea..c7d6ee3b9aa 100644 --- a/addOns/zest/src/main/javahelp/org/zaproxy/zap/extension/zest/resources/help/contents/zest.html +++ b/addOns/zest/src/main/javahelp/org/zaproxy/zap/extension/zest/resources/help/contents/zest.html @@ -90,6 +90,9 @@

Editing Zest scripts

Zest includes a set of 'built in' variables as well as allowing you to declare your own.
A right click menu is provided (where relevant) in the edit dialogs to allow you to paste in any of the available variable names.
+

Authentication Scripts

+Authentication scripts can use a custom ZAP variable, called TOTP, to send a value to an input field with a TOTP code generated from the user's credentials TOTP data. +

External links