Skip to content

Commit

Permalink
ZCS-15588: Updated ChangePasswordRequest to support zimbraPasswordMus…
Browse files Browse the repository at this point in the history
…tChange flow
  • Loading branch information
zimsuchitgupta committed Aug 9, 2024
1 parent 3e71388 commit 3f52278
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 10 deletions.
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
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;
}

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; }


}
33 changes: 32 additions & 1 deletion store/src/java/com/zimbra/cs/service/account/Auth.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
package com.zimbra.cs.service.account;

import com.zimbra.common.localconfig.LC;
import com.zimbra.cs.service.AuthProviderException;
import io.jsonwebtoken.Claims;

import java.util.Arrays;
Expand Down Expand Up @@ -333,7 +334,14 @@ public Element handle(Element request, Map<String, Object> context) throws Servi
}
} else {
if (password != null || recoveryCode != null) {
prov.authAccount(acct, code, AuthContext.Protocol.soap, authCtxt);
try {
prov.authAccount(acct, code, AuthContext.Protocol.soap, authCtxt);
} catch (AccountServiceException ase) {
if (AccountServiceException.CHANGE_PASSWORD.equals(ase.getCode())) {
ZimbraLog.account.info("zimbraPasswordMustChange is enabled so creating a auth-token used to change password.");
return needResetPassword(context, request, acct, twoFactorManager, zsc, tokenType);
}
}
} else {
// it's ok to not have a password if the client is using a 2FA auth token for the 2nd step of 2FA
if (!twoFactorAuthWithToken) {
Expand Down Expand Up @@ -475,6 +483,29 @@ private Element needTwoFactorAuth(Map<String, Object> context, Element requestEl
}
}

/**
* This method is used to create a temporary auth token with usage RESET_PASSWORD.
* This auth token further be used for changing the password.
* This will be executed iff zimbraPasswordMustChange is set to true
* @param context
* @param requestElement
* @param account
* @param auth
* @param zsc
* @param tokenType
* @return response
* @throws ServiceException
*/
private Element needResetPassword(Map<String, Object> context, Element requestElement, Account account, TwoFactorAuth auth,
ZimbraSoapContext zsc, TokenType tokenType) throws ServiceException {
Element response = zsc.createElement(AccountConstants.AUTH_RESPONSE);
AuthToken authToken = AuthProvider.getAuthToken(account, Usage.RESET_PASSWORD , tokenType);
response.addAttribute(AccountConstants.E_LIFETIME, authToken.getExpires() - System.currentTimeMillis(), Element.Disposition.CONTENT);
response.addUniqueElement(AccountConstants.E_RESET_PWD).setText("true");
authToken.encodeAuthResp(response, false);
return response;
}

private String getTwoFactorAuthRequiredSetupErrorMessage(Account account) {
String[] twoFactorAuthMethodAllowed = account.getTwoFactorAuthMethodAllowed();
if (twoFactorAuthMethodAllowed == null || twoFactorAuthMethodAllowed.length == 0) {
Expand Down
45 changes: 36 additions & 9 deletions store/src/java/com/zimbra/cs/service/account/ChangePassword.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@
import com.zimbra.common.soap.AccountConstants;
import com.zimbra.common.soap.Element;
import com.zimbra.common.util.StringUtil;
import com.zimbra.common.util.ZimbraLog;
import com.zimbra.cs.account.Account;
import com.zimbra.cs.account.AccountServiceException.AuthFailedServiceException;
import com.zimbra.cs.account.AuthToken;
import com.zimbra.cs.account.AuthToken.Usage;
import com.zimbra.cs.account.AuthTokenException;
import com.zimbra.cs.account.Domain;
import com.zimbra.cs.account.Provisioning;
import com.zimbra.cs.service.AuthProvider;
Expand All @@ -50,14 +51,18 @@ public Element handle(Element request, Map<String, Object> context) throws Servi
}

ZimbraSoapContext zsc = getZimbraSoapContext(context);
Provisioning prov = Provisioning.getInstance();
Element authTokenEl = request.getOptionalElement(AccountConstants.E_AUTH_TOKEN);
if (authTokenEl == null && zsc.getAuthToken() == null) {
throw ServiceException.INVALID_REQUEST("invalid request parameter", null);
}

String namePassedIn = request.getAttribute(AccountConstants.E_ACCOUNT);
String name = namePassedIn;

Element virtualHostEl = request.getOptionalElement(AccountConstants.E_VIRTUAL_HOST);
String virtualHost = virtualHostEl == null ? null : virtualHostEl.getText().toLowerCase();

Provisioning prov = Provisioning.getInstance();
if (virtualHost != null && name.indexOf('@') == -1) {
Domain d = prov.get(Key.DomainBy.virtualHostname, virtualHost);
if (d != null)
Expand All @@ -72,22 +77,44 @@ public Element handle(Element request, Map<String, Object> context) throws Servi
}
}

Account acct = prov.get(AccountBy.name, name, zsc.getAuthToken());
AuthToken at = zsc.getAuthToken();
Account acct = prov.get(AccountBy.name, name, at);
if (acct == null) {
throw AuthFailedServiceException.AUTH_FAILED(name, namePassedIn, "account not found");
}

if (!canAccessAccount(zsc, acct)) {
Usage usage = Usage.AUTH;
if (authTokenEl != null) {
try {
at = AuthProvider.getAuthToken(authTokenEl, acct);
} catch (AuthTokenException e) {
throw ServiceException.AUTH_REQUIRED();
}
if (at == null) {
throw ServiceException.AUTH_REQUIRED("invalid auth token");
}
usage = Usage.RESET_PASSWORD;
} else if (!canAccessAccount(zsc, acct)) {
throw ServiceException.PERM_DENIED("cannot access account");
}

acct = AuthProvider.validateAuthToken(prov, at, false, usage);
if (acct == null) {
throw AuthFailedServiceException.AUTH_FAILED(name, namePassedIn, "account not found");
}
String oldPassword = request.getAttribute(AccountConstants.E_OLD_PASSWORD);
String newPassword = request.getAttribute(AccountConstants.E_PASSWORD);

if (acct.isIsExternalVirtualAccount() && StringUtil.isNullOrEmpty(oldPassword)
boolean mustChange = acct.getBooleanAttr(Provisioning.A_zimbraPasswordMustChange, false);
if (mustChange && Usage.RESET_PASSWORD == at.getUsage()) {
prov.changePassword(acct, oldPassword, newPassword, dryRun);
try {
at.deRegister();
} catch (AuthTokenException e) {
throw ServiceException.FAILURE("cannot de-register reset password auth token", e);
}
} else if (acct.isIsExternalVirtualAccount() && StringUtil.isNullOrEmpty(oldPassword)
&& !acct.isVirtualAccountInitialPasswordSet() && acct.getId().equals(zsc.getAuthtokenAccountId())) {
// need a valid auth token in this case
AuthProvider.validateAuthToken(prov, zsc.getAuthToken(), false);
prov.setPassword(acct, newPassword, true);
acct.setVirtualAccountInitialPasswordSet(true);
} else {
Expand All @@ -96,7 +123,7 @@ public Element handle(Element request, Map<String, Object> context) throws Servi

Element response = zsc.createElement(AccountConstants.CHANGE_PASSWORD_RESPONSE);
if (!dryRun) {
AuthToken at = AuthProvider.getAuthToken(acct);
at = AuthProvider.getAuthToken(acct);
at.encodeAuthResp(response, false);
response.addAttribute(AccountConstants.E_LIFETIME, at.getExpires() - System.currentTimeMillis(), Element.Disposition.CONTENT);
}
Expand All @@ -112,6 +139,6 @@ public boolean needsAuth(Map<String, Object> context) {
// them the means to do that. We cannot rely on the Provisioning
// changePassword check alone, or we bypass 2FA protection.

return true;
return false;
}
}

0 comments on commit 3f52278

Please sign in to comment.