/** * Copyright 2013 Tommi S.E. Laukkanen * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.bubblecloud.ilves.security; import org.apache.commons.lang.ArrayUtils; import org.apache.directory.api.ldap.model.cursor.EntryCursor; import org.apache.directory.api.ldap.model.entry.Entry; import org.apache.directory.api.ldap.model.exception.LdapException; import org.apache.directory.api.ldap.model.message.SearchScope; import org.apache.directory.ldap.client.api.LdapConnection; import org.apache.directory.ldap.client.api.LdapNetworkConnection; import org.apache.log4j.Logger; import org.bubblecloud.ilves.model.Company; import org.bubblecloud.ilves.model.Group; import org.bubblecloud.ilves.model.User; import org.bubblecloud.ilves.model.UserDirectory; import org.bubblecloud.ilves.util.StringUtil; import org.joda.time.DateTime; import javax.persistence.EntityManager; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.*; /** * Password login utility. * * @author Tommi S.E. Laukkanen */ public class PasswordLoginUtil { /** Default serial version UID. */ private static final long serialVersionUID = 1L; /** The logger. */ private static final Logger LOGGER = Logger.getLogger(PasswordLoginUtil.class); /** * Calculates and sets user password hash. Updates password expiration date. * * @param company the company (local web site) entity under which user belongs to * @param user the user * @param password the password * @throws UnsupportedEncodingException if encoding is not supported * @throws NoSuchAlgorithmException if algorithm is not supported */ public static void setUserPasswordHash(final Company company, final User user, final char[] password) throws UnsupportedEncodingException, NoSuchAlgorithmException { if (user.getUserId() == null) { user.setUserId(UUID.randomUUID().toString()); } final byte[] passwordAndSaltBytes = SecurityUtil.convertCharactersToBytes(ArrayUtils.addAll((user.getUserId() + ":").toCharArray(), password)); final MessageDigest md = MessageDigest.getInstance("SHA-256"); final byte[] passwordAndSaltDigest = md.digest(passwordAndSaltBytes); user.setPasswordHash(StringUtil.toHexString(passwordAndSaltDigest)); if (company.getPasswordValidityPeriodDays() != 0) { user.setPasswordExpirationDate(new DateTime().plusDays(company.getPasswordValidityPeriodDays()).toDate()); } else { user.setPasswordExpirationDate(null); } } /** * Attempt password login through local password or LDAP directory. * * @param remoteHost the remote host from which user is attempting to login * @param remoteIpAddress the remote IP address from which user is attempting to login * @param remotePort the remote port from which user is attempting to login * @param entityManager the entity manager for database access * @param company the company (local web site) entity under which user belongs to * @param user the user entity * @param userPassword the user password * @return null on success and error localization key on failure. */ public static String login(final String emailAddress, final String remoteHost, final String remoteIpAddress, final int remotePort, final EntityManager entityManager, final Company company, final User user, final char[] userPassword) { if (user == null) { LOGGER.warn("User login failed due to not registered email address: " + emailAddress + " (Remote address: " + remoteHost + " (" + remoteIpAddress + "):" + remotePort + ")"); return "message-login-failed"; } if (user.isLockedOut()) { LOGGER.warn("User login failed due to user being locked out: " + user.getEmailAddress() + " (Remote address: " + remoteHost + " (" + remoteIpAddress + "):" + remotePort + ")"); return "message-login-failed"; } try { final List<UserDirectory> userDirectories = UserDirectoryDao.getUserDirectories(entityManager, company); for (final UserDirectory userDirectory : userDirectories) { if (!userDirectory.isEnabled()) { continue; } final String[] subnets = userDirectory.getSubNetWhiteList().split(","); for (final String subnet : subnets) { final CidrUtil cidrUtils = new CidrUtil(subnet); if (cidrUtils.isInRange(remoteIpAddress)) { return attemptDirectoryLogin(remoteHost, remoteIpAddress, remotePort, entityManager, company, user, userPassword, userDirectory); } } } return attemptLocalLogin(remoteHost, remoteIpAddress, remotePort, entityManager, company, user, userPassword); } catch (final Exception e) { LOGGER.error("Error logging in user: " + user.getEmailAddress() + " (Remote address: " + remoteHost + " (" + remoteIpAddress + "):" + remotePort + ")", e); return "message-login-error"; } } /** * Attempt directory login. * * @param remoteHost * @param remoteIpAddress * @param remotePort * @param entityManager * @param company * @param user * @param userPassword * @param userDirectory * @return * @throws Exception */ private static String attemptDirectoryLogin( final String remoteHost, final String remoteIpAddress, final int remotePort, final EntityManager entityManager, final Company company, final User user, final char[] userPassword, final UserDirectory userDirectory) throws Exception { LOGGER.info("Attempting LDAP login: address: " + userDirectory.getAddress() + ":" + userDirectory.getPort() + ") email: " + user.getEmailAddress() + " (Remote address: " + remoteHost + " (" + remoteIpAddress + "):" + remotePort + ")"); final LdapConnection connection = new LdapNetworkConnection(userDirectory.getAddress(), userDirectory.getPort()); boolean passwordMatch = false; try { final String ldapLoginDn = userDirectory.getLoginDn(); final String ldapLoginPassword = userDirectory.getLoginPassword(); final String userEmailAttribute = userDirectory.getUserEmailAttribute(); final String userSearchBaseDn = userDirectory.getUserSearchBaseDn(); final String groupSearchBaseDn = userDirectory.getGroupSearchBaseDn(); final String userFilter = "(" + userEmailAttribute + "=" + user.getEmailAddress() + ")"; connection.bind(ldapLoginDn, ldapLoginPassword); final EntryCursor userCursor = connection.search(userSearchBaseDn, userFilter, SearchScope.ONELEVEL); if (!userCursor.next()) { LOGGER.warn("User not found from LDAP address: " + userDirectory.getAddress() + ":" + userDirectory.getPort() + ") email: " + user.getEmailAddress() + " (Remote address: " + remoteHost + " (" + remoteIpAddress + "):" + remotePort + ")"); userCursor.close(); connection.unBind(); return "message-directory-user-not-found"; } else { final Entry userEntry = userCursor.get(); userCursor.close(); connection.unBind(); connection.bind(userEntry.getDn(), new String(userPassword)); if (!isInRemoteGroup(connection, groupSearchBaseDn, userEntry, userDirectory.getRequiredRemoteGroup())) { LOGGER.warn("User not in required remote group '" + userDirectory.getRequiredRemoteGroup() + "', LDAP address: " + userDirectory.getAddress() + ":" + userDirectory.getPort() + ") email: " + user.getEmailAddress() + " (Remote address: " + remoteHost + " (" + remoteIpAddress + "):" + remotePort + ")"); return "message-login-failed"; } final List<Group> groups = UserDao.getUserGroups(entityManager, company, user); final Map<String, Group> localGroups = new HashMap<String, Group>(); for (final Group group : groups) { localGroups.put(group.getName(), group); } for (final String remoteLocalGroupPair : userDirectory.getRemoteLocalGroupMapping().split(",")) { final String[] parts = remoteLocalGroupPair.split("="); if (parts.length != 2) { continue; } final String remoteGroupName = parts[0].trim(); final String localGroupName = parts[1].trim(); final boolean remoteGroupMember = isInRemoteGroup(connection, groupSearchBaseDn, userEntry, remoteGroupName); final boolean localGroupMember = localGroups.containsKey(localGroupName); final Group localGroup = UserDao.getGroup(entityManager, company, localGroupName); if (localGroup == null) { LOGGER.warn("No local group '" + localGroupName + "'. Skipping group membership synchronization."); continue; } if (remoteGroupMember && !localGroupMember) { UserDao.addGroupMember(entityManager, localGroup, user); LOGGER.info("Added user '" + user.getEmailAddress() + "' to group '" + localGroupName + "' (Remote address: " + remoteHost + " (" + remoteIpAddress + "):" + remotePort + ")"); } else if (!remoteGroupMember && localGroupMember) { UserDao.removeGroupMember(entityManager, localGroup, user); LOGGER.info("Removed user '" + user.getEmailAddress() + "' from group '" + localGroupName + "' (Remote address: " + remoteHost + " (" + remoteIpAddress + "):" + remotePort + ")"); } } passwordMatch = true; connection.unBind(); } } catch (final LdapException exception) { LOGGER.error("LDAP error: " + user.getEmailAddress() + " (Remote address: " + remoteHost + " (" + remoteIpAddress + "):" + remotePort + ")", exception); } if (passwordMatch) { LOGGER.info("User login: " + user.getEmailAddress() + " (Remote address: " + remoteHost + " (" + remoteIpAddress + "):" + remotePort + ")"); user.setFailedLoginCount(0); UserDao.updateUser(entityManager, user); return null; } else { LOGGER.warn("User login, password mismatch: " + user.getEmailAddress() + " (Remote address: " + remoteHost + " (" + remoteIpAddress + "):" + remotePort + ")"); user.setFailedLoginCount(user.getFailedLoginCount() + 1); if (user.getFailedLoginCount() > company.getMaxFailedLoginCount()) { user.setLockedOut(true); LOGGER.warn("User locked out due to too many failed login attempts: " + user.getEmailAddress() + " (Remote address: " + remoteHost + " (" + remoteIpAddress + "):" + remotePort + ")"); } UserDao.updateUser(entityManager, user); return "message-login-failed"; } } /** * Checks whether user is in LDAP group * * @param connection * @param groupSearchBaseDn * @param userEntry * @param remoteGroupName * @return * @throws Exception */ private static boolean isInRemoteGroup(LdapConnection connection, String groupSearchBaseDn, Entry userEntry, String remoteGroupName) throws Exception { final String groupFilter = "(&(uniqueMember="+ userEntry.getDn() +")(cn=" + remoteGroupName + "))"; final EntryCursor groupCursor = connection.search(groupSearchBaseDn, groupFilter, SearchScope.ONELEVEL ); final boolean remoteGroupMember = groupCursor.next(); groupCursor.close(); return remoteGroupMember; } /** * Attempt local login. * * @param remoteHost * @param remoteIpAddress * @param remotePort * @param entityManager * @param company * @param user * @param userPassword * @return * @throws UnsupportedEncodingException * @throws NoSuchAlgorithmException */ private static String attemptLocalLogin(final String remoteHost, final String remoteIpAddress, final int remotePort, final EntityManager entityManager, final Company company, final User user, final char[] userPassword) throws UnsupportedEncodingException, NoSuchAlgorithmException { if (user.getPasswordExpirationDate() != null && System.currentTimeMillis() > user.getPasswordExpirationDate().getTime()) { LOGGER.warn("User login, password expired: " + user.getEmailAddress() + " (Remote address: " + remoteHost + " (" + remoteIpAddress + "):" + remotePort + ")"); return "message-password-expired"; } boolean passwordMatch = checkPasswordMatchWithUserIdAsSalt(user, userPassword); if (!passwordMatch) { passwordMatch = checkPasswordMatchWithEmailAsSalt(user, userPassword); } if (passwordMatch) { LOGGER.info("User login: " + user.getEmailAddress() + " (Remote address: " + remoteHost + ":" + remotePort + ")"); user.setFailedLoginCount(0); UserDao.updateUser(entityManager, user); return null; } else { LOGGER.warn("User login, password mismatch: " + user.getEmailAddress() + " (Remote address: " + remoteHost + " (" + remoteIpAddress + "):" + remotePort + ")"); user.setFailedLoginCount(user.getFailedLoginCount() + 1); if (user.getFailedLoginCount() > company.getMaxFailedLoginCount()) { user.setLockedOut(true); LOGGER.warn("User locked out due to too many failed login attempts: " + user.getEmailAddress() + " (Remote address: " + remoteHost + " (" + remoteIpAddress + "):" + remotePort + ")"); } UserDao.updateUser(entityManager, user); return "message-login-failed"; } } private static boolean checkPasswordMatchWithUserIdAsSalt(User user, char[] userPassword) throws UnsupportedEncodingException, NoSuchAlgorithmException { final byte[] passwordAndSaltBytes = SecurityUtil.convertCharactersToBytes(ArrayUtils.addAll((user.getUserId() + ":").toCharArray(), userPassword)); final MessageDigest md = MessageDigest.getInstance("SHA-256"); final String passwordAndSaltDigest = StringUtil.toHexString(md.digest(passwordAndSaltBytes)); return passwordAndSaltDigest.equals(user.getPasswordHash()); } private static boolean checkPasswordMatchWithEmailAsSalt(User user, char[] userPassword) throws UnsupportedEncodingException, NoSuchAlgorithmException { final byte[] passwordAndSaltBytes = SecurityUtil.convertCharactersToBytes(ArrayUtils.addAll((user.getEmailAddress() + ":").toCharArray(), userPassword)); final MessageDigest md = MessageDigest.getInstance("SHA-256"); final String passwordAndSaltDigest = StringUtil.toHexString(md.digest(passwordAndSaltBytes)); return passwordAndSaltDigest.equals(user.getPasswordHash()); } }