/* * Copyright (c) 2005-2016 Vincent Vandenschrick. All rights reserved. * * This file is part of the Jspresso framework. * * Jspresso is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Jspresso 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Jspresso. If not, see <http://www.gnu.org/licenses/>. */ package org.jspresso.framework.application.backend.action.security; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Hex; import org.jspresso.framework.action.ActionBusinessException; import org.jspresso.framework.action.IActionHandler; import org.jspresso.framework.application.backend.action.BackendAction; import org.jspresso.framework.model.descriptor.IComponentDescriptor; import org.jspresso.framework.model.descriptor.IPropertyDescriptor; import org.jspresso.framework.model.descriptor.basic.BasicComponentDescriptor; import org.jspresso.framework.model.descriptor.basic.BasicPasswordPropertyDescriptor; import org.jspresso.framework.model.descriptor.basic.BasicStringPropertyDescriptor; import org.jspresso.framework.security.UserPrincipal; import org.jspresso.framework.util.lang.ObjectUtils; /** * This is the base class for implementing an action that performs actual * modification of a logged-in user password. This implementation delegates to * subclasses the actual change in the concrete JAAS store. This backend action * expects a Map<String,Object> in as action parameter in the context. * This map must contain : * <p> * <ul> * <li>{@code password_current} entry containing current password. Entry * key can be referred to as PASSWD_CURRENT static constant.</li> * <li>{@code password_typed} entry containing the new password. Entry key * can be referred to as PASSWD_TYPED static constant.</li> * <li>{@code password_retyped} entry containing the new password retyped. * Entry key can be referred to as PASSWD_RETYPED static constant.</li> * </ul> * For the action to succeed, {@code current_password} must match the * logged-in user current password and {@code password_typed} and * {@code password_retyped} mut match between each other. The only method to * be implemented by concrete subclasses is : * <p> * * <pre> * protected abstract boolean changePassword(UserPrincipal userPrincipal, * String currentPassword, String newPassword) * </pre> * * @author Vincent Vandenschrick */ public abstract class AbstractChangePasswordAction extends BackendAction { /** * {@code PASSWD_CHANGE_DESCRIPTOR} is a unique reference to the model * descriptor of the change password action. */ public static final IComponentDescriptor<Map<String, String>> PASSWD_CHANGE_DESCRIPTOR = createPasswordChangeModel(); /** * {@code TO_STRING}. */ public static final String TO_STRING = "to_string"; /** * {@code PASSWD_CURRENT}. */ public static final String PASSWD_CURRENT = "password_current"; /** * {@code PASSWD_RETYPED}. */ public static final String PASSWD_RETYPED = "password_retyped"; /** * {@code PASSWD_TYPED}. */ public static final String PASSWD_TYPED = "password_typed"; /** * {@code BASE64_ENCODING} is "BASE64". */ public static final String BASE64_ENCODING = "BASE64"; /** * {@code BASE16_ENCODING} is "HEX". */ public static final String BASE16_ENCODING = "BASE16"; /** * {@code HEX_ENCODING} is "HEX". */ public static final String HEX_ENCODING = "HEX"; private String digestAlgorithm; private String hashEncoding; private boolean allowEmptyPasswords = true; private boolean allowLoginPasswords = true; private String passwordRegex; private String passwordRegexSample; private static IComponentDescriptor<Map<String, String>> createPasswordChangeModel() { BasicComponentDescriptor<Map<String, String>> passwordChangeModel = new BasicComponentDescriptor<>(); BasicStringPropertyDescriptor toString = new BasicStringPropertyDescriptor(); toString.setName(TO_STRING); BasicPasswordPropertyDescriptor currentPassword = new BasicPasswordPropertyDescriptor(); currentPassword.setName(PASSWD_CURRENT); BasicPasswordPropertyDescriptor typedPassword = new BasicPasswordPropertyDescriptor(); typedPassword.setName(PASSWD_TYPED); BasicPasswordPropertyDescriptor retypedPassword = new BasicPasswordPropertyDescriptor(); retypedPassword.setName(PASSWD_RETYPED); List<IPropertyDescriptor> propertyDescriptors = new ArrayList<>(); propertyDescriptors.add(toString); propertyDescriptors.add(currentPassword); propertyDescriptors.add(typedPassword); propertyDescriptors.add(retypedPassword); passwordChangeModel.setPropertyDescriptors(propertyDescriptors); passwordChangeModel.setToStringProperty(TO_STRING); passwordChangeModel.setRenderedProperties(Arrays.asList(PASSWD_CURRENT, PASSWD_TYPED, PASSWD_RETYPED)); return passwordChangeModel; } /** * {@inheritDoc} */ @Override public boolean execute(IActionHandler actionHandler, Map<String, Object> context) { Map<String, Object> actionParam = getModelConnector(context).getConnectorValue(); String typedPasswd = (String) actionParam.get(PASSWD_TYPED); String retypedPasswd = (String) actionParam.get(PASSWD_RETYPED); if (!ObjectUtils.equals(typedPasswd, retypedPasswd)) { throw new ActionBusinessException("Typed and retyped passwords are different.", "password.typed.retyped.different"); } checkPasswordValidity(typedPasswd, context); UserPrincipal principal = getApplicationSession(context).getPrincipal(); if (changePassword(principal, (String) actionParam.get(PASSWD_CURRENT), typedPasswd)) { setActionParameter(getTranslationProvider(context).getTranslation("password.change.success", getLocale(context)), context); return super.execute(actionHandler, context); } return false; } /** * Gives the opportunity to check the new password validity against some * business rule. Buy default, it only checks that the password is not empty * if {@code allowEmptyPassword} is {@code false}. * * @param typedPasswd the password to check. * @param context the context */ protected void checkPasswordValidity(String typedPasswd, Map<String, Object> context) { if (!isAllowEmptyPasswords() && (typedPasswd == null || typedPasswd.length() == 0)) { throw new ActionBusinessException("Empty passwords are not allowed.", "password.empty.disallowed"); } if (!isAllowLoginPasswords() && ObjectUtils.equals(typedPasswd, getApplicationSession(context).getUsername())) { throw new ActionBusinessException("Passwords which are identical to username are not allowed.", "password.login.disallowed"); } if (getPasswordRegex() != null && !Pattern.matches(getPasswordRegex(), typedPasswd)) { throw new ActionBusinessException("Password does not match enforcing rules.", "password.regex.failed", getPasswordRegexSample()); } } /** * Sets the digestAlgorithm to use to hash the password before storing it (MD5 * for instance). * * @param digestAlgorithm * the digestAlgorithm to set. */ public void setDigestAlgorithm(String digestAlgorithm) { this.digestAlgorithm = digestAlgorithm; } /** * Sets the hashEncoding to encode the password hash before storing it. You * may choose between : * <ul> * <li>{@code BASE64} for base 64 encoding.</li> * <li>{@code HEX} for base 16 encoding.</li> * </ul> * Default encoding is {@code BASE64}. * * @param hashEncoding * the hashEncoding to set. */ public void setHashEncoding(String hashEncoding) { this.hashEncoding = hashEncoding; } /** * Performs the effective password change depending on the underlying storage. * * @param userPrincipal * the connected user principal. * @param currentPassword * the current password. * @param newPassword * the new password. * @return true if password was changed successfully. */ protected abstract boolean changePassword(UserPrincipal userPrincipal, String currentPassword, String newPassword); /** * Hashes a char array using the algorithm parametrised in the instance. * * @param newPassword * the new password to hash. * @return the password digest. * @throws NoSuchAlgorithmException * when the digest algorithm is not supported. * @throws IOException * whenever an I/O exception occurs. */ protected String digestAndEncode(char... newPassword) throws NoSuchAlgorithmException, IOException { if (getDigestAlgorithm() != null) { MessageDigest md = MessageDigest.getInstance(getDigestAlgorithm()); md.reset(); md.update(new String(newPassword).getBytes(StandardCharsets.UTF_8.name())); byte[] digest = md.digest(); return getPasswordStorePrefix() + encode(digest); } return new String(newPassword); } /** * Encodes the password hash based on the hash encoding parameter (either * Base64, Base16). Defaults to Base64. * * @param source * the byte array (hash) to encode. * @return the encoded string. */ protected String encode(byte[] source) { String he = getHashEncoding(); if (BASE64_ENCODING.equalsIgnoreCase(he)) { return Base64.encodeBase64String(source); } if (BASE16_ENCODING.equalsIgnoreCase(he) || HEX_ENCODING.equalsIgnoreCase(he)) { return Hex.encodeHexString(source); } // defaults to Base64 return Base64.encodeBase64String(source); } /** * Gets the digestAlgorithm. * * @return the digestAlgorithm. */ protected String getDigestAlgorithm() { return digestAlgorithm; } /** * Gets the hashEncoding. * * @return the hashEncoding. */ protected String getHashEncoding() { return hashEncoding; } /** * Returns a prefix to use before storing a password. An example usage is to * prefix the password hash with the type of hash, e.g. {MD5}. * * @return a prefix to use before storing a password. */ protected String getPasswordStorePrefix() { return ""; } /** * Gets the allowEmptyPasswords. * * @return the allowEmptyPasswords. */ protected boolean isAllowEmptyPasswords() { return allowEmptyPasswords; } /** * Configures the possibility to choose an empty password. * <p> * Default value is {@code true}, i.e. allow for empty passwords. * * @param allowEmptyPasswords * the allowEmptyPasswords to set. */ public void setAllowEmptyPasswords(boolean allowEmptyPasswords) { this.allowEmptyPasswords = allowEmptyPasswords; } /** * Is allow login passwords. * * @return the boolean */ protected boolean isAllowLoginPasswords() { return allowLoginPasswords; } /** * Configures the possibility to choose a password that equals the login. * <p> * Default value is {@code true}, i.e. allow for password equals login. * * @param allowLoginPasswords the allow login passwords */ public void setAllowLoginPasswords(boolean allowLoginPasswords) { this.allowLoginPasswords = allowLoginPasswords; } /** * Gets password regex. * * @return the password regex */ protected String getPasswordRegex() { return passwordRegex; } /** * Configures a regex that new passwords must match. * <p> * Default value is {@code null}, i.e. no regex is enforced. * * @param passwordRegex the password regex */ public void setPasswordRegex(String passwordRegex) { this.passwordRegex = passwordRegex; } /** * Gets password regex sample. * * @return the password regex sample */ protected String getPasswordRegexSample() { return passwordRegexSample; } /** * Configures an example of a valid password to explain the regex rules. * * @param passwordRegexSample the password regex sample */ public void setPasswordRegexSample(String passwordRegexSample) { this.passwordRegexSample = passwordRegexSample; } }