/* * #%L * BroadleafCommerce Open Admin Platform * %% * Copyright (C) 2009 - 2013 Broadleaf Commerce * %% * 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. * #L% */ package org.broadleafcommerce.openadmin.server.security.service; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang.RandomStringUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.broadleafcommerce.common.email.service.EmailService; import org.broadleafcommerce.common.email.service.info.EmailInfo; import org.broadleafcommerce.common.security.util.PasswordChange; import org.broadleafcommerce.common.security.util.PasswordUtils; import org.broadleafcommerce.common.service.GenericResponse; import org.broadleafcommerce.common.time.SystemTime; import org.broadleafcommerce.common.util.BLCSystemProperty; import org.broadleafcommerce.openadmin.server.security.dao.AdminPermissionDao; import org.broadleafcommerce.openadmin.server.security.dao.AdminRoleDao; import org.broadleafcommerce.openadmin.server.security.dao.AdminUserDao; import org.broadleafcommerce.openadmin.server.security.dao.ForgotPasswordSecurityTokenDao; import org.broadleafcommerce.openadmin.server.security.domain.AdminPermission; import org.broadleafcommerce.openadmin.server.security.domain.AdminRole; import org.broadleafcommerce.openadmin.server.security.domain.AdminUser; import org.broadleafcommerce.openadmin.server.security.domain.ForgotPasswordSecurityToken; import org.broadleafcommerce.openadmin.server.security.domain.ForgotPasswordSecurityTokenImpl; import org.broadleafcommerce.openadmin.server.security.service.type.PermissionType; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.dao.SaltSource; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import javax.annotation.PostConstruct; import javax.annotation.Resource; /** * * @author jfischer * */ @Service("blAdminSecurityService") public class AdminSecurityServiceImpl implements AdminSecurityService { private static final Log LOG = LogFactory.getLog(AdminSecurityServiceImpl.class); private static int PASSWORD_TOKEN_LENGTH = 12; @Resource(name = "blAdminRoleDao") protected AdminRoleDao adminRoleDao; @Resource(name = "blAdminUserDao") protected AdminUserDao adminUserDao; @Resource(name = "blForgotPasswordSecurityTokenDao") protected ForgotPasswordSecurityTokenDao forgotPasswordSecurityTokenDao; @Resource(name = "blAdminPermissionDao") protected AdminPermissionDao adminPermissionDao; /** * <p>Set by {@link #setupPasswordEncoder()} if the blPasswordEncoder bean provided is the deprecated version. * * @deprecated Spring Security has deprecated this encoder interface, this will be removed in 4.2 */ @Deprecated protected org.springframework.security.authentication.encoding.PasswordEncoder passwordEncoder; /** * <p>Set by {@link #setupPasswordEncoder()} if the blPasswordEncoder bean provided is the new version. */ protected PasswordEncoder passwordEncoderNew; /** * <p>This is simply a placeholder to be used by {@link #setupPasswordEncoder()} to determine if we're using the * new {@link PasswordEncoder} or the deprecated {@link org.springframework.security.authentication.encoding.PasswordEncoder PasswordEncoder} */ @Resource(name="blAdminPasswordEncoder") protected Object passwordEncoderBean; /** * Optional password salt to be used with the passwordEncoder * * @deprecated use {@link #saltSource} instead, this will be removed in 4.2 */ @Deprecated protected String salt; /** * Use a Salt Source ONLY if there's one configured * * @deprecated the new {@link PasswordEncoder} handles salting internally, this will be removed in 4.2 */ @Deprecated @Autowired(required=false) @Qualifier("blAdminSaltSource") protected SaltSource saltSource; @Resource(name="blEmailService") protected EmailService emailService; @Resource(name="blSendAdminResetPasswordEmail") protected EmailInfo resetPasswordEmailInfo; @Resource(name="blSendAdminUsernameEmailInfo") protected EmailInfo sendUsernameEmailInfo; /** * <p>Sets either {@link #passwordEncoder} or {@link #passwordEncoderNew} based on the type of {@link #passwordEncoderBean} * in order to provide bean configuration backwards compatibility with the deprecated {@link org.springframework.security.authentication.encoding.PasswordEncoder PasswordEncoder} bean. * * <p>{@link #passwordEncoderBean} is set by the bean defined as "blPasswordEncoder". * * <p>This class will utilize either the new or deprecated PasswordEncoder type depending on which is not null. * * @throws NoSuchBeanDefinitionException if {@link #passwordEncoderBean} is null or not an instance of either PasswordEncoder */ @PostConstruct protected void setupPasswordEncoder() { passwordEncoderNew = null; passwordEncoder = null; if (passwordEncoderBean instanceof PasswordEncoder) { passwordEncoderNew = (PasswordEncoder) passwordEncoderBean; } else if (passwordEncoderBean instanceof org.springframework.security.authentication.encoding.PasswordEncoder) { passwordEncoder = (org.springframework.security.authentication.encoding.PasswordEncoder) passwordEncoderBean; } else { throw new NoSuchBeanDefinitionException("No PasswordEncoder bean is defined"); } } protected int getTokenExpiredMinutes() { return BLCSystemProperty.resolveIntSystemProperty("tokenExpiredMinutes"); } protected String getResetPasswordURL() { return BLCSystemProperty.resolveSystemProperty("resetPasswordURL"); } @Override @Transactional("blTransactionManager") public void deleteAdminPermission(AdminPermission permission) { adminPermissionDao.deleteAdminPermission(permission); } @Override @Transactional("blTransactionManager") public void deleteAdminRole(AdminRole role) { adminRoleDao.deleteAdminRole(role); } @Override @Transactional("blTransactionManager") public void deleteAdminUser(AdminUser user) { adminUserDao.deleteAdminUser(user); } @Override public AdminPermission readAdminPermissionById(Long id) { return adminPermissionDao.readAdminPermissionById(id); } @Override public AdminRole readAdminRoleById(Long id) { return adminRoleDao.readAdminRoleById(id); } @Override public AdminUser readAdminUserById(Long id) { return adminUserDao.readAdminUserById(id); } @Override @Transactional("blTransactionManager") public AdminPermission saveAdminPermission(AdminPermission permission) { return adminPermissionDao.saveAdminPermission(permission); } @Override @Transactional("blTransactionManager") public AdminRole saveAdminRole(AdminRole role) { return adminRoleDao.saveAdminRole(role); } @Override @Transactional("blTransactionManager") public AdminUser saveAdminUser(AdminUser user) { boolean encodePasswordNeeded = false; String unencodedPassword = user.getUnencodedPassword(); if (user.getUnencodedPassword() != null) { encodePasswordNeeded = true; user.setPassword(unencodedPassword); } // If no password is set, default to a secure password. if (user.getPassword() == null) { user.setPassword(generateSecurePassword()); } AdminUser returnUser = adminUserDao.saveAdminUser(user); if (encodePasswordNeeded) { returnUser.setPassword(encodePassword(unencodedPassword, getSalt(returnUser, unencodedPassword))); } return adminUserDao.saveAdminUser(returnUser); } protected String generateSecurePassword() { return RandomStringUtils.randomAlphanumeric(16); } @Override @Transactional("blTransactionManager") public AdminUser changePassword(PasswordChange passwordChange) { AdminUser user = readAdminUserByUserName(passwordChange.getUsername()); user.setUnencodedPassword(passwordChange.getNewPassword()); user = saveAdminUser(user); Authentication auth = SecurityContextHolder.getContext().getAuthentication(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(passwordChange.getUsername(), passwordChange.getNewPassword(), auth.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authRequest); auth.setAuthenticated(false); return user; } @Override public boolean isUserQualifiedForOperationOnCeilingEntity(AdminUser adminUser, PermissionType permissionType, String ceilingEntityFullyQualifiedName) { boolean response = adminPermissionDao.isUserQualifiedForOperationOnCeilingEntity(adminUser, permissionType, ceilingEntityFullyQualifiedName); if (!response) { response = adminPermissionDao.isUserQualifiedForOperationOnCeilingEntityViaDefaultPermissions(ceilingEntityFullyQualifiedName); } return response; } @Override public boolean doesOperationExistForCeilingEntity(PermissionType permissionType, String ceilingEntityFullyQualifiedName) { return adminPermissionDao.doesOperationExistForCeilingEntity(permissionType, ceilingEntityFullyQualifiedName); } @Override public AdminUser readAdminUserByUserName(String userName) { return adminUserDao.readAdminUserByUserName(userName); } @Override public List<AdminUser> readAdminUsersByEmail(String email) { return adminUserDao.readAdminUserByEmail(email); } @Override public List<AdminUser> readAllAdminUsers() { return adminUserDao.readAllAdminUsers(); } @Override public List<AdminRole> readAllAdminRoles() { return adminRoleDao.readAllAdminRoles(); } @Override public List<AdminPermission> readAllAdminPermissions() { return adminPermissionDao.readAllAdminPermissions(); } @Override @Transactional("blTransactionManager") public GenericResponse sendForgotUsernameNotification(String emailAddress) { GenericResponse response = new GenericResponse(); List<AdminUser> users = null; if (emailAddress != null) { users = adminUserDao.readAdminUserByEmail(emailAddress); } if (CollectionUtils.isEmpty(users)) { response.addErrorCode("notFound"); } else { List<String> activeUsernames = new ArrayList<String>(); for (AdminUser user : users) { if (user.getActiveStatusFlag()) { activeUsernames.add(user.getLogin()); } } if (activeUsernames.size() > 0) { HashMap<String, Object> vars = new HashMap<String, Object>(); vars.put("accountNames", activeUsernames); emailService.sendTemplateEmail(emailAddress, getSendUsernameEmailInfo(), vars); } else { // send inactive username found email. response.addErrorCode("inactiveUser"); } } return response; } @Override @Transactional("blTransactionManager") public GenericResponse sendResetPasswordNotification(String username) { GenericResponse response = new GenericResponse(); AdminUser user = null; if (username != null) { user = adminUserDao.readAdminUserByUserName(username); } checkUser(user,response); if (! response.getHasErrors()) { String token = PasswordUtils.generateTemporaryPassword(PASSWORD_TOKEN_LENGTH); token = token.toLowerCase(); ForgotPasswordSecurityToken fpst = new ForgotPasswordSecurityTokenImpl(); fpst.setAdminUserId(user.getId()); fpst.setToken(encodePassword(token, null)); fpst.setCreateDate(SystemTime.asDate()); forgotPasswordSecurityTokenDao.saveToken(fpst); HashMap<String, Object> vars = new HashMap<String, Object>(); vars.put("token", token); String resetPasswordUrl = getResetPasswordURL(); if (!StringUtils.isEmpty(resetPasswordUrl)) { if (resetPasswordUrl.contains("?")) { resetPasswordUrl=resetPasswordUrl+"&token="+token; } else { resetPasswordUrl=resetPasswordUrl+"?token="+token; } } vars.put("resetPasswordUrl", resetPasswordUrl); emailService.sendTemplateEmail(user.getEmail(), getResetPasswordEmailInfo(), vars); } return response; } @Override @Transactional("blTransactionManager") public GenericResponse resetPasswordUsingToken(String username, String token, String password, String confirmPassword) { GenericResponse response = new GenericResponse(); AdminUser user = null; if (username != null) { user = adminUserDao.readAdminUserByUserName(username); } checkUser(user, response); checkPassword(password, confirmPassword, response); if (StringUtils.isBlank(token)) { response.addErrorCode("invalidToken"); } ForgotPasswordSecurityToken fpst = null; if (! response.getHasErrors()) { token = token.toLowerCase(); List<ForgotPasswordSecurityToken> fpstoks = forgotPasswordSecurityTokenDao.readUnusedTokensByAdminUserId(user.getId()); for (ForgotPasswordSecurityToken fpstok : fpstoks) { if (isPasswordValid(fpstok.getToken(), token, null)) { fpst = fpstok; break; } } if (fpst == null) { response.addErrorCode("invalidToken"); } else if (fpst.isTokenUsedFlag()) { response.addErrorCode("tokenUsed"); } else if (isTokenExpired(fpst)) { response.addErrorCode("tokenExpired"); } } if (! response.getHasErrors()) { if (! user.getId().equals(fpst.getAdminUserId())) { if (LOG.isWarnEnabled()) { LOG.warn("Password reset attempt tried with mismatched user and token " + user.getId() + ", " + token); } response.addErrorCode("invalidToken"); } } if (! response.getHasErrors()) { user.setUnencodedPassword(password); saveAdminUser(user); invalidateAllTokensForAdminUser(user); } return response; } protected void invalidateAllTokensForAdminUser(AdminUser user) { List<ForgotPasswordSecurityToken> tokens = forgotPasswordSecurityTokenDao.readUnusedTokensByAdminUserId(user.getId()); for (ForgotPasswordSecurityToken token : tokens) { token.setTokenUsedFlag(true); forgotPasswordSecurityTokenDao.saveToken(token); } } protected void checkUser(AdminUser user, GenericResponse response) { if (user == null) { response.addErrorCode("invalidUser"); } else if (StringUtils.isBlank(user.getEmail())) { response.addErrorCode("emailNotFound"); } else if (BooleanUtils.isNotTrue(user.getActiveStatusFlag())) { response.addErrorCode("inactiveUser"); } } protected void checkPassword(String password, String confirmPassword, GenericResponse response) { if (StringUtils.isBlank(password) || StringUtils.isBlank(confirmPassword)) { response.addErrorCode("invalidPassword"); } else if (! password.equals(confirmPassword)) { response.addErrorCode("passwordMismatch"); } } protected void checkExistingPassword(String unencodedPassword, AdminUser user, GenericResponse response) { if (!isPasswordValid(user.getPassword(), unencodedPassword, getSalt(user, unencodedPassword))) { response.addErrorCode("invalidPassword"); } } protected boolean isTokenExpired(ForgotPasswordSecurityToken fpst) { Date now = SystemTime.asDate(); long currentTimeInMillis = now.getTime(); long tokenSaveTimeInMillis = fpst.getCreateDate().getTime(); long minutesSinceSave = (currentTimeInMillis - tokenSaveTimeInMillis)/60000; return minutesSinceSave > getTokenExpiredMinutes(); } public static int getPASSWORD_TOKEN_LENGTH() { return PASSWORD_TOKEN_LENGTH; } public static void setPASSWORD_TOKEN_LENGTH(int PASSWORD_TOKEN_LENGTH) { AdminSecurityServiceImpl.PASSWORD_TOKEN_LENGTH = PASSWORD_TOKEN_LENGTH; } public EmailInfo getSendUsernameEmailInfo() { return sendUsernameEmailInfo; } public void setSendUsernameEmailInfo(EmailInfo sendUsernameEmailInfo) { this.sendUsernameEmailInfo = sendUsernameEmailInfo; } public EmailInfo getResetPasswordEmailInfo() { return resetPasswordEmailInfo; } public void setResetPasswordEmailInfo(EmailInfo resetPasswordEmailInfo) { this.resetPasswordEmailInfo = resetPasswordEmailInfo; } @Deprecated @Override public Object getSalt(AdminUser user, String unencodedPassword) { Object salt = null; if (saltSource != null) { salt = saltSource.getSalt(new AdminUserDetails(user.getId(), user.getLogin(), unencodedPassword, new ArrayList<GrantedAuthority>())); } return salt; } @Deprecated @Override public String getSalt() { return salt; } @Deprecated @Override public void setSalt(String salt) { this.salt = salt; } @Deprecated @Override public SaltSource getSaltSource() { return saltSource; } @Deprecated @Override public void setSaltSource(SaltSource saltSource) { this.saltSource = saltSource; } @Override @Transactional("blTransactionManager") public GenericResponse changePassword(String username, String oldPassword, String password, String confirmPassword) { GenericResponse response = new GenericResponse(); AdminUser user = null; if (username != null) { user = adminUserDao.readAdminUserByUserName(username); } checkUser(user, response); checkPassword(password, confirmPassword, response); if (!response.getHasErrors()) { checkExistingPassword(oldPassword, user, response); } if (!response.getHasErrors()) { user.setUnencodedPassword(password); saveAdminUser(user); } return response; } /** * Determines if a password is valid by comparing it to the encoded string, optionally using a salt. * <p> * The externally salted {@link org.springframework.security.authentication.encoding.PasswordEncoder PasswordEncoder} support is * being deprecated, following in Spring Security's footsteps, in order to move towards self salting hashing algorithms such as bcrypt. * Bcrypt is a superior hashing algorithm that randomly generates a salt per password in order to protect against rainbow table attacks * and is an intentionally expensive algorithm to further guard against brute force attempts to crack hashed passwords. * Additionally, having the encoding algorithm handle the salt internally reduces code complexity and dependencies such as {@link SaltSource}. * * @deprecated the new {@link PasswordEncoder} handles salting internally, this will be removed in 4.2 * * @param encodedPassword the encoded password * @param rawPassword the unencoded password * @param salt the optional salt * @return true if rawPassword matches the encodedPassword, false otherwise */ @Deprecated protected boolean isPasswordValid(String encodedPassword, String rawPassword, Object salt) { if (usingDeprecatedPasswordEncoder()) { return passwordEncoder.isPasswordValid(encodedPassword, rawPassword, salt); } else { return isPasswordValid(encodedPassword, rawPassword); } } /** * Determines if a password is valid by comparing it to the encoded string, salting is handled internally to the {@link PasswordEncoder}. * <p> * This method must always be called to verify if a password is valid after the original encoded password is generated * due to {@link PasswordEncoder} randomly generating salts internally and appending them to the resulting hash. * * @param encodedPassword the encoded password * @param rawPassword the raw password to check against the encoded password * @return true if rawPassword matches the encodedPassword, false otherwise */ protected boolean isPasswordValid(String encodedPassword, String rawPassword) { return passwordEncoderNew.matches(rawPassword, encodedPassword); } /** * Generate an encoded password from a raw password, optionally using a salt. * <p> * The externally salted {@link org.springframework.security.authentication.encoding.PasswordEncoder PasswordEncoder} support is * being deprecated, following in Spring Security's footsteps, in order to move towards self salting hashing algorithms such as bcrypt. * Bcrypt is a superior hashing algorithm that randomly generates a salt per password in order to protect against rainbow table attacks * and is an intentionally expensive algorithm to further guard against brute force attempts to crack hashed passwords. * Additionally, having the encoding algorithm handle the salt internally reduces code complexity and dependencies such as {@link SaltSource}. * * @deprecated the new {@link PasswordEncoder} handles salting internally, this will be removed in 4.2 * * @param rawPassword * @param salt * @return */ @Deprecated protected String encodePassword(String rawPassword, Object salt) { if (usingDeprecatedPasswordEncoder()) { return passwordEncoder.encodePassword(rawPassword, salt); } else { return encodePassword(rawPassword); } } /** * Generate an encoded password from a raw password, salting is handled internally to the {@link PasswordEncoder}. * <p> * This method can only be called once per password. The salt is randomly generated internally in the {@link PasswordEncoder} * and appended to the hash to provide the resulting encoded password. Once this has been called on a password, * going forward all checks for authenticity must be done by {@link #isPasswordValid(String, String)} as encoding the * same password twice will result in different encoded passwords. * * @param rawPassword the unencoded password to encode * @return the encoded password */ protected String encodePassword(String rawPassword) { return passwordEncoderNew.encode(rawPassword); } @Deprecated protected boolean usingDeprecatedPasswordEncoder() { return passwordEncoder != null; } }