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 * @return true if device is valid
*/ */
public static boolean isValid(String identifier) { 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) MMFARegisteredDevice rd = new Query(Env.getCtx(), Table_Name, where, null)
.setParameters(Env.getAD_User_ID(Env.getCtx()), identifier) .setParameters(Env.getAD_Client_ID(Env.getCtx()), Env.getAD_User_ID(Env.getCtx()), identifier)
.setClient_ID()
.setOnlyActiveRecords(true) .setOnlyActiveRecords(true)
.first(); .first();
return (rd != null); return (rd != null);

View File

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

View File

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

View File

@ -109,6 +109,8 @@ public class TOTPMechanism implements IMFAMechanism {
int expireMinutes = method.getExpireInMinutes(); int expireMinutes = method.getExpireInMinutes();
MMFARegistration reg = new MMFARegistration(ctx, 0, trxName); 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)) { if (! Util.isEmpty(prm)) {
reg.setName(prm); reg.setName(prm);
reg.setParameterValue(prm); reg.setParameterValue(prm);
@ -122,7 +124,7 @@ public class TOTPMechanism implements IMFAMechanism {
reg.setIsUserMFAPreferred(false); reg.setIsUserMFAPreferred(false);
if (expireMinutes > 0) if (expireMinutes > 0)
reg.setExpiration(new Timestamp(System.currentTimeMillis() + (expireMinutes*60000))); reg.setExpiration(new Timestamp(System.currentTimeMillis() + (expireMinutes*60000)));
reg.saveEx(); saveRegistration(reg);
// Invalidate any other previous pending registration with same method // Invalidate any other previous pending registration with same method
MMFARegistration.invalidatePreviousPending(method, prm, reg); MMFARegistration.invalidatePreviousPending(method, prm, reg);
@ -161,7 +163,7 @@ public class TOTPMechanism implements IMFAMechanism {
reg.setName(name); reg.setName(name);
if (preferred) if (preferred)
reg.setIsUserMFAPreferred(true); reg.setIsUserMFAPreferred(true);
reg.saveEx(); saveRegistration(reg);
return Msg.getMsg(ctx, "MFARegistrationCompleted"); return Msg.getMsg(ctx, "MFARegistrationCompleted");
} }
@ -204,12 +206,7 @@ public class TOTPMechanism implements IMFAMechanism {
reg.setLastFailure(new Timestamp(System.currentTimeMillis())); reg.setLastFailure(new Timestamp(System.currentTimeMillis()));
reg.setFailedLoginCount(reg.getFailedLoginCount() + 1); reg.setFailedLoginCount(reg.getFailedLoginCount() + 1);
} }
try { saveRegistration(reg);
PO.setCrossTenantSafe();
reg.saveEx();
} finally {
PO.clearCrossTenantSafe();
}
return valid; return valid;
} }
@ -241,6 +238,17 @@ public class TOTPMechanism implements IMFAMechanism {
if (setPreferred) { if (setPreferred) {
reg.setIsUserMFAPreferred(true); 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 { try {
PO.setCrossTenantSafe(); PO.setCrossTenantSafe();
reg.saveEx(); reg.saveEx();
@ -249,7 +257,4 @@ public class TOTPMechanism implements IMFAMechanism {
} }
} }
return null;
}
} }

View File

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