/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 2009 Sun Microsystems Inc. All Rights Reserved * * The contents of this file are subject to the terms * of the Common Development and Distribution License * (the License). You may not use this file except in * compliance with the License. * * You can obtain a copy of the License at * https://opensso.dev.java.net/public/CDDLv1.0.html or * opensso/legal/CDDLv1.0.txt * See the License for the specific language governing * permission and limitations under the License. * * When distributing Covered Code, include this CDDL * Header Notice in each file and include the License file * at opensso/legal/CDDLv1.0.txt. * If applicable, add the following below the CDDL Header, * with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * $Id: HOTP.java,v 1.1 2009/03/24 23:52:12 pluo Exp $ * * Portions Copyrighted 2013-2016 ForgeRock AS. * Portions Copyrighted 2014-2015 Nomura Research Institute, Ltd. */ package com.sun.identity.authentication.modules.hotp; import static org.forgerock.openam.utils.Time.*; import com.iplanet.sso.SSOException; import com.sun.identity.authentication.spi.AuthLoginException; import com.sun.identity.idm.AMIdentity; import com.sun.identity.idm.AMIdentityRepository; import com.sun.identity.idm.IdRepoException; import com.sun.identity.idm.IdSearchControl; import com.sun.identity.idm.IdSearchOpModifier; import com.sun.identity.idm.IdSearchResults; import com.sun.identity.idm.IdType; import com.sun.identity.shared.datastruct.CollectionHelper; import com.sun.identity.shared.debug.Debug; import org.forgerock.openam.utils.CollectionUtils; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; /** * Provides the functionality to send OTP codes to a users Telephone and email. */ public class HOTPService { private static final Debug DEBUG = Debug.getInstance(HOTP.amAuthHOTP); // TODO : the moving factor should be retrieved from user's profile private static int movingFactor = 0; private final AMIdentityRepository amIdentityRepo; private final String gatewaySMSImplClass; private final long codeValidityDuration; private String telephoneAttribute; private final String carrierAttribute; private String emailAttribute; private final String codeDelivery; private SecureRandom secureRandom; private final Map currentConfig; private final String userName; private final int codeLength; private final String messageSubject; private final String messageContent; private final String fromAddressAttributeName; private String sentHOTPCode; private long sentHOTPCodeTime; private final Set userSearchAttributes; /** * Constructs an instance of the HOTPService. * * @param amIdentityRepo An instance of the AMIdentityRepository. * @param userName The user's name. * @param hotpParams The authentication modules configuration settings. */ public HOTPService(AMIdentityRepository amIdentityRepo, String userName, HOTPParams hotpParams) { this.amIdentityRepo = amIdentityRepo; this.userName = userName; this.gatewaySMSImplClass = hotpParams.getGatewaySMSImplClass(); this.codeValidityDuration = hotpParams.getCodeValidityDuration(); this.codeLength = hotpParams.getCodeLength(); this.codeDelivery = hotpParams.getCodeDelivery(); this.telephoneAttribute = hotpParams.getTelephoneLdapAttributeName(); this.carrierAttribute = hotpParams.getCarrierLdapAttributeName(); this.emailAttribute = hotpParams.getEmailLdapAttributeName(); this.currentConfig = hotpParams.getConfig(); this.messageSubject = hotpParams.getMessageSubject(); this.messageContent = hotpParams.getMessageContent(); this.fromAddressAttributeName = hotpParams.getFromAddressAttributeName(); this.userSearchAttributes = hotpParams.getUserSearchAttributes(); try { secureRandom = SecureRandom.getInstance("SHA1PRNG"); } catch (NoSuchAlgorithmException ex) { DEBUG.error("HOTP.HOTP() : HOTP : Initialization Failed", ex); secureRandom = null; } } /** * Sends a otp code to the users telephone number and/or email address, based on the authentication module's * configuration settings. * * @throws AuthLoginException If there is a problem sending the OTP code. */ public void sendHOTP() throws AuthLoginException { try { sentHOTPCode = HOTPAlgorithm.generateOTP(getSharedSecret(), getMovingFactor(), codeLength, false, 16); } catch (NoSuchAlgorithmException e) { DEBUG.error("HOTP.sendHOTPCode() : " + "no such algorithm", e); throw new AuthLoginException("amAuth", "noSuchAlgorithm", null); } catch (InvalidKeyException e) { DEBUG.error("HOTP.sendHOTPCode() : " + "invalid key",e); throw new AuthLoginException("amAuth", "invalidKey", null); } sendHOTP(sentHOTPCode, messageSubject, messageContent); sentHOTPCodeTime = currentTimeMillis(); } private byte[] getSharedSecret() { return Long.toHexString(secureRandom.nextLong()).getBytes(); } private int getMovingFactor() { return movingFactor++; } /** * Determines if the given OTP code matches the OTP code that was sent previously. * * @param enteredHOTPCode The OTP code to verify. * @return Whether the OTP code matches the OTP code that was sent to the user. */ public boolean isValidHOTP(String enteredHOTPCode) { if (sentHOTPCode != null && sentHOTPCode.equals(enteredHOTPCode)) { long timePassed = currentTimeMillis() - sentHOTPCodeTime; if (timePassed <= (codeValidityDuration * 60000)) { // one time use only sentHOTPCode = null; return true; } else { if (DEBUG.messageEnabled()) { DEBUG.message("HOTP.process() : HOTP code has expired"); } return false; } } else { if (DEBUG.messageEnabled()) { DEBUG.message("HOTP.process() : HOTP code is not valid"); } return false; } } /** * Sends the otp code to the users telephone number and/or email address, based on the authentication module's * configuration settings. * * @param otpCode The OTP code to send. * @param subject The subject of the message. * @param message The body of the message. * @throws AuthLoginException If there is a problem sending the OTP code. */ private void sendHOTP(String otpCode, String subject, String message) throws AuthLoginException { Exception cause = null; try { AMIdentity identity = getIdentity(); if (identity == null) { throw new AuthLoginException("HOTP.sendSMS() : Unable to send OTP code " + "because of error searching identities with username : " + userName); } String phone = getTelephoneNumber(identity); String mail = getEmailAddress(identity); boolean delivered = false; if (phone != null || mail != null) { String from = CollectionHelper.getMapAttr(currentConfig, fromAddressAttributeName); SMSGateway gateway = Class.forName(gatewaySMSImplClass).asSubclass(SMSGateway.class).newInstance(); if (codeDelivery.equals("SMS and E-mail")) { try { if (phone != null) { gateway.sendSMSMessage(from, phone, subject, message, otpCode, currentConfig); delivered = true; } } catch (AuthLoginException ale) { DEBUG.error("Error while sending HOTP code to user via SMS", ale); cause = ale; } try { if (mail != null) { gateway.sendEmail(from, mail, subject, message, otpCode, currentConfig); delivered = true; } } catch (AuthLoginException ale) { DEBUG.error("Error while sending HOTP code to user via e-mail", ale); cause = ale; } if (!delivered && cause != null) { throw cause; } } else if (codeDelivery.equals("SMS")) { gateway.sendSMSMessage(from, phone, subject, message, otpCode, currentConfig); } else if (codeDelivery.equals("E-mail")) { gateway.sendEmail(from, mail, subject, message, otpCode, currentConfig); } } else { if (DEBUG.messageEnabled()) { DEBUG.message("HOTP.sendSMS() : IdRepo: no phone or email found with username : " + userName); } throw new AuthLoginException("HOTP.sendSMS() : Unable to send OTP code " + "because no phone or e-mail found for user: " + userName); } } catch (ClassNotFoundException ee) { DEBUG.error("HOTP.sendSMS() : " + "class not found SMSGateway class", ee); cause = ee; } catch (InstantiationException ie) { DEBUG.error("HOTP.sendSMS() : " + "can not instantiate SMSGateway class", ie); cause = ie; } catch (IdRepoException e) { DEBUG.error("HOTP.sendSMS() : error searching Identities with username : " + userName, e); cause = e; } catch (AuthLoginException e) { throw e; } catch (Exception e) { DEBUG.error("HOTP.sendSMS() : HOTP module exception : ", e); cause = e; } if (cause != null) { throw new AuthLoginException("HOTP.sendSMS() : Unable to send OTP code", cause); } } private AMIdentity getIdentity() { AMIdentity amIdentity = null; IdSearchControl idsc = new IdSearchControl(); idsc.setRecursive(true); idsc.setTimeOut(0); final Set returnAttributes = getReturnAttributes(); idsc.setReturnAttributes(returnAttributes); // search for the identity Set results = Collections.EMPTY_SET; idsc.setMaxResults(0); IdSearchResults searchResults; try { searchResults = amIdentityRepo.searchIdentities(IdType.USER, userName, idsc); if (searchResults.getSearchResults().isEmpty() && !userSearchAttributes.isEmpty()) { if (DEBUG.messageEnabled()) { DEBUG.message("HOTP.getIdentity: searching user identity " + "with alternative attributes " + userSearchAttributes); } final Map> searchAVP = CollectionUtils.toAvPairMap(userSearchAttributes, userName); idsc.setSearchModifiers(IdSearchOpModifier.OR, searchAVP); // workaround as data store always adds 'user-naming-attribute' to searchfilter searchResults = amIdentityRepo.searchIdentities(IdType.USER, "*", idsc); } if (searchResults != null) { results = searchResults.getSearchResults(); } if (results.isEmpty()) { DEBUG.error("HTOP:getIdentity : User " + userName + " is not found"); } else if (results.size() > 1) { DEBUG.error("HTOP:getIdentity : More than one user found for the userName " + userName); } else { amIdentity = results.iterator().next(); } } catch (IdRepoException e) { DEBUG.error("HTOP.getIdentity : Error searching Identities with username : " + userName, e); } catch (SSOException e) { DEBUG.error("HTOP.getIdentity : Module exception : ", e); } return amIdentity; } /** * Gets the Telephone number of the user. * * @param identity The user's identity. * @return The user's telephone number. * @throws IdRepoException If there is a problem getting the user's telephone number. * @throws SSOException If there is a problem getting the user's telephone number. */ private String getTelephoneNumber(AMIdentity identity) throws IdRepoException, SSOException { if (telephoneAttribute == null || telephoneAttribute.trim().length() == 0) { telephoneAttribute="telephoneNumber"; } if (DEBUG.messageEnabled()) { DEBUG.message("HOTP.sendSMS() : Using phone attribute of " + telephoneAttribute); } Set telephoneNumbers = identity.getAttribute(telephoneAttribute); String phone = null; Iterator itor = null; if (telephoneNumbers != null && !telephoneNumbers.isEmpty()) { // use the first number in the set itor = telephoneNumbers.iterator(); phone = (String) itor.next(); if (carrierAttribute!=null && carrierAttribute.trim().length()>0) { // add in the carrier Set carriers = identity.getAttribute(carrierAttribute); if (carriers != null && !carriers.isEmpty()) { Iterator itca = carriers.iterator(); String carrier = (String) itca.next(); if (carrier.startsWith("@")) { phone=phone.concat(carrier); } else { phone=phone.concat("@"+carrier); } if (DEBUG.messageEnabled()) { DEBUG.message("HOTP.sendSMS() : Using carrier attribute of " + carrierAttribute); } } } if (DEBUG.messageEnabled()) { DEBUG.message("HOTP.sendSMS() : " + "IdRepoException : phone number found " + phone + " with username : " + userName); /*                      * Log a message if the carrier is unknown.  The SMSGateway module is designed to use AT&T's SMS gateway                      * as default.  Not sure why the product uses a default in this situation instead of simply not attempting                       * to send a text message but we don't want to break any existing installations so just log it for debug                      * purposes.                      *                       */ if (!phone.contains("@")) { DEBUG.message("HOTP.sendSMS() : No carrier detected - SMSGateway module will use default of " + "@txt.att.net "); } } } else { if (DEBUG.messageEnabled()) { DEBUG.message("HOTP.sendSMS() : " + "IdRepoException : no phone number found with username : " + userName); } } return phone; } /** * Gets the Email address of the user. * * @param identity The user's identity. * @return The user's email address. * @throws IdRepoException If there is a problem getting the user's email address. * @throws SSOException If there is a problem getting the user's email address. */ private String getEmailAddress(AMIdentity identity) throws IdRepoException, SSOException { if (emailAttribute==null || emailAttribute.trim().length()==0) { emailAttribute="mail"; } if (DEBUG.messageEnabled()) { DEBUG.message("HOTP.sendSMS() : " + "Using email attribute of " + emailAttribute); } Set emails = identity.getAttribute(emailAttribute); Iterator itor = null; String mail = null; if (emails != null && !emails.isEmpty()) { itor = emails.iterator(); mail = (String) itor.next(); if (DEBUG.messageEnabled()) { DEBUG.message("HOTP.sendSMS() : IdRepo: email address found " + mail + " with username : " + userName); } } else { if (DEBUG.messageEnabled()) { DEBUG.message("HOTP.sendSMS() : IdRepo: no email found with username : " + userName); } } return mail; } /** * * @return the attributes to be returned when querying the data store */ private Set getReturnAttributes() { Set returnAttributes = new HashSet(2); if ((emailAttribute != null) && (emailAttribute.trim().length() != 0)) { returnAttributes.add(emailAttribute); } if ((telephoneAttribute != null) && (telephoneAttribute.trim().length() != 0)) { returnAttributes.add(telephoneAttribute); } return returnAttributes; } }