Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JSON-based authentication method #4624

Merged
merged 1 commit into from Apr 25, 2018

Conversation

thc202
Copy link
Member

@thc202 thc202 commented Apr 24, 2018

Add an authentication method, JsonBasedAuthenticationMethodType, that
allows to send a JSON object in the request body. It can be configured
through the API with the name "jsonBasedAuthentication".
Extract a class, PostBasedAuthenticationMethodType, to be used by
FormBasedAuthenticationMethodType and the new auth method, the behaviour
is exactly the same except for the data they handle (application/json or
application/x-www-form-urlencoded) and the POST data required.
Add libraries used to quote the authentication credentials when injected
into the request body.
Add JSON Content-Type to HttpHeader.

Fix #2439 - Configure Authentication with Json object

@thc202
Copy link
Member Author

thc202 commented Apr 24, 2018

If it helps here's the diff of the extracted class:

diff --git a/FormBasedAuthenticationMethodType.java b/PostBasedAuthenticationMethodType.java
index d2fd00212..104ab9266 100644
--- a/FormBasedAuthenticationMethodType.java
+++ b/PostBasedAuthenticationMethodType.java
@@ -1,37 +1,35 @@
 /*
  * Zed Attack Proxy (ZAP) and its related class files.
- * 
+ *
  * ZAP is an HTTP/HTTPS proxy for assessing web application security.
- * 
- * Copyright 2013 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. 
+ *
+ * Copyright 2018 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.zap.authentication;
 
 import java.awt.Component;
 import java.awt.GridBagLayout;
+import java.awt.event.FocusAdapter;
 import java.awt.event.FocusEvent;
-import java.awt.event.FocusListener;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
-import java.net.URLDecoder;
 import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
+import java.util.function.UnaryOperator;
 
 import javax.swing.DefaultComboBoxModel;
 import javax.swing.ImageIcon;
@@ -60,8 +58,6 @@ import org.parosproxy.paros.extension.ExtensionHook;
 import org.parosproxy.paros.model.Model;
 import org.parosproxy.paros.model.Session;
 import org.parosproxy.paros.model.SiteNode;
-import org.parosproxy.paros.network.HtmlParameter;
-import org.parosproxy.paros.network.HtmlParameter.Type;
 import org.parosproxy.paros.network.HttpHeader;
 import org.parosproxy.paros.network.HttpMalformedHeaderException;
 import org.parosproxy.paros.network.HttpMessage;
@@ -77,6 +73,7 @@ import org.zaproxy.zap.extension.authentication.AuthenticationAPI;
 import org.zaproxy.zap.extension.authentication.ContextAuthenticationPanel;
 import org.zaproxy.zap.extension.users.ExtensionUserManagement;
 import org.zaproxy.zap.model.Context;
+import org.zaproxy.zap.model.NameValuePair;
 import org.zaproxy.zap.session.SessionManagementMethod;
 import org.zaproxy.zap.session.WebSession;
 import org.zaproxy.zap.users.User;
@@ -88,42 +85,110 @@ import org.zaproxy.zap.view.popup.PopupMenuItemContext;
 import org.zaproxy.zap.view.popup.PopupMenuItemSiteNodeContextMenuFactory;
 
 /**
- * The implementation for an {@link AuthenticationMethodType} where the Users are authenticated by
- * posting a form with user and password.
+ * An {@link AuthenticationMethodType} where the Users are authenticated by POSTing the username and password.
+ * <p>
+ * The actual format of the POST body is defined by extending classes.
+ * 
+ * @since TODO add version
  */
