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:
parent
9356fc1d76
commit
4e4a3d9bac
|
@ -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);
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue