IDEMPIERE-4782 Multi-factor authentication (FHCA-2034) (#955)

- support for System user with access to multiple tenants
- default to re-register device when expired
This commit is contained in:
Carlos Ruiz 2021-10-29 08:48:36 +02:00 committed by GitHub
parent 9356fc1d76
commit 4e4a3d9bac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 66 additions and 50 deletions

View File

@ -67,10 +67,9 @@ public class MMFARegisteredDevice extends X_MFA_RegisteredDevice {
* @return true if device is valid
*/
public static boolean isValid(String identifier) {
final String where = "AD_User_ID=? AND MFADeviceIdentifier=? AND Expiration>SYSDATE";
final String where = "AD_Client_ID IN (0,?) AND AD_User_ID=? AND MFADeviceIdentifier=? AND Expiration>SYSDATE";
MMFARegisteredDevice rd = new Query(Env.getCtx(), Table_Name, where, null)
.setParameters(Env.getAD_User_ID(Env.getCtx()), identifier)
.setClient_ID()
.setParameters(Env.getAD_Client_ID(Env.getCtx()), Env.getAD_User_ID(Env.getCtx()), identifier)
.setOnlyActiveRecords(true)
.first();
return (rd != null);

View File

@ -95,7 +95,7 @@ public class MMFARegistration extends X_MFA_Registration {
List<Object> params = new ArrayList<Object>();
params.add(Env.getAD_User_ID(method.getCtx()));
params.add(method.getMFA_Method_ID());
params.add(Env.getAD_Client_ID(method.getCtx()));
params.add(reg.getAD_Client_ID());
params.add(reg.getMFA_Registration_ID());
StringBuilder sql = new StringBuilder();
sql.append("UPDATE MFA_Registration"

View File

@ -77,6 +77,8 @@ public class EMailMechanism implements IMFAMechanism {
MUser user = MUser.get(ctx);
MMFARegistration reg = new MMFARegistration(ctx, 0, trxName);
reg.set_ValueOfColumn(MMFARegistration.COLUMNNAME_AD_Client_ID, user.getAD_Client_ID());
reg.setAD_Org_ID(0);
reg.setName(obfuscateEMail(prm));
reg.setParameterValue(prm);
reg.setMFA_Method_ID(method.getMFA_Method_ID());
@ -85,7 +87,7 @@ public class EMailMechanism implements IMFAMechanism {
reg.setIsValid(false);
reg.setIsUserMFAPreferred(false);
reg.setExpiration(new Timestamp(System.currentTimeMillis() + (expireMinutes * 60000)));
reg.saveEx();
saveRegistration(reg);
// send the email
MClient client = MClient.get(ctx);
@ -148,12 +150,7 @@ public class EMailMechanism implements IMFAMechanism {
if (! valid) {
reg.setLastFailure(new Timestamp(System.currentTimeMillis()));
reg.setFailedLoginCount(reg.getFailedLoginCount() + 1);
try {
PO.setCrossTenantSafe();
reg.saveEx();
} finally {
PO.clearCrossTenantSafe();
}
saveRegistration(reg);
throw new AdempiereException(Msg.getMsg(ctx, "MFACodeInvalid"));
}
@ -168,12 +165,7 @@ public class EMailMechanism implements IMFAMechanism {
reg.setName(name);
if (preferred)
reg.setIsUserMFAPreferred(true);
try {
PO.setCrossTenantSafe();
reg.saveEx();
} finally {
PO.clearCrossTenantSafe();
}
saveRegistration(reg);
return Msg.getMsg(ctx, "MFARegistrationCompleted");
}
@ -198,12 +190,7 @@ public class EMailMechanism implements IMFAMechanism {
MUser user = MUser.get(reg.getCtx());
reg.setMFASecret(otp);
reg.setExpiration(new Timestamp(System.currentTimeMillis() + (expireMinutes * 60000)));
try {
PO.setCrossTenantSafe();
reg.saveEx();
} finally {
PO.clearCrossTenantSafe();
}
saveRegistration(reg);
String mail_to = reg.getParameterValue();
// send the email
@ -268,7 +255,7 @@ public class EMailMechanism implements IMFAMechanism {
if (! valid) {
reg.setLastFailure(new Timestamp(System.currentTimeMillis()));
reg.setFailedLoginCount(reg.getFailedLoginCount() + 1);
reg.saveEx();
saveRegistration(reg);
return Msg.getMsg(ctx, "MFACodeInvalid");
}
@ -277,14 +264,22 @@ public class EMailMechanism implements IMFAMechanism {
reg.setFailedLoginCount(0);
if (setPreferred)
reg.setIsUserMFAPreferred(true);
saveRegistration(reg);
return null;
}
/**
* Save the registration record allowing cross-tenant (saving for a user in System tenant)
* @param reg
*/
private void saveRegistration(MMFARegistration reg) {
try {
PO.setCrossTenantSafe();
reg.saveEx();
} finally {
PO.clearCrossTenantSafe();
}
return null;
}
}

View File

@ -109,6 +109,8 @@ public class TOTPMechanism implements IMFAMechanism {
int expireMinutes = method.getExpireInMinutes();
MMFARegistration reg = new MMFARegistration(ctx, 0, trxName);
reg.set_ValueOfColumn(MMFARegistration.COLUMNNAME_AD_Client_ID, user.getAD_Client_ID());
reg.setAD_Org_ID(0);
if (! Util.isEmpty(prm)) {
reg.setName(prm);
reg.setParameterValue(prm);
@ -122,7 +124,7 @@ public class TOTPMechanism implements IMFAMechanism {
reg.setIsUserMFAPreferred(false);
if (expireMinutes > 0)
reg.setExpiration(new Timestamp(System.currentTimeMillis() + (expireMinutes*60000)));
reg.saveEx();
saveRegistration(reg);
// Invalidate any other previous pending registration with same method
MMFARegistration.invalidatePreviousPending(method, prm, reg);
@ -161,7 +163,7 @@ public class TOTPMechanism implements IMFAMechanism {
reg.setName(name);
if (preferred)
reg.setIsUserMFAPreferred(true);
reg.saveEx();
saveRegistration(reg);
return Msg.getMsg(ctx, "MFARegistrationCompleted");
}
@ -204,12 +206,7 @@ public class TOTPMechanism implements IMFAMechanism {
reg.setLastFailure(new Timestamp(System.currentTimeMillis()));
reg.setFailedLoginCount(reg.getFailedLoginCount() + 1);
}
try {
PO.setCrossTenantSafe();
reg.saveEx();
} finally {
PO.clearCrossTenantSafe();
}
saveRegistration(reg);
return valid;
}
@ -241,15 +238,23 @@ public class TOTPMechanism implements IMFAMechanism {
if (setPreferred) {
reg.setIsUserMFAPreferred(true);
try {
PO.setCrossTenantSafe();
reg.saveEx();
} finally {
PO.clearCrossTenantSafe();
}
saveRegistration(reg);
}
return null;
}
/**
* Save the registration record allowing cross-tenant (saving for a user in System tenant)
* @param reg
*/
private void saveRegistration(MMFARegistration reg) {
try {
PO.setCrossTenantSafe();
reg.saveEx();
} finally {
PO.clearCrossTenantSafe();
}
}
}

View File

@ -56,6 +56,7 @@ import org.compiere.model.MMFARegisteredDevice;
import org.compiere.model.MMFARegistration;
import org.compiere.model.MSysConfig;
import org.compiere.model.MUser;
import org.compiere.model.PO;
import org.compiere.util.CLogger;
import org.compiere.util.Env;
import org.compiere.util.KeyNamePair;
@ -82,7 +83,7 @@ public class ValidateMFAPanel extends Window implements EventListener<Event> {
/**
*
*/
private static final long serialVersionUID = 5521412080450156787L;
private static final long serialVersionUID = -2347409338340527333L;
private static final CLogger logger = CLogger.getCLogger(ValidateMFAPanel.class);
@ -121,11 +122,10 @@ public class ValidateMFAPanel extends Window implements EventListener<Event> {
this.m_orgKNPair = orgKNPair;
this.component = this;
String cookieName = Env.getAD_User_ID(m_ctx) + "|" + Env.getAD_Client_ID(m_ctx);
String registerCookie = getCookie(cookieName);
String registerCookie = getCookie(getCookieName());
login = new Login(ctx);
if (login.isMFARequired(registerCookie)) {
initComponents();
initComponents(registerCookie != null);
init();
this.setId("validateMFAPanel");
this.setSclass("login-box");
@ -242,7 +242,7 @@ public class ValidateMFAPanel extends Window implements EventListener<Event> {
this.appendChild(div);
}
private void initComponents() {
private void initComponents(boolean hasCookie) {
lblMFAMechanism = new Label();
lblMFAMechanism.setId("lblMFAMechanism");
lblMFAMechanism.setValue(Msg.getMsg(m_ctx, "MFALoginMechanism"));
@ -288,7 +288,7 @@ public class ValidateMFAPanel extends Window implements EventListener<Event> {
chkRegisterDevice.setId("chkRegisterDevice");
boolean enableRegisterDevice = (daysExpire > 0);
chkRegisterDevice.setVisible(enableRegisterDevice);
chkRegisterDevice.setChecked(false);
chkRegisterDevice.setChecked(hasCookie);
txtValidationCode = new Textbox();
txtValidationCode.setId("txtValidationCode");
@ -358,17 +358,24 @@ public class ValidateMFAPanel extends Window implements EventListener<Event> {
}
if (chkRegisterDevice != null && chkRegisterDevice.isChecked()) {
String cookieName = Env.getAD_User_ID(m_ctx) + "|" + Env.getAD_Client_ID(m_ctx);
// TODO: generate the random cookie if possible with some fingerprint of the device
String cookieValue = UUID.randomUUID().toString();
setCookie(cookieName, cookieValue);
setCookie(getCookieName(), cookieValue);
MUser user = MUser.get(Env.getCtx());
MMFARegisteredDevice rd = new MMFARegisteredDevice(m_ctx, 0, null);
rd.setAD_User_ID(Env.getAD_User_ID(m_ctx));
rd.set_ValueOfColumn(MMFARegistration.COLUMNNAME_AD_Client_ID, user.getAD_Client_ID());
rd.setAD_Org_ID(0);
rd.setAD_User_ID(user.getAD_User_ID());
rd.setMFADeviceIdentifier(cookieValue);
long daysExpire = MSysConfig.getIntValue(MSysConfig.MFA_REGISTERED_DEVICE_EXPIRATION_DAYS, 30, Env.getAD_Client_ID(m_ctx));
rd.setExpiration(new Timestamp(System.currentTimeMillis() + (daysExpire * 86400000L)));
// TODO: rd.setHelp -> add information about the browser, device and IP address (fingerprint)
rd.saveEx();
try {
PO.setCrossTenantSafe();
rd.saveEx();
} finally {
PO.clearCrossTenantSafe();
}
}
Session currSess = Executions.getCurrent().getDesktop().getSession();
@ -408,6 +415,16 @@ public class ValidateMFAPanel extends Window implements EventListener<Event> {
wndLogin.loginCompleted();
}
/**
* The cookie name for the MFA registered device
* @return
*/
private String getCookieName() {
StringBuilder sb = new StringBuilder("UD_") // User Device
.append(Env.getAD_User_ID(m_ctx));
return sb.toString();
}
/**
* Set a cookie
* @param name