-public class FormBasedAuthenticationMethodType extends AuthenticationMethodType {
-
-	public static final String CONTEXT_CONFIG_AUTH_FORM = AuthenticationMethod.CONTEXT_CONFIG_AUTH + ".form";
-	public static final String CONTEXT_CONFIG_AUTH_FORM_LOGINURL = CONTEXT_CONFIG_AUTH_FORM + ".loginurl";
-	public static final String CONTEXT_CONFIG_AUTH_FORM_LOGINBODY = CONTEXT_CONFIG_AUTH_FORM + ".loginbody";
-
-	private static final int METHOD_IDENTIFIER = 2;
-
-	/** The Authentication method's name. */
-	private static final String METHOD_NAME = Constant.messages.getString("authentication.method.fb.name");
+public abstract class PostBasedAuthenticationMethodType extends AuthenticationMethodType {
+
+	private static final String CONTEXT_CONFIG_AUTH_FORM = AuthenticationMethod.CONTEXT_CONFIG_AUTH + ".form";
+	private static final String CONTEXT_CONFIG_AUTH_FORM_LOGINURL = CONTEXT_CONFIG_AUTH_FORM + ".loginurl";
+	private static final String CONTEXT_CONFIG_AUTH_FORM_LOGINBODY = CONTEXT_CONFIG_AUTH_FORM + ".loginbody";
+
+	private static final String POST_DATA_LABEL = Constant.messages
+			.getString("authentication.method.pb.field.label.postData");
+	private static final String POST_DATA_REQUIRED_LABEL = Constant.messages
+			.getString("authentication.method.pb.field.label.postDataRequired");
+	private static final String USERNAME_PARAM_LABEL = Constant.messages
+			.getString("authentication.method.pb.field.label.usernameParam");
+	private static final String PASSWORD_PARAM_LABEL = Constant.messages
+			.getString("authentication.method.pb.field.label.passwordParam");
+	private static final String LOGIN_URL_LABEL = Constant.messages
+			.getString("authentication.method.pb.field.label.loginUrl");
+	private static final String AUTH_DESCRIPTION = Constant.messages
+			.getString("authentication.method.pb.field.label.description");
+
+	private static final Logger log = Logger.getLogger(PostBasedAuthenticationMethodType.class);
+
+	private final String methodName;
+	private final int methodIdentifier;
+	private final String apiMethodName;
+	private final String labelPopupMenuKey;
+	private final boolean postDataRequired;
 
-	private static final String API_METHOD_NAME = "formBasedAuthentication";
-
-	private static final Logger log = Logger.getLogger(FormBasedAuthenticationMethodType.class);
+	/**
+	 * Constructs a {@code PostBasedAuthenticationMethodType} with the given data.
+	 *
+	 * @param methodName the name of the authentication method, should not be {@code null}.
+	 * @param methodIdentifier the ID of the authentication method.
+	 * @param apiMethodName the API name of the authentication method, should not be {@code null}.
+	 * @param labelPopupMenuKey the name of the menu item that flags the request as login.
+	 * @param postDataRequired {@code true} if the POST data is required by the authentication method, {@code false} otherwise.
+	 */
+	protected PostBasedAuthenticationMethodType(
+			String methodName,
+			int methodIdentifier,
+			String apiMethodName,
+			String labelPopupMenuKey,
+			boolean postDataRequired) {
+		this.methodName = methodName;
+		this.methodIdentifier = methodIdentifier;
+		this.apiMethodName = apiMethodName;
+		this.labelPopupMenuKey = labelPopupMenuKey;
+		this.postDataRequired = postDataRequired;
+	}
 
 	/**
-	 * The implementation for an {@link AuthenticationMethod} where the Users are authenticated by
-	 * posting a form with user and password.
+	 * An {@link AuthenticationMethod} where the Users are authenticated by POSTing the username and password.
+	 * <p>
+	 * The actual format of the POST body is defined by extending classes.
 	 */
-	public static class FormBasedAuthenticationMethod extends AuthenticationMethod {
+	public abstract class PostBasedAuthenticationMethod extends AuthenticationMethod {
 
 		private static final String LOGIN_ICON_RESOURCE = "/resource/icon/fugue/door-open-green-arrow.png";
 		public static final String MSG_USER_PATTERN = "{%username%}";
 		public static final String MSG_PASS_PATTERN = "{%password%}";
 
+		private final String contentType;
+		private final UnaryOperator<String> paramEncoder;
+
 		private HttpSender httpSender;
 		private SiteNode markedLoginSiteNode;
 		private SiteNode loginSiteNode = null;
 		private String loginRequestURL;
 		private String loginRequestBody;
 
+		/**
+		 * Constructs a {@code PostBasedAuthenticationMethod} with the given data.
+		 * 
+		 * @param contentType the value of the Content-Type, to be added to the authentication message-
+		 * @param paramEncoder the encoded of parameters, used to encode the authentication credentials in the POST body.
+		 * @param authenticationMethod the authentication method to copy from, might be {@code null}.
+		 */
+		protected PostBasedAuthenticationMethod(
+				String contentType,
+				UnaryOperator<String> paramEncoder,
+				PostBasedAuthenticationMethod authenticationMethod) {
+			this.contentType = contentType + "; charset=utf-8";
+			this.paramEncoder = paramEncoder;
+			if (authenticationMethod != null) {
+				this.loginRequestURL = authenticationMethod.loginRequestURL;
+				this.loginRequestBody = authenticationMethod.loginRequestBody;
+				this.loginSiteNode = authenticationMethod.loginSiteNode;
+				this.markedLoginSiteNode = authenticationMethod.markedLoginSiteNode;
+			}
+		}
+
 		@Override
 		public boolean isConfigured() {
+			if (postDataRequired) {
+				if (loginRequestBody == null || loginRequestBody.isEmpty()) {
+					return false;
+				}
+			}
+
 			// check if the login url is valid
 			return loginRequestURL != null && !loginRequestURL.isEmpty();
 		}
@@ -133,11 +198,6 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 			return new UsernamePasswordAuthenticationCredentials();
 		}
 
-		@Override
-		public AuthenticationMethodType getType() {
-			return new FormBasedAuthenticationMethodType();
-		}
-
 		protected HttpSender getHttpSender() {
 			if (this.httpSender == null) {
 				this.httpSender = new HttpSender(Model.getSingleton().getOptionsParam().getConnectionParam(),
@@ -156,7 +216,7 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 		 * @throws HttpMalformedHeaderException if the constructed HTTP request is malformed
 		 * @throws DatabaseException if an error occurred while reading the request from database
 		 */
-		private HttpMessage prepareRequestMessage(UsernamePasswordAuthenticationCredentials credentials)
+		protected HttpMessage prepareRequestMessage(UsernamePasswordAuthenticationCredentials credentials)
 				throws URIException, HttpMalformedHeaderException, DatabaseException {
 
 			URI requestURI = createLoginUrl(loginRequestURL, credentials.getUsername(), credentials.getPassword());
@@ -164,7 +224,7 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 			// Replace the username and password in the post data of the request, if needed
 			String requestBody = null;
 			if (loginRequestBody != null && !loginRequestBody.isEmpty()) {
-				requestBody = replaceUserData(loginRequestBody, credentials.getUsername(), credentials.getPassword());
+				requestBody = replaceUserData(loginRequestBody, credentials.getUsername(), credentials.getPassword(), paramEncoder);
 			}
 
 			// Prepare the actual message, either based on the existing one, or create a new one
@@ -184,6 +244,7 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 						new HttpRequestHeader(method, requestURI, HttpHeader.HTTP10,
 								Model.getSingleton().getOptionsParam().getConnectionParam()));
 				if (requestBody != null) {
+					requestMessage.getRequestHeader().setHeader(HttpHeader.CONTENT_TYPE, contentType);
 					requestMessage.getRequestBody().setBody(requestBody);
 				}
 			}
@@ -199,7 +260,7 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 			// type check
 			if (!(credentials instanceof UsernamePasswordAuthenticationCredentials)) {
 				throw new UnsupportedAuthenticationCredentialsException(
-						"Form based authentication method only supports "
+						"Post based authentication method only supports "
 								+ UsernamePasswordAuthenticationCredentials.class.getSimpleName()
 								+ ". Received: " + credentials.getClass());
 			}
@@ -345,17 +406,7 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 
 		@Override
 		public String toString() {
-			return "FormBasedAuthenticationMethod [loginURI=" + loginRequestURL + "]";
-		}
-
-		@Override
-		public FormBasedAuthenticationMethod duplicate() {
-			FormBasedAuthenticationMethod clonedMethod = new FormBasedAuthenticationMethod();
-			clonedMethod.loginRequestURL = this.loginRequestURL;
-			clonedMethod.loginRequestBody = this.loginRequestBody;
-			clonedMethod.loginSiteNode = this.loginSiteNode;
-			clonedMethod.markedLoginSiteNode = this.markedLoginSiteNode;
-			return clonedMethod;
+			return getClass().getSimpleName() + " [loginURI=" + loginRequestURL + "]";
 		}
 
 		@Override
@@ -371,7 +422,7 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 		@Override
 		public ApiResponse getApiResponseRepresentation() {
 			Map<String, String> values = new HashMap<>();
-			values.put("methodName", API_METHOD_NAME);
+			values.put("methodName", apiMethodName);
 			values.put("loginUrl", loginRequestURL);
 			values.put("loginRequestData", this.loginRequestBody);
 			return new AuthMethodApiResponseRepresentation<>(values);
@@ -394,7 +445,7 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 				return false;
 			if (getClass() != obj.getClass())
 				return false;
-			FormBasedAuthenticationMethod other = (FormBasedAuthenticationMethod) obj;
+			PostBasedAuthenticationMethod other = (PostBasedAuthenticationMethod) obj;
 			if (loginRequestBody == null) {
 				if (other.loginRequestBody != null)
 					return false;
@@ -410,12 +461,12 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 	}
 
 	private static URI createLoginUrl(String loginData, String username, String password) throws URIException {
-		return new URI(replaceUserData(loginData, username, password), true);
+		return new URI(replaceUserData(loginData, username, password, PostBasedAuthenticationMethodType::encodeParameter), true);
 	}
 
-	private static String replaceUserData(String loginData, String username, String password) {
-		return loginData.replace(FormBasedAuthenticationMethod.MSG_USER_PATTERN, encodeParameter(username))
-				.replace(FormBasedAuthenticationMethod.MSG_PASS_PATTERN, encodeParameter(password));
+	private static String replaceUserData(String loginData, String username, String password, UnaryOperator<String> encoder) {
+		return loginData.replace(PostBasedAuthenticationMethod.MSG_USER_PATTERN, encoder.apply(username))
+				.replace(PostBasedAuthenticationMethod.MSG_PASS_PATTERN, encoder.apply(password));
 	}
 
 	private static String encodeParameter(String parameter) {
@@ -441,37 +492,29 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 	}
 
 	/**
-	 * The Options Panel used for configuring a {@link FormBasedAuthenticationMethod}.
+	 * The Options Panel used for configuring a {@link PostBasedAuthenticationMethod}.
 	 */
-	private static class FormBasedAuthenticationMethodOptionsPanel extends
+	protected abstract class PostBasedAuthenticationMethodOptionsPanel extends
 			AbstractAuthenticationMethodOptionsPanel {
 
-		private static final long serialVersionUID = -9010956260384814566L;
-
-		private static final String POST_DATA_LABEL = Constant.messages
-				.getString("authentication.method.fb.field.label.postData");
-		private static final String USERNAME_PARAM_LABEL = Constant.messages
-				.getString("authentication.method.fb.field.label.usernameParam");
-		private static final String PASSWORD_PARAM_LABEL = Constant.messages
-				.getString("authentication.method.fb.field.label.passwordParam");
-		private static final String LOGIN_URL_LABEL = Constant.messages
-				.getString("authentication.method.fb.field.label.loginUrl");
-		private static final String AUTH_DESCRIPTION = Constant.messages
-				.getString("authentication.method.fb.field.label.description");
+		private static final long serialVersionUID = 1L;
 
 		private ZapTextField loginUrlField;
 		private ZapTextField postDataField;
-		private JComboBox<HtmlParameter> usernameParameterCombo;
-		private JComboBox<HtmlParameter> passwordParameterCombo;
-		private FormBasedAuthenticationMethod authenticationMethod;
+		private JComboBox<NameValuePair> usernameParameterCombo;
+		private JComboBox<NameValuePair> passwordParameterCombo;
+		private PostBasedAuthenticationMethod authenticationMethod;
 
 		private Context context;
 		private ExtensionUserManagement userExt = null;
 
-		public FormBasedAuthenticationMethodOptionsPanel(Context context) {
+		private final UnaryOperator<String> paramDecoder;
+
+		public PostBasedAuthenticationMethodOptionsPanel(Context context, UnaryOperator<String> paramDecoder) {
 			super();
 			initialize();
 			this.context = context;
+			this.paramDecoder = paramDecoder;
 		}
 
 		@SuppressWarnings("unchecked")
@@ -518,7 +561,7 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 					if (node != null && node.getHistoryReference() != null) {
 						try {
 							if (log.isInfoEnabled()) {
-								log.info("Selected Form Based Auth Login URL via dialog: "
+								log.info("Selected Post Based Auth Login URL via dialog: "
 										+ node.getHistoryReference().getURI().toString());
 							}
 
@@ -537,56 +580,56 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 			urlSelectPanel.add(selectButton, LayoutHelper.getGBC(1, 0, 1, 0.0D));
 			this.add(urlSelectPanel, LayoutHelper.getGBC(0, 1, 2, 1.0d, 0.0d));
 
-			this.add(new JLabel(POST_DATA_LABEL), LayoutHelper.getGBC(0, 2, 2, 1.0d, 0.0d));
+			this.add(new JLabel(postDataRequired ? POST_DATA_REQUIRED_LABEL : POST_DATA_LABEL), LayoutHelper.getGBC(0, 2, 2, 1.0d, 0.0d));
 			this.add(this.postDataField, LayoutHelper.getGBC(0, 3, 2, 1.0d, 0.0d));
 
 			this.add(new JLabel(USERNAME_PARAM_LABEL), LayoutHelper.getGBC(0, 4, 1, 1.0d, 0.0d));
 			this.usernameParameterCombo = new JComboBox<>();
-			this.usernameParameterCombo.setRenderer(new HtmlParameterRenderer());
+			this.usernameParameterCombo.setRenderer(NameValuePairRenderer.INSTANCE);
 			this.add(usernameParameterCombo, LayoutHelper.getGBC(0, 5, 1, 1.0d, 0.0d));
 
 			this.add(new JLabel(PASSWORD_PARAM_LABEL), LayoutHelper.getGBC(1, 4, 1, 1.0d, 0.0d));
 			this.passwordParameterCombo = new JComboBox<>();
-			this.passwordParameterCombo.setRenderer(new HtmlParameterRenderer());
+			this.passwordParameterCombo.setRenderer(NameValuePairRenderer.INSTANCE);
 			this.add(passwordParameterCombo, LayoutHelper.getGBC(1, 5, 1, 1.0d, 0.0d));
 
 			this.add(new JLabel(AUTH_DESCRIPTION), LayoutHelper.getGBC(0, 8, 2, 1.0d, 0.0d));
 
 			// Make sure we update the parameters when something has been changed in the
 			// postDataField
-			this.postDataField.addFocusListener(new FocusListener() {
+			this.postDataField.addFocusListener(new FocusAdapter() {
 				@Override
 				public void focusLost(FocusEvent e) {
 					updateParameters();
 				}
-
-				@Override
-				public void focusGained(FocusEvent e) {
-				}
 			});
 		}
 
+		/**
+		 * Gets the context being configured.
+		 *
+		 * @return the context, never {@code null}.
+		 */
+		protected Context getContext() {
+			return context;
+		}
+
 		@Override
 		public void validateFields() {
 			if (!isValidLoginUrl(loginUrlField.getText())) {
 				loginUrlField.requestFocusInWindow();
 				throw new IllegalStateException(
-						Constant.messages.getString("authentication.method.fb.dialog.error.url.text"));
+						Constant.messages.getString("authentication.method.pb.dialog.error.url.text"));
 			}
-		}
 
-		private String replaceParameterValue(String originalString, HtmlParameter parameter,
-				String replaceString) {
-			String keyValueSeparator = context.getPostParamParser().getDefaultKeyValueSeparator();
-			String nameAndSeparator = parameter.getName() + keyValueSeparator;
-			// Make sure we handle the case when there's only the parameter name in the POST data instead of
-			// parameter name + separator + value (e.g. just 'param1&...' instead of 'param1=...&...')
-			if (originalString.contains(nameAndSeparator))
-				return originalString.replace(nameAndSeparator + parameter.getValue(), nameAndSeparator
-						+ replaceString);
-			else
-				return originalString.replace(parameter.getName(), nameAndSeparator + replaceString);
+			if (postDataRequired && postDataField.getText().isEmpty()) {
+				postDataField.requestFocusInWindow();
+				throw new IllegalStateException(
+						Constant.messages.getString("authentication.method.pb.dialog.error.postData.text"));
+			}
 		}
+
+		protected abstract String replaceParameterValue(String originalString, NameValuePair parameter, String replaceString);
 		
 		private ExtensionUserManagement getUserExt() {
 			if (userExt == null) {
@@ -601,19 +644,18 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 			try {
 				String postData = postDataField.getText();
 				if (!postData.isEmpty()) {
-					HtmlParameter userParam = (HtmlParameter) usernameParameterCombo.getSelectedItem();
-					HtmlParameter passwdParam = (HtmlParameter) passwordParameterCombo.getSelectedItem();
+					NameValuePair userParam = (NameValuePair) usernameParameterCombo.getSelectedItem();
+					NameValuePair passwdParam = (NameValuePair) passwordParameterCombo.getSelectedItem();
 					
 					ExtensionUserManagement userExt = getUserExt();
 					if (userExt != null && userExt.getUIConfiguredUsers(context.getIndex()).size() == 0) {
 						String username = userParam.getValue();
 						String password = passwdParam.getValue();
-						if (!username.isEmpty() && !username.contains(FormBasedAuthenticationMethod.MSG_USER_PATTERN)
-								&& !password.contains(FormBasedAuthenticationMethod.MSG_PASS_PATTERN)) {
+						if (!username.isEmpty() && !username.contains(PostBasedAuthenticationMethod.MSG_USER_PATTERN)
+								&& !password.contains(PostBasedAuthenticationMethod.MSG_PASS_PATTERN)) {
 							// Add the user based on the details provided
-							// Note that right now application/x-www-form-urlencoded forms are supported 
-							String userStr = decodeValue(username);
-							String passwdStr = decodeValue(password);
+							String userStr = paramDecoder.apply(username);
+							String passwdStr = paramDecoder.apply(password);
 							if (!userStr.isEmpty() && !passwdStr.isEmpty()) {
 								User user = new User(context.getIndex(), userStr);
 								UsernamePasswordAuthenticationCredentials upac = 
@@ -625,9 +667,9 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 					}
 					
 					postData = this.replaceParameterValue(postData, userParam,
-							FormBasedAuthenticationMethod.MSG_USER_PATTERN);
+							PostBasedAuthenticationMethod.MSG_USER_PATTERN);
 					postData = this.replaceParameterValue(postData, passwdParam,
-							FormBasedAuthenticationMethod.MSG_PASS_PATTERN);
+							PostBasedAuthenticationMethod.MSG_PASS_PATTERN);
 				}
 				getMethod().setLoginRequest(loginUrlField.getText(), postData);
 			} catch (Exception e) {
@@ -635,20 +677,9 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 			}
 		}
 
-		private static String decodeValue(String value) {
-			try {
-				return URLDecoder.decode(value, StandardCharsets.UTF_8.name());
-			} catch (UnsupportedEncodingException ignore) {
-				// Standard charset.
-			} catch (IllegalArgumentException e) {
-				log.debug("Failed to URL decode: " + value, e);
-			}
-			return "";
-		}
-
 		@Override
 		public void bindMethod(AuthenticationMethod method) {
-			this.authenticationMethod = (FormBasedAuthenticationMethod) method;
+			this.authenticationMethod = (PostBasedAuthenticationMethod) method;
 			this.loginUrlField.setText(authenticationMethod.loginRequestURL);
 			this.postDataField.setText(authenticationMethod.loginRequestBody);
 
@@ -662,28 +693,27 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 		 * @param value the value
 		 * @return the index of param with value, or -1 if no match was found
 		 */
-		private int getIndexOfParamWithValue(HtmlParameter[] params, String value) {
+		private int getIndexOfParamWithValue(NameValuePair[] params, String value) {
 			for (int i = 0; i < params.length; i++)
-				if (params[i].getValue().equals(value))
+				if (value.equals(params[i].getValue()))
 					return i;
 			return -1;
 		}
 
 		private void updateParameters() {
 			try {
-				Map<String, String> params = this.context.getPostParamParser().parse(
-						this.postDataField.getText());
-				HtmlParameter[] paramsArray = mapToParamArray(params);
+				List<NameValuePair> params = extractParameters(this.postDataField.getText());
+				NameValuePair[] paramsArray = params.toArray(new NameValuePair[params.size()]);
 				this.usernameParameterCombo.setModel(new DefaultComboBoxModel<>(paramsArray));
 				this.passwordParameterCombo.setModel(new DefaultComboBoxModel<>(paramsArray));
 
 				int index = getIndexOfParamWithValue(paramsArray,
-						FormBasedAuthenticationMethod.MSG_USER_PATTERN);
+						PostBasedAuthenticationMethod.MSG_USER_PATTERN);
 				if (index >= 0) {
 					this.usernameParameterCombo.setSelectedIndex(index);
 				}
 
-				index = getIndexOfParamWithValue(paramsArray, FormBasedAuthenticationMethod.MSG_PASS_PATTERN);
+				index = getIndexOfParamWithValue(paramsArray, PostBasedAuthenticationMethod.MSG_PASS_PATTERN);
 				if (index >= 0) {
 					this.passwordParameterCombo.setSelectedIndex(index);
 				}
@@ -692,28 +722,29 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 			}
 		}
 
-		private HtmlParameter[] mapToParamArray(Map<String, String> map) {
-			HtmlParameter[] array = new HtmlParameter[map.size()];
-			int i = 0;
-			for (Entry<String, String> param : map.entrySet()) {
-				array[i++] = new HtmlParameter(Type.form, param.getKey(), param.getValue());
-			}
-			return array;
-		}
+		protected abstract List<NameValuePair> extractParameters(String postData);
 
 		@Override
-		public FormBasedAuthenticationMethod getMethod() {
+		public PostBasedAuthenticationMethod getMethod() {
 			return this.authenticationMethod;
 		}
 	}
 
 	/**
-	 * A renderer for properly displaying the name of an HtmlParameter in a ComboBox.
+	 * A renderer for properly displaying the name of a {@link NameValuePair} in a ComboBox.
+	 * 
+	 * @see #INSTANCE
 	 */
-	private static class HtmlParameterRenderer extends BasicComboBoxRenderer {
+	private static class NameValuePairRenderer extends BasicComboBoxRenderer {
+
+		public static final NameValuePairRenderer INSTANCE = new NameValuePairRenderer();
+
 		private static final long serialVersionUID = 3654541772447187317L;
 		private static final Border BORDER = new EmptyBorder(2, 3, 3, 3);
 
+		private NameValuePairRenderer() {
+		}
+
 		@Override
 		@SuppressWarnings("rawtypes")
 		public Component getListCellRendererComponent(JList list, Object value, int index,
@@ -721,7 +752,7 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 			super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
 			if (value != null) {
 				setBorder(BORDER);
-				HtmlParameter item = (HtmlParameter) value;
+				NameValuePair item = (NameValuePair) value;
 				setText(item.getName());
 			}
 			return this;
@@ -729,18 +760,11 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 	}
 
 	@Override
-	public FormBasedAuthenticationMethod createAuthenticationMethod(int contextId) {
-		return new FormBasedAuthenticationMethod();
-	}
+	public abstract PostBasedAuthenticationMethod createAuthenticationMethod(int contextId);
 
 	@Override
 	public String getName() {
-		return METHOD_NAME;
-	}
-
-	@Override
-	public AbstractAuthenticationMethodOptionsPanel buildOptionsPanel(Context uiSharedContext) {
-		return new FormBasedAuthenticationMethodOptionsPanel(uiSharedContext);
+		return methodName;
 	}
 
 	@Override
@@ -760,11 +784,6 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 		return true;
 	}
 
-	@Override
-	public boolean isTypeForMethod(AuthenticationMethod method) {
-		return (method instanceof FormBasedAuthenticationMethod);
-	}
-
 	@Override
 	public void hook(ExtensionHook extensionHook) {
 		extensionHook.getHookMenu().addPopupMenuItem(getPopupFlagLoginRequestMenuFactory());
@@ -776,14 +795,14 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 	 * @return the popup flag login request menu factory
 	 */
 	private PopupMenuItemSiteNodeContextMenuFactory getPopupFlagLoginRequestMenuFactory() {
-	    PopupMenuItemSiteNodeContextMenuFactory popupFlagLoginRequestMenuFactory = new PopupMenuItemSiteNodeContextMenuFactory(
+		PopupMenuItemSiteNodeContextMenuFactory popupFlagLoginRequestMenuFactory = new PopupMenuItemSiteNodeContextMenuFactory(
 				Constant.messages.getString("context.flag.popup")) {
 			private static final long serialVersionUID = 8927418764L;
 
 			@Override
 			public PopupMenuItemContext getContextMenu(Context context, String parentMenu) {
 				return new PopupMenuItemContext(context, parentMenu, 
-						Constant.messages.getString("authentication.method.fb.popup.login.request", context.getName())) {
+						Constant.messages.getString(labelPopupMenuKey, context.getName())) {
 
 					private static final long serialVersionUID = 1967885623005183801L;
 					private ExtensionUserManagement usersExtension;
@@ -821,10 +840,10 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 						uiSharedContext = sessionDialog.getUISharedContext(this.getContext().getIndex());
 
 						// Do the work/changes on the UI shared context
-						if (this.getContext().getAuthenticationMethod() instanceof FormBasedAuthenticationMethod) {
-							log.info("Selected new login request via PopupMenu. Changing existing Form-Based Authentication instance for Context "
+						if (isTypeForMethod(this.getContext().getAuthenticationMethod())) {
+							log.info("Selected new login request via PopupMenu. Changing existing " + methodName + " instance for Context "
 									+ getContext().getIndex());
-							FormBasedAuthenticationMethod method = (FormBasedAuthenticationMethod) uiSharedContext
+							PostBasedAuthenticationMethod method = (PostBasedAuthenticationMethod) uiSharedContext
 									.getAuthenticationMethod();
 
 							try {
@@ -841,9 +860,9 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 											ContextAuthenticationPanel
 													.buildName(this.getContext().getIndex()), false);
 						} else {
-							log.info("Selected new login request via PopupMenu. Creating new Form-Based Authentication instance for Context "
+							log.info("Selected new login request via PopupMenu. Creating new " + methodName + " instance for Context "
 									+ getContext().getIndex());
-							FormBasedAuthenticationMethod method = new FormBasedAuthenticationMethod();
+							PostBasedAuthenticationMethod method = createAuthenticationMethod(getContext().getIndex());
 
 							try {
 								method.setLoginRequest(sn);
@@ -891,7 +910,7 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 
 	@Override
 	public AuthenticationMethod loadMethodFromSession(Session session, int contextId) throws DatabaseException {
-		FormBasedAuthenticationMethod method = new FormBasedAuthenticationMethod();
+		PostBasedAuthenticationMethod method = createAuthenticationMethod(contextId);
 		List<String> urls = session.getContextDataStrings(contextId, RecordContext.TYPE_AUTH_METHOD_FIELD_1);
 		String url = "";
 		if (urls != null && urls.size() > 0) {
@@ -908,7 +927,7 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 		try {
 			method.setLoginRequest(url, postData);
 		} catch (Exception e) {
-			log.error("Unable to load FormBasedAuthenticationMethod. ", e);
+			log.error("Unable to load Post based authentication method data:", e);
 		}
 		return method;
 	}
@@ -916,19 +935,19 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 	@Override
 	public void persistMethodToSession(Session session, int contextId, AuthenticationMethod authMethod)
 			throws DatabaseException {
-		if (!(authMethod instanceof FormBasedAuthenticationMethod)) {
+		if (!(authMethod instanceof PostBasedAuthenticationMethod)) {
 			throw new UnsupportedAuthenticationMethodException(
-					"Form based authentication type only supports: " + FormBasedAuthenticationMethod.class);
+					"Post based authentication type only supports: " + PostBasedAuthenticationMethod.class);
 		}
 
-		FormBasedAuthenticationMethod method = (FormBasedAuthenticationMethod) authMethod;
+		PostBasedAuthenticationMethod method = (PostBasedAuthenticationMethod) authMethod;
 		session.setContextData(contextId, RecordContext.TYPE_AUTH_METHOD_FIELD_1, method.loginRequestURL);
 		session.setContextData(contextId, RecordContext.TYPE_AUTH_METHOD_FIELD_2, method.loginRequestBody);
 	}
 
 	@Override
 	public int getUniqueIdentifier() {
-		return METHOD_IDENTIFIER;
+		return methodIdentifier;
 	}
 
 	@Override
@@ -942,8 +961,16 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 
 	@Override
 	public ApiDynamicActionImplementor getSetMethodForContextApiAction() {
-		return new ApiDynamicActionImplementor(API_METHOD_NAME, new String[] { PARAM_LOGIN_URL },
-				new String[] { PARAM_LOGIN_REQUEST_DATA }) {
+		String[] mandatoryParamNames;
+		String[] optionalParamNames;
+		if (postDataRequired) {
+			mandatoryParamNames = new String[] { PARAM_LOGIN_URL, PARAM_LOGIN_REQUEST_DATA };
+			optionalParamNames = null;
+		} else {
+			mandatoryParamNames = new String[] { PARAM_LOGIN_URL };
+			optionalParamNames = new String[] { PARAM_LOGIN_REQUEST_DATA };
+		}
+		return new ApiDynamicActionImplementor(apiMethodName, mandatoryParamNames, optionalParamNames) {
 
 			@Override
 			public void handleAction(JSONObject params) throws ApiException {
@@ -953,12 +980,14 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 					throw new ApiException(ApiException.Type.ILLEGAL_PARAMETER, PARAM_LOGIN_URL);
 				}
 				String postData = "";
-				if (params.containsKey(PARAM_LOGIN_REQUEST_DATA)) {
+				if (postDataRequired) {
+					postData = ApiUtils.getNonEmptyStringParam(params, PARAM_LOGIN_REQUEST_DATA);
+				} else if (params.containsKey(PARAM_LOGIN_REQUEST_DATA)) {
 					postData = params.getString(PARAM_LOGIN_REQUEST_DATA);
 				}
 
 				// Set the method
-				FormBasedAuthenticationMethod method = createAuthenticationMethod(context.getIndex());
+				PostBasedAuthenticationMethod method = createAuthenticationMethod(context.getIndex());
 				try {
 					method.setLoginRequest(loginUrl, postData);
 				} catch (Exception e) {
@@ -979,11 +1008,11 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 
 	@Override
 	public void exportData(Configuration config, AuthenticationMethod authMethod) {
-		if (!(authMethod instanceof FormBasedAuthenticationMethod)) {
+		if (!(authMethod instanceof PostBasedAuthenticationMethod)) {
 			throw new UnsupportedAuthenticationMethodException(
-					"Form based authentication type only supports: " + FormBasedAuthenticationMethod.class.getName());
+					"Post based authentication type only supports: " + PostBasedAuthenticationMethod.class.getName());
 		}
-		FormBasedAuthenticationMethod method = (FormBasedAuthenticationMethod) authMethod;
+		PostBasedAuthenticationMethod method = (PostBasedAuthenticationMethod) authMethod;
 
 		config.setProperty(CONTEXT_CONFIG_AUTH_FORM_LOGINURL, method.loginRequestURL);
 		config.setProperty(CONTEXT_CONFIG_AUTH_FORM_LOGINBODY, method.loginRequestBody);
@@ -991,11 +1020,11 @@ public class FormBasedAuthenticationMethodType extends AuthenticationMethodType
 
 	@Override
 	public void importData(Configuration config, AuthenticationMethod authMethod) throws ConfigurationException {
-		if (!(authMethod instanceof FormBasedAuthenticationMethod)) {
+		if (!(authMethod instanceof PostBasedAuthenticationMethod)) {
 			throw new UnsupportedAuthenticationMethodException(
-					"Form based authentication type only supports: " + FormBasedAuthenticationMethod.class.getName());
+					"Post based authentication type only supports: " + PostBasedAuthenticationMethod.class.getName());
 		}
-		FormBasedAuthenticationMethod method = (FormBasedAuthenticationMethod) authMethod;
+		PostBasedAuthenticationMethod method = (PostBasedAuthenticationMethod) authMethod;
 		
 		try {
 			method.setLoginRequest(config.getString(CONTEXT_CONFIG_AUTH_FORM_LOGINURL), 

Add an authentication method, JsonBasedAuthenticationMethodType, that
allows to send a JSON object in the request body. It can be configured
through the API with the name "jsonBasedAuthentication".
Extract a class, PostBasedAuthenticationMethodType, to be used by
FormBasedAuthenticationMethodType and the new auth method, the behaviour
is exactly the same except for the data they handle (application/json or
application/x-www-form-urlencoded) and the POST data required.
Add libraries used to quote the authentication credentials when injected
into the request body.
Add JSON Content-Type to HttpHeader.

Fix zaproxy#2439 - Configure Authentication with Json object
@psiinon psiinon merged commit 0985585 into zaproxy:develop Apr 25, 2018
@thc202 thc202 deleted the json-auth-method branch April 25, 2018 08:31
@kirksl
Copy link

kirksl commented Apr 27, 2018

@kingthorin @psiinon is there an eta when this will make it into stable and will that be 2.8.0?

@psiinon
Copy link
Member

psiinon commented Apr 27, 2018

@kirksl We havnt planned when 2.8.0 will be yet I'm afraid. We're aiming for ~ 2 releases a year, so maybe within the next few months.
You can use the weekly releases...

@kirksl
Copy link

kirksl commented Apr 27, 2018

@psiinon no worries. we're happy to use the weekly releases. are they stable enough if we're just doing passive, active scans on our nightly CI builds?

@thc202
Copy link
Member Author

thc202 commented Apr 27, 2018

They might break occasionally, but we react quickly to fix it.

@psiinon
Copy link
Member

psiinon commented Apr 28, 2018

We've only had to fix them a couple of times in the last couple of years.
At Mozilla we use the live docker images for our daily baseline scans, so I get to find out pretty quickly if something does break ;)

@kirksl
Copy link

kirksl commented May 7, 2018

@psiinon @thc202 Thanks for the details. Do you know when the next weekly docker image will be available?

@thc202
Copy link
Member Author

thc202 commented May 7, 2018

Most likely tomorrow (live image already includes this though).

@psiinon
Copy link
Member

psiinon commented May 8, 2018

The weekly docker image which includes this change is available now :)

@kirksl
Copy link

kirksl commented May 8, 2018

Cheers mate. Greatly appreciate it.

@lock
Copy link

lock bot commented Nov 1, 2019

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@lock lock bot locked and limited conversation to collaborators Nov 1, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Development

Successfully merging this pull request may close these issues.

Configure Authentication with Json object
4 participants