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

ZCS-15588: Updated ChangePasswordRequest to support zimbraPasswordMustChange flow #1645

Open
wants to merge 4 commits into
base: bugfix/ZBUG-4048/ChgPwdMfa
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,7 @@ public class AccountConstants {
public static final String A_NUM_OTHER_TRUSTED_DEVICES = "nOtherDevices";
public static final String E_DEVICE_ID = "deviceId";
public static final String A_GENERATE_DEVICE_ID = "generateDeviceId";
public static final String E_RESET_PWD = "resetPassword";
public static final String E_TWO_FACTOR_AUTH_REQUIRED = "twoFactorAuthRequired";
public static final String E_TRUSTED_DEVICES_ENABLED = "trustedDevicesEnabled";
public static final String E_TWO_FACTOR_METHOD_APP = "app";
Expand Down
4 changes: 4 additions & 0 deletions common/src/java/com/zimbra/common/soap/AdminConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ public final class AdminConstants {

public static final String E_AUTH_REQUEST = "AuthRequest";
public static final String E_AUTH_RESPONSE = "AuthResponse";
public static final String E_CHANGE_PASSWORD_REQUEST = "ChangePasswordRequest";
public static final String E_CHANGE_PASSWORD_RESPONSE = "ChangePasswordResponse";
public static final String E_CREATE_ACCOUNT_REQUEST = "CreateAccountRequest";
public static final String E_CREATE_ACCOUNT_RESPONSE = "CreateAccountResponse";
public static final String E_CREATE_GAL_SYNC_ACCOUNT_REQUEST = "CreateGalSyncAccountRequest";
Expand Down Expand Up @@ -599,6 +601,8 @@ public final class AdminConstants {
public static final QName SEARCH_ACCOUNTS_RESPONSE = QName.get(E_SEARCH_ACCOUNTS_RESPONSE, NAMESPACE);
public static final QName RENAME_ACCOUNT_REQUEST = QName.get(E_RENAME_ACCOUNT_REQUEST, NAMESPACE);
public static final QName RENAME_ACCOUNT_RESPONSE = QName.get(E_RENAME_ACCOUNT_RESPONSE, NAMESPACE);
public static final QName CHANGE_PASSWORD_REQUEST = QName.get(E_CHANGE_PASSWORD_REQUEST, NAMESPACE);
public static final QName CHANGE_PASSWORD_RESPONSE = QName.get(E_CHANGE_PASSWORD_RESPONSE, NAMESPACE);
public static final QName CHANGE_PRIMARY_EMAIL_REQUEST = QName.get(E_CHANGE_PRIMARY_EMAIL_REQUEST, NAMESPACE);
public static final QName CHANGE_PRIMARY_EMAIL_RESPONSE = QName.get(E_CHANGE_PRIMARY_EMAIL_RESPONSE, NAMESPACE);

Expand Down
2 changes: 2 additions & 0 deletions soap/src/java/com/zimbra/soap/JaxbUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ public final class JaxbUtil {
com.zimbra.soap.admin.message.CancelPendingAccountOnlyRemoteWipeResponse.class,
com.zimbra.soap.admin.message.CancelPendingRemoteWipeRequest.class,
com.zimbra.soap.admin.message.CancelPendingRemoteWipeResponse.class,
com.zimbra.soap.admin.message.ChangePasswordRequest.class,
com.zimbra.soap.admin.message.ChangePasswordResponse.class,
com.zimbra.soap.admin.message.CheckAuthConfigRequest.class,
com.zimbra.soap.admin.message.CheckAuthConfigResponse.class,
com.zimbra.soap.admin.message.CheckBlobConsistencyRequest.class,
Expand Down
12 changes: 12 additions & 0 deletions soap/src/java/com/zimbra/soap/account/message/AuthResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ public class AuthResponse {
@XmlElement(name=AccountConstants.E_PREF_PASSWORD_RECOVERY_ADDRESS, required=false)
private String prefPasswordRecoveryAddress;

@XmlElement(name=AccountConstants.E_RESET_PWD, required=false)
private String resetPassword;

public AuthResponse() {
}

Expand Down Expand Up @@ -280,6 +283,15 @@ public String getTrustedToken() {
public ZmBoolean getTrustedDevicesEnabled() { return trustedDevicesEnabled; }
public AuthResponse setTrustedDevicesEnabled(boolean bool) { this.trustedDevicesEnabled = ZmBoolean.fromBool(bool); return this; }

@GraphQLQuery(name="resetPassword", description="if true then auth token will be used to change password")
public String getResetPassword() {
return resetPassword;
}

public void setResetPassword(String resetPassword) {
this.resetPassword = resetPassword;
}
zimsuchitgupta marked this conversation as resolved.
Show resolved Hide resolved

public AuthResponse addTwoFactorAuthMethodAllowed(String method) {
this.twoFactorAuthMethodAllowed.add(method);
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import javax.xml.bind.annotation.XmlType;

import com.zimbra.common.soap.AccountConstants;
import com.zimbra.soap.account.type.AuthToken;
import com.zimbra.soap.type.AccountSelector;

/**
Expand Down Expand Up @@ -66,6 +67,9 @@ public class ChangePasswordRequest {
@XmlElement(name=AccountConstants.E_DRYRUN, required=false)
private boolean dryRun;

@XmlElement(name=AccountConstants.E_AUTH_TOKEN /* authToken */, required=false)
private AuthToken authToken;

public ChangePasswordRequest() {
}

Expand Down Expand Up @@ -117,5 +121,8 @@ public void setDryRun(boolean dryRun) {
this.dryRun = dryRun;
}

public AuthToken getAuthToken() { return authToken; }
public ChangePasswordRequest setAuthToken(AuthToken authToken) { this.authToken = authToken; return this; }


}
113 changes: 113 additions & 0 deletions soap/src/java/com/zimbra/soap/admin/message/ChangePasswordRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* ***** BEGIN LICENSE BLOCK *****
* Zimbra Collaboration Suite Server
* Copyright (C) 2024 Synacor, Inc.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software Foundation,
* version 2 of the License.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <https://www.gnu.org/licenses/>.
* ***** END LICENSE BLOCK *****
*/
package com.zimbra.soap.admin.message;

import com.zimbra.common.soap.AccountConstants;
import com.zimbra.common.soap.AdminConstants;
import com.zimbra.soap.account.type.AuthToken;
import com.zimbra.soap.type.AccountSelector;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

@XmlRootElement(name= AdminConstants.E_CHANGE_PASSWORD_REQUEST)
@XmlType(propOrder = {})
public class ChangePasswordRequest {
/**
* @zm-api-field-description Details of the account
*/
@XmlElement(name=AccountConstants.E_ACCOUNT, required=true)
private AccountSelector account;
/**
* @zm-api-field-description Old password
*/
@XmlElement(name=AccountConstants.E_OLD_PASSWORD, required=true)
private String oldPassword;
/**
* @zm-api-field-description New Password to assign
*/
@XmlElement(name=AccountConstants.E_PASSWORD, required=true)
private String password;
/**
* @zm-api-field-tag virtual-host
* @zm-api-field-description if specified virtual-host is used to determine the domain of the account name,
* if it does not include a domain component. For example, if the domain foo.com has a zimbraVirtualHostname of
* "mail.foo.com", and an auth request comes in for "joe" with a virtualHost of "mail.foo.com", then the request
* will be equivalent to logging in with "[email protected]".
*/
@XmlElement(name=AccountConstants.E_VIRTUAL_HOST, required=false)
private String virtualHost;

@XmlElement(name=AccountConstants.E_DRYRUN, required=false)
private boolean dryRun;

@XmlElement(name=AccountConstants.E_AUTH_TOKEN /* authToken */, required=false)
private AuthToken authToken;

public ChangePasswordRequest() {
}

public ChangePasswordRequest(AccountSelector account, String oldPassword, String newPassword) {
setAccount(account);
setOldPassword(oldPassword);
setPassword(newPassword);
}

public ChangePasswordRequest(AccountSelector account, String oldPassword, String newPassword, boolean dryRun) {
setAccount(account);
setOldPassword(oldPassword);
setPassword(newPassword);
setDryRun(dryRun);
}

public AccountSelector getAccount() { return account; }
public String oldPassword() { return oldPassword; }
public String getPassword() { return password; }
public String getVirtualHost() { return virtualHost; }

public ChangePasswordRequest setAccount(AccountSelector account) {
this.account = account;
return this;
}

public ChangePasswordRequest setOldPassword(String oldPassword) {
this.oldPassword = oldPassword;
return this;
}

public ChangePasswordRequest setPassword(String password) {
this.password = password;
return this;
}

public ChangePasswordRequest setVirtualHost(String host) {
virtualHost = host;
return this;
}

public boolean isDryRun() {
return dryRun;
}

public void setDryRun(boolean dryRun) {
this.dryRun = dryRun;
}

public AuthToken getAuthToken() { return authToken; }
public ChangePasswordRequest setAuthToken(AuthToken authToken) { this.authToken = authToken; return this; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* ***** BEGIN LICENSE BLOCK *****
* Zimbra Collaboration Suite Server
* Copyright (C) 2024 Synacor, Inc.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software Foundation,
* version 2 of the License.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <https://www.gnu.org/licenses/>.
* ***** END LICENSE BLOCK *****
*/
package com.zimbra.soap.admin.message;

import com.zimbra.common.gql.GqlConstants;
import com.zimbra.common.soap.AccountConstants;
import com.zimbra.common.soap.AdminConstants;
import com.zimbra.soap.json.jackson.annotate.ZimbraJsonAttribute;
import io.leangen.graphql.annotations.GraphQLQuery;
import io.leangen.graphql.annotations.types.GraphQLType;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

@XmlRootElement(name= AdminConstants.E_CHANGE_PASSWORD_RESPONSE)
@GraphQLType(name= GqlConstants.CLASS_CHANGE_PASSWORD_RESPONSE, description="The response to change password request.")
@XmlType(propOrder = {})
public class ChangePasswordResponse {

/**
* @zm-api-field-tag new-auth-token
* @zm-api-field-description New authToken, as old authToken is invalidated on password change.
*/
@XmlElement(name=AccountConstants.E_AUTH_TOKEN /* authToken */, required=true)
private String authToken;
/**
* @zm-api-field-description Life time associated with <b>{new-auth-token}</b>
*/
@ZimbraJsonAttribute
@XmlElement(name=AccountConstants.E_LIFETIME /* lifetime */, required=true)
private long lifetime;

public ChangePasswordResponse() {
}

@GraphQLQuery(name=GqlConstants.AUTH_TOKEN, description="Auth token based on the new password")
public String getAuthToken() { return authToken; }
@GraphQLQuery(name=GqlConstants.LIFETIME, description="Life time of the auth token")
public long getLifetime() { return lifetime; }

public ChangePasswordResponse setAuthToken(String authToken) {
this.authToken = authToken;
return this;
}

public ChangePasswordResponse setLifetime(long lifetime) {
this.lifetime = lifetime;
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
import java.util.HashMap;
import java.util.Map;

import com.zimbra.cs.account.AccountServiceException.AuthFailedServiceException;
import com.zimbra.cs.account.AuthToken;
import com.zimbra.cs.service.AuthProvider;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
Expand All @@ -32,13 +35,16 @@
public class ChangePasswordTest {
private static final String USERNAME_1 = "[email protected]";
private static final String USERNAME_2 = "[email protected]";
private static final String USERNAME_3 = "[email protected]";
private static final String PASSWORD_1 = "H3@pBigPassw0rd";
private static final String PASSWORD_2 = "An0therP@$$w0rd";
private static final String PASSWORD_3 = "An0therP@1$$w0rd";

private static final MockProvisioning prov = new MockProvisioning();

private Account account1;
private Account account2;
private Account account3;

@BeforeClass
public static void init() throws Exception {
Expand All @@ -57,6 +63,11 @@ public void setUp() throws Exception {
final Map<String,Object> attrs2 = new HashMap<>(1);
attrs2.put(Provisioning.A_zimbraId, LdapUtil.generateUUID());
account2 = prov.createAccount(USERNAME_2, PASSWORD_2, attrs2);

final Map<String,Object> attrs3 = new HashMap<>(1);
attrs3.put(Provisioning.A_zimbraId, LdapUtil.generateUUID());
attrs3.put(Provisioning.A_zimbraPasswordMustChange, "TRUE");
account3 = prov.createAccount(USERNAME_3, PASSWORD_3, attrs3);
}

@Test
Expand Down Expand Up @@ -97,18 +108,38 @@ public void testBasicHandlerWrongAuth() throws Exception {
}

@Test
public void testNeedsAuth() throws Exception {
public void testNeedsAuth() {
final ChangePassword handler = new ChangePassword();
final Map<String,Object> context = Collections.emptyMap();

Assert.assertTrue("handler.needsAuth()", handler.needsAuth(context));
Assert.assertFalse("handler.needsAuth()", handler.needsAuth(context));
}

@Test
public void testBasicHandlerWithResetPasswordAuthTokenUsageIfMustChangePasswordIsEnabled() throws Exception {
zimsuchitgupta marked this conversation as resolved.
Show resolved Hide resolved
AuthToken authToken = AuthProvider.getAuthToken(account3, AuthToken.Usage.RESET_PASSWORD , AuthToken.TokenType.AUTH);
final ChangePasswordRequest request = new ChangePasswordRequest()
.setAccount(AccountSelector.fromName(USERNAME_3)) // Not the authed user from context below
.setOldPassword(PASSWORD_3)
.setPassword(PASSWORD_3)
.setAuthToken(new com.zimbra.soap.account.type.AuthToken(authToken.getEncoded(), false));

final ChangePassword handler = new ChangePassword();
final Map<String,Object> context = ServiceTestUtil.getRequestContext(account3);

final Element response = handler.handle(JaxbUtil.jaxbToElement(request), context);
Assert.assertNotNull("response", response);

String at = response.getAttribute(AccountConstants.E_AUTH_TOKEN);
Assert.assertNotNull("authtoken", at);
}

@After
public void tearDown() throws Exception {
MailboxTestUtil.clearData();
prov.deleteAccount(account1.getId());
prov.deleteAccount(account1.getId());
prov.deleteAccount(account2.getId());
prov.deleteAccount(account3.getId());
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -50,34 +50,7 @@ protected Element proxyIfNecessary(Element request, Map<String, Object> context)
throw e;
}
}

/*
* bug 27389
*/
protected boolean checkPasswordSecurity(Map<String, Object> context) throws ServiceException {
dasiyogesh marked this conversation as resolved.
Show resolved Hide resolved
HttpServletRequest req = (HttpServletRequest)context.get(SoapServlet.SERVLET_REQUEST);
boolean isHttps = req.getScheme().equals("https");
if (isHttps)
return true;

// clear text
Server server = Provisioning.getInstance().getLocalServer();
String modeString = server.getAttr(Provisioning.A_zimbraMailMode, null);
if (modeString == null) {
// not likely, but just log and let it through
ZimbraLog.soap.warn("missing " + Provisioning.A_zimbraMailMode +
" for checking password security, allowing the request");
return true;
}

MailMode mailMode = Provisioning.MailMode.fromString(modeString);
if (mailMode == MailMode.mixed &&
!server.getBooleanAttr(Provisioning.A_zimbraMailClearTextPasswordEnabled, true))
return false;
else
return true;
}


protected Set<String> getReqAttrs(Element request, AttributeClass klass) throws ServiceException {
String attrsStr = request.getAttribute(AccountConstants.A_ATTRS, null);
if (attrsStr == null) {
Expand Down
Loading