/*********************************************************************** * This file is part of iDempiere ERP Open Source * * http://www.idempiere.org * * * * Copyright (C) Contributors * * * * 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; either version 2 * * of the License, or (at your option) any later version. * * * * 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, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * * MA 02110-1301, USA. * * * * Contributors: * * - Carlos Ruiz (sponsored by FH) * **********************************************************************/ package org.idempiere.mfa; import java.sql.Timestamp; import java.util.Properties; import java.util.Random; import org.adempiere.exceptions.AdempiereException; import org.compiere.model.IMFAMechanism; import org.compiere.model.MClient; import org.compiere.model.MMFAMethod; import org.compiere.model.MMFARegistration; import org.compiere.model.MMailText; import org.compiere.model.MUser; import org.compiere.util.EMail; import org.compiere.util.Env; import org.compiere.util.Msg; import org.compiere.util.Util; /** * Email based Multi-factor authentication implementation. */ public class EMailMechanism implements IMFAMechanism { /** * Implement the registration mechanism for EMail Generate random code and * return in the array * * @param ctx * @param method * @param prm email * @param trxName * @return Object[] - first object is the String with the instructions to follow, * second object is the registration generated */ @Override public Object[] register(Properties ctx, MMFAMethod method, String prm, String trxName) { if (Util.isEmpty(prm)) throw new AdempiereException(Msg.getMsg(ctx, "MFAEMailRequired")); if (!EMail.validate(prm)) throw new AdempiereException(Msg.getMsg(ctx, "MFAInvalidEMail")); if (method.getR_MailText_ID() <= 0) throw new AdempiereException("EMail method wrongly configured - requires mail template"); // just one time allowed per EMail if (MMFARegistration.alreadyExistsValid(method, prm)) throw new AdempiereException(Msg.getMsg(ctx, "MFAMethodAlreadyRegistered")); // Generate a random code and save it in MFA_Registration String otp = generateRandomString(6); int expireMinutes = method.getExpireInMinutes(); if (expireMinutes <= 0) expireMinutes = 15; // default to 15 minutes 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()); reg.setAD_User_ID(user.getAD_User_ID()); reg.setMFASecret(otp); reg.setIsValid(false); reg.setIsUserMFAPreferred(false); reg.setExpiration(new Timestamp(System.currentTimeMillis() + (expireMinutes * 60000))); saveRegistration(reg); // send the email MClient client = MClient.get(ctx); MMailText mt = new MMailText(ctx, method.getR_MailText_ID(), trxName); mt.setLanguage(Env.getContext(ctx, "#AD_Language")); mt.setUser(user.getAD_User_ID()); mt.setPO(reg); String message = mt.getMailText(true); EMail email = client.createEMail(prm, mt.getMailHeader(), message, mt.isHtml()); if (mt.isHtml()) email.setMessageHTML(mt.getMailHeader(), message); else { email.setSubject(mt.getMailHeader()); email.setMessageText(message); } if (!email.isValid() && !email.isValid(true)) throw new AdempiereException("The EMail is not valid, check configuration"); if (!EMail.SENT_OK.equals(email.send())) throw new AdempiereException(Msg.getMsg(ctx, "MFAProblemSendingEMail")); // Invalidate any other previous pending registration with same method and email MMFARegistration.invalidatePreviousPending(method, prm, reg); // Notify the user to check the email for the code Object[] ret = new Object[2]; ret[0] = Msg.getMsg(Env.getCtx(), "MFAEMailCodeSent"); ret[1] = reg; return ret; } /** * Generates a numeric random string of the specified length * * @param len * @return random String */ public static String generateRandomString(int len) { // String chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; // if upper alphanumeric is wanted String chars = "0123456789"; Random rnd = new Random(); StringBuilder sb = new StringBuilder(len); for (int i = 0; i < len; i++) sb.append(chars.charAt(rnd.nextInt(chars.length()))); return sb.toString(); } /** * Complete/Validate a previous EMail registration * * @param ctx * @param reg The registration object * @param p_MFAValidationCode The code to be validated * @param p_Name Optional - a name to assign the registration * @param get_TrxName * @return msg A message indicating success, errors throw exception */ @Override public String complete(Properties ctx, MMFARegistration reg, String code, String name, boolean preferred, String trxName) { boolean valid = code.equals(reg.getMFASecret()); if (! valid) { reg.setLastFailure(new Timestamp(System.currentTimeMillis())); reg.setFailedLoginCount(reg.getFailedLoginCount() + 1); saveRegistration(reg); throw new AdempiereException(Msg.getMsg(ctx, "MFACodeInvalid")); } // valid code reg.setMFALastSecret(code); reg.setLastSuccess(new Timestamp(System.currentTimeMillis())); reg.setFailedLoginCount(0); reg.setIsValid(true); reg.setMFAValidatedAt(new Timestamp(System.currentTimeMillis())); reg.setExpiration(null); if (!Util.isEmpty(name)) reg.setName(name); if (preferred) reg.setIsUserMFAPreferred(true); saveRegistration(reg); return Msg.getMsg(ctx, "MFARegistrationCompleted"); } /** * Send email with validation code * @param reg * @return */ @Override public String generateValidationCode(MMFARegistration reg) { Properties ctx = reg.getCtx(); MMFAMethod method = new MMFAMethod(ctx, reg.getMFA_Method_ID(), reg.get_TrxName()); // Generate a random code and save it in MFA_Registration String otp = generateRandomString(6); int expireMinutes = method.getExpireInMinutes(); if (expireMinutes <= 0) expireMinutes = 15; // default to 15 minutes MUser user = MUser.get(reg.getCtx()); reg.setMFASecret(otp); reg.setExpiration(new Timestamp(System.currentTimeMillis() + (expireMinutes * 60000))); saveRegistration(reg); String mail_to = reg.getParameterValue(); // send the email MClient client = MClient.get(ctx); MMailText mt = new MMailText(ctx, method.getR_MailText_ID(), reg.get_TrxName()); mt.setLanguage(Env.getContext(ctx, "#AD_Language")); mt.setUser(user.getAD_User_ID()); mt.setPO(reg); String message = mt.getMailText(true); EMail email = client.createEMail(mail_to, mt.getMailHeader(), message, mt.isHtml()); if (mt.isHtml()) email.setMessageHTML(mt.getMailHeader(), message); else { email.setSubject(mt.getMailHeader()); email.setMessageText(message); } if (!email.isValid() && !email.isValid(true)) throw new AdempiereException("The EMail is not valid, check configuration"); if (!EMail.SENT_OK.equals(email.send())) throw new AdempiereException(Msg.getMsg(ctx, "MFAProblemSendingEMail")); return Msg.getMsg(Env.getCtx(), "MFAEMailValidationCodeSent", new Object[] { obfuscateEMail(mail_to) }); } /** * Replace internal characters in email with * * @param mail * @return */ private String obfuscateEMail(String mail) { StringBuilder mailObfuscated = new StringBuilder(); boolean atFound = false; for (int idx = 0; idx < mail.length(); idx++) { char chr = mail.charAt(idx); if (chr == '@') { atFound = true; } else if ((!atFound && idx > 1) || (atFound && idx < mail.length() - 4)) { chr = '*'; } mailObfuscated.append(chr); } return mailObfuscated.toString(); } /** * Validate a code * @param reg * @param code * @param setPreferred * @return message on error, null when OK */ @Override public String validateCode(MMFARegistration reg, String code, boolean setPreferred) { Properties ctx = reg.getCtx(); Timestamp now = new Timestamp(System.currentTimeMillis()); if (reg.getExpiration() != null && now.after(reg.getExpiration())) return Msg.getMsg(ctx, "MFARegistrationExpired"); if (code.equals(reg.getMFALastSecret())) return Msg.getMsg(ctx, "MFACodeAlreadyConsumed"); boolean valid = code.equals(reg.getMFASecret()); if (! valid) { reg.setLastFailure(new Timestamp(System.currentTimeMillis())); reg.setFailedLoginCount(reg.getFailedLoginCount() + 1); saveRegistration(reg); return Msg.getMsg(ctx, "MFACodeInvalid"); } reg.setMFALastSecret(code); reg.setLastSuccess(new Timestamp(System.currentTimeMillis())); 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) { reg.saveCrossTenantSafeEx(); } }