You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by an...@apache.org on 2015/07/17 14:18:09 UTC

svn commit: r1691532 - in /jackrabbit/oak/trunk: oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/ oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/ oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/ oak-cor...

Author: angela
Date: Fri Jul 17 12:18:09 2015
New Revision: 1691532

URL: http://svn.apache.org/r1691532
Log:
OAK-2445 : Password History Support (path provided by dominique jaeggi, thanks a lot)

Added:
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/PasswordHistory.java
    jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordHistoryTest.java
    jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user/history.md
    jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/UserImportHistoryTest.java
      - copied, changed from r1691382, jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/UserImportPwExpiryTest.java
Modified:
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserImpl.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/UserConstants.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/package-info.java
    jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImplTest.java
    jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/user/action/PasswordChangeActionTest.java
    jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user.md
    jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user/expiry.md

Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/PasswordHistory.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/PasswordHistory.java?rev=1691532&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/PasswordHistory.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/PasswordHistory.java Fri Jul 17 12:18:09 2015
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.jackrabbit.oak.security.user;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.Nonnull;
+import javax.jcr.AccessDeniedException;
+import javax.jcr.nodetype.ConstraintViolationException;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
+import org.apache.jackrabbit.oak.spi.security.user.UserConstants;
+import org.apache.jackrabbit.oak.spi.security.user.util.PasswordUtil;
+import org.apache.jackrabbit.oak.util.NodeUtil;
+import org.apache.jackrabbit.oak.util.TreeUtil;
+
+/**
+ * Helper class for the password history feature.
+ */
+final class PasswordHistory implements UserConstants {
+
+    private static final int HISTORY_MAX_SIZE = 1000;
+
+    private final int maxSize;
+    private final boolean isEnabled;
+
+    public PasswordHistory(@Nonnull ConfigurationParameters config) {
+        maxSize = Math.min(HISTORY_MAX_SIZE, config.getConfigValue(UserConstants.PARAM_PASSWORD_HISTORY_SIZE, UserConstants.PASSWORD_HISTORY_DISABLED_SIZE));
+        isEnabled = maxSize > UserConstants.PASSWORD_HISTORY_DISABLED_SIZE;
+    }
+
+    /**
+     * If password history is enabled this method validates the new password and
+     * updated the history; otherwise it returns {@code false}.
+     *
+     * @param userTree The user tree.
+     * @param password The new password to be validated.
+     * @return {@code true} if the history is enabled, the new password is not
+     * included in the history and the history was successfully updated;
+     * {@code false} otherwise.
+     * @throws javax.jcr.nodetype.ConstraintViolationException If the feature
+     * is enabled and the new password is found in the history.
+     * @throws javax.jcr.AccessDeniedException If the rep:pwd tree cannot be
+     * accessed.
+     */
+    boolean updatePasswordHistory(@Nonnull Tree userTree, @Nonnull String password) throws ConstraintViolationException, AccessDeniedException {
+        boolean updated = false;
+        if (isEnabled) {
+            checkPasswordInHistory(userTree, password);
+            shiftPasswordHistory(userTree);
+            updated = true;
+        }
+        return updated;
+    }
+
+    /**
+     * Update the history property with the current pw-hash stored in rep:password
+     * and trim the list of hashes in the list according to the configured maxSize.
+     *
+     * @param userTree The user tree.
+     * @throws AccessDeniedException If the editing session cannot access or
+     * create the rep:pwd node.
+     */
+    private void shiftPasswordHistory(@Nonnull Tree userTree) throws AccessDeniedException {
+        String currentPasswordHash = TreeUtil.getString(userTree, UserConstants.REP_PASSWORD);
+        if (currentPasswordHash != null) {
+            Tree passwordTree = getPasswordTree(userTree, true);
+            PropertyState historyProp = passwordTree.getProperty(UserConstants.REP_PWD_HISTORY);
+
+            // insert the current (old) password at the beginning of the password history
+            List<String> historyEntries = (historyProp == null) ? new ArrayList<String>() : Lists.newArrayList(historyProp.getValue(Type.STRINGS));
+            historyEntries.add(0, currentPasswordHash);
+
+            /* remove oldest history entries exceeding configured history max size (e.g. after
+             * a configuration change)
+             */
+            if (historyEntries.size() > maxSize) {
+                historyEntries = historyEntries.subList(0, maxSize);
+            }
+
+            passwordTree.setProperty(UserConstants.REP_PWD_HISTORY, historyEntries, Type.STRINGS);
+        }
+    }
+
+    /**
+     * Verify that the specified new password is not contained in the history.
+     *
+     * @param userTree The user tree.
+     * @param newPassword The new password
+     * @throws ConstraintViolationException If the passsword is found in the history
+     * @throws AccessDeniedException If the editing session cannot access the rep:pwd node.
+     */
+    private void checkPasswordInHistory(@Nonnull Tree userTree, @Nonnull String newPassword) throws ConstraintViolationException, AccessDeniedException {
+        if (PasswordUtil.isSame(TreeUtil.getString(userTree, UserConstants.REP_PASSWORD), newPassword)) {
+            throw new ConstraintViolationException("New password is identical to the current password.");
+        }
+        Tree pwTree = getPasswordTree(userTree, false);
+        if (pwTree.exists()) {
+            PropertyState pwHistoryProperty = pwTree.getProperty(UserConstants.REP_PWD_HISTORY);
+            if (pwHistoryProperty != null) {
+                for (String historyPwHash : Iterables.limit(pwHistoryProperty.getValue(Type.STRINGS), maxSize)) {
+                    if (PasswordUtil.isSame(historyPwHash, newPassword)) {
+                        throw new ConstraintViolationException("New password was found in password history.");
+                    }
+                }
+            }
+        }
+    }
+
+    @Nonnull
+    private static Tree getPasswordTree(@Nonnull Tree userTree, boolean doCreate) throws AccessDeniedException {
+        if (doCreate) {
+            return new NodeUtil(userTree).getOrAddChild(UserConstants.REP_PWD, UserConstants.NT_REP_PASSWORD).getTree();
+        } else {
+            return userTree.getChild(UserConstants.REP_PWD);
+        }
+    }
+}
\ No newline at end of file

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java?rev=1691532&r1=1691531&r2=1691532&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java Fri Jul 17 12:18:09 2015
@@ -105,7 +105,11 @@ import org.apache.jackrabbit.oak.spi.xml
         @Property(name = UserConstants.PARAM_PASSWORD_INITIAL_CHANGE,
                 label = "Change Password On First Login",
                 description = "When enabled, forces users to change their password upon first login.",
-                boolValue = UserConstants.DEFAULT_PASSWORD_INITIAL_CHANGE)
+                boolValue = UserConstants.DEFAULT_PASSWORD_INITIAL_CHANGE),
+        @Property(name = UserConstants.PARAM_PASSWORD_HISTORY_SIZE,
+                label = "Maximum Password History Size",
+                description = "Maximum number of passwords recorded for a user after changing her password (NOTE: upper limit is 1000). When changing the password the new password must not be present in the password history. A value of 0 indicates no password history is recorded.",
+                intValue = UserConstants.PASSWORD_HISTORY_DISABLED_SIZE)
 })
 public class UserConfigurationImpl extends ConfigurationBase implements UserConfiguration, SecurityConfiguration {
 

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserImpl.java?rev=1691532&r1=1691531&r2=1691532&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserImpl.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserImpl.java Fri Jul 17 12:18:09 2015
@@ -41,11 +41,13 @@ import static org.apache.jackrabbit.oak.
 class UserImpl extends AuthorizableImpl implements User {
 
     private final boolean isAdmin;
+    private final PasswordHistory pwHistory;
 
     UserImpl(String id, Tree tree, UserManagerImpl userManager) throws RepositoryException {
         super(id, tree, userManager);
 
         isAdmin = UserUtil.isAdmin(userManager.getConfig(), id);
+        pwHistory = new PasswordHistory(userManager.getConfig());
     }
 
     //---------------------------------------------------< AuthorizableImpl >---
@@ -107,6 +109,9 @@ class UserImpl extends AuthorizableImpl
         }
         UserManagerImpl userManager = getUserManager();
         userManager.onPasswordChange(this, password);
+
+        pwHistory.updatePasswordHistory(getTree(), password);
+
         userManager.setPassword(getTree(), getID(),  password, true);
     }
 

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/UserConstants.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/UserConstants.java?rev=1691532&r1=1691531&r2=1691532&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/UserConstants.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/UserConstants.java Fri Jul 17 12:18:09 2015
@@ -49,6 +49,7 @@ public interface UserConstants {
     String REP_MEMBERS_LIST = "rep:membersList";
     String REP_IMPERSONATORS = "rep:impersonators";
     String REP_PWD = "rep:pwd";
+    String REP_PWD_HISTORY = "rep:pwdHistory";
 
     Collection<String> GROUP_PROPERTY_NAMES = ImmutableSet.of(
             REP_PRINCIPAL_NAME,
@@ -228,4 +229,21 @@ public interface UserConstants {
      * This may be used change the password via the credentials object.
      */
     String CREDENTIALS_ATTRIBUTE_NEWPASSWORD = "user.newpassword";
+
+    /**
+     * Optional configuration parameter indicating the maximum number of passwords recorded for a user after
+     * password changes. If the value specified is > 0, password history checking during password change is implicitly
+     * enabled and the new password provided during a password change must not be found in the already recorded
+     * history.
+     *
+     * @since Oak 1.3.3
+     */
+    String PARAM_PASSWORD_HISTORY_SIZE = "passwordHistorySize";
+
+    /**
+     * Constant to indicate disabled password history, which is the default.
+     *
+     * @since Oak 1.3.3
+     */
+    int PASSWORD_HISTORY_DISABLED_SIZE = 0;
 }

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/package-info.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/package-info.java?rev=1691532&r1=1691531&r2=1691532&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/package-info.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/package-info.java Fri Jul 17 12:18:09 2015
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-@Version("2.0")
+@Version("2.1.0")
 @Export(optional = "provide:=true")
 package org.apache.jackrabbit.oak.spi.security.user;
 

Added: jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordHistoryTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordHistoryTest.java?rev=1691532&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordHistoryTest.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordHistoryTest.java Fri Jul 17 12:18:09 2015
@@ -0,0 +1,310 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.jackrabbit.oak.security.user;
+
+import java.lang.reflect.Field;
+import java.util.List;
+import javax.annotation.Nonnull;
+import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.ConstraintViolationException;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import org.apache.jackrabbit.api.security.user.User;
+import org.apache.jackrabbit.oak.AbstractSecurityTest;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
+import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration;
+import org.apache.jackrabbit.oak.spi.security.user.UserConstants;
+import org.apache.jackrabbit.oak.spi.security.user.util.PasswordUtil;
+import org.apache.jackrabbit.oak.util.NodeUtil;
+import org.apache.jackrabbit.oak.util.TreeUtil;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @see OAK-2445
+ */
+public class PasswordHistoryTest extends AbstractSecurityTest implements UserConstants {
+
+    private static final String[] PASSWORDS = {
+            "abc", "def", "ghi", "jkl", "mno", "pqr", "stu", "vwx", "yz0", "123", "456", "789"
+    };
+
+    private static final ConfigurationParameters CONFIG = ConfigurationParameters.of(PARAM_PASSWORD_HISTORY_SIZE, 10);
+
+    @Override
+    protected ConfigurationParameters getSecurityConfigParameters() {
+        return ConfigurationParameters.of(ImmutableMap.of(UserConfiguration.NAME, CONFIG));
+    }
+
+    @Nonnull
+    private List<String> getHistory(@Nonnull User user) throws RepositoryException {
+        return ImmutableList.copyOf(TreeUtil.getStrings(
+                root.getTree(user.getPath()).getChild(REP_PWD),
+                REP_PWD_HISTORY)).reverse();
+    }
+
+    /**
+     * Use reflection to access the private fields stored in the PasswordHistory
+     */
+    private static Integer getMaxSize(@Nonnull PasswordHistory history) throws Exception {
+        Field maxSize = history.getClass().getDeclaredField("maxSize");
+        maxSize.setAccessible(true);
+        return (Integer) maxSize.get(history);
+    }
+
+    /**
+     * Use reflection to access the private fields stored in the PasswordHistory
+     */
+    private static boolean isEnabled(@Nonnull PasswordHistory history) throws Exception {
+        Field isEnabled = history.getClass().getDeclaredField("isEnabled");
+        isEnabled.setAccessible(true);
+        return (Boolean) isEnabled.get(history);
+    }
+
+    @Test
+    public void testNoPwdTreeOnUserCreation() throws Exception {
+        User user = getTestUser();
+        assertFalse(root.getTree(user.getPath()).hasChild(REP_PWD));
+    }
+
+    @Test
+    public void testHistoryEmptyOnUserCreationWithPassword() throws Exception {
+        User user = getTestUser(); // the user is created with a password set
+
+        // the rep:pwd child must not exist. without the rep:pwd child no password history can exist.
+        assertFalse(root.getTree(user.getPath()).hasChild(REP_PWD));
+    }
+
+    @Test
+    public void testHistoryWithSinglePasswordChange() throws Exception {
+        // the user must be able to change the password
+        User user = getTestUser();
+        String oldPassword = TreeUtil.getString(root.getTree(user.getPath()), REP_PASSWORD);
+        user.changePassword("newPwd");
+        root.commit();
+
+        // after changing the password, 1 password history entry should be present and the
+        // recorded password should be equal to the user's initial password
+        // however, the user's current password must not match the old password.
+        assertTrue(root.getTree(user.getPath()).hasChild(REP_PWD));
+
+        Tree pwTree = root.getTree(user.getPath()).getChild(REP_PWD);
+        assertTrue(pwTree.hasProperty(REP_PWD_HISTORY));
+
+        List<String> history = getHistory(user);
+        assertEquals(1, history.size());
+        assertEquals(oldPassword, history.iterator().next());
+
+        String currentPw = TreeUtil.getString(root.getTree(user.getPath()), REP_PASSWORD);
+        assertNotSame(currentPw, oldPassword);
+    }
+
+    @Test
+    public void testHistoryMaxSize() throws Exception {
+        User user = getTestUser();
+
+        // we're changing the password 12 times, history max is 10
+        for (String pw : PASSWORDS) {
+            user.changePassword(pw);
+            root.commit();
+        }
+
+        assertEquals(10, getHistory(user).size());
+    }
+
+    @Test
+    public void testHistoryOrder() throws Exception {
+        User user = getTestUser();
+
+        // we're changing the password 12 times, history max is 10
+        for (String pw : PASSWORDS) {
+            user.changePassword(pw);
+        }
+
+        // we skip the first entry in the password list as it was shifted out
+        // due to max history size = 10.
+        int i = 1;
+        for (String pwHash : getHistory(user)) {
+            assertTrue(PasswordUtil.isSame(pwHash, PASSWORDS[i++]));
+        }
+    }
+
+    @Test
+    public void testRepeatedPwAfterHistorySizeReached() throws Exception {
+        User user = getTestUser();
+        for (String pw : PASSWORDS) {
+            user.changePassword(pw);
+        }
+
+        // changing pw back to the original value (as used for creation) must succeed
+        user.changePassword(user.getID());
+        // now, using all old passwords must also succeed as they get shifted out
+        for (String pw : PASSWORDS) {
+            user.changePassword(pw);
+        }
+    }
+
+    @Test(expected = ConstraintViolationException.class)
+    public void testHistoryViolationAtFirstChange() throws Exception {
+        User user = getTestUser();
+        user.changePassword(user.getID());
+    }
+
+    @Test(expected = ConstraintViolationException.class)
+    public void testHistoryViolation() throws Exception {
+        User user = getTestUser();
+        user.changePassword("abc");
+        user.changePassword("def");
+        user.changePassword("abc");
+    }
+
+    @Test
+    public void testNoHistoryUpdateOnViolation() throws Exception {
+        User user = getTestUser();
+        try {
+            user.changePassword("abc");
+            user.changePassword("def");
+            user.changePassword("abc");
+            fail("history violation not detected");
+        } catch (ConstraintViolationException e) {
+            String[] expected = new String[] {user.getID(), "abc"};
+            int i = 0;
+            for (String pwHash : getHistory(user)) {
+                assertTrue(PasswordUtil.isSame(pwHash, expected[i++]));
+            }
+        }
+    }
+
+    @Test
+    public void testEnabledPasswordHistory() throws Exception {
+        PasswordHistory history = new PasswordHistory(CONFIG);
+        assertTrue(isEnabled(history));
+        assertEquals(10, getMaxSize(history).longValue());
+    }
+
+    @Test
+    public void testHistoryUpperLimit() throws Exception {
+        PasswordHistory history = new PasswordHistory(ConfigurationParameters.of(PARAM_PASSWORD_HISTORY_SIZE, Integer.MAX_VALUE));
+
+        assertTrue(isEnabled(history));
+        assertEquals(1000, getMaxSize(history).longValue());
+    }
+
+    @Test
+    public void testDisabledPasswordHistory() throws Exception {
+        User user = getTestUser();
+        Tree userTree = root.getTree(user.getPath());
+
+        List<ConfigurationParameters> configs = ImmutableList.of(
+                ConfigurationParameters.EMPTY,
+                ConfigurationParameters.of(PARAM_PASSWORD_HISTORY_SIZE, PASSWORD_HISTORY_DISABLED_SIZE),
+                ConfigurationParameters.of(PARAM_PASSWORD_HISTORY_SIZE, -1),
+                ConfigurationParameters.of(PARAM_PASSWORD_HISTORY_SIZE, Integer.MIN_VALUE)
+        );
+
+        for (ConfigurationParameters config : configs) {
+            PasswordHistory disabledHistory = new PasswordHistory(config);
+
+            assertFalse(isEnabled(disabledHistory));
+            assertFalse(disabledHistory.updatePasswordHistory(userTree, user.getID()));
+        }
+    }
+
+    @Test(expected = ConstraintViolationException.class)
+    public void testCheckPasswordHistory() throws Exception {
+        Tree userTree = root.getTree(getTestUser().getPath());
+
+        PasswordHistory history = new PasswordHistory(CONFIG);
+        assertTrue(isEnabled(history));
+        assertEquals(10, getMaxSize(history).longValue());
+
+        history.updatePasswordHistory(userTree, getTestUser().getID());
+    }
+
+    @Test
+    public void testConfigurationChange() throws Exception {
+        User user = getTestUser();
+        Tree userTree = root.getTree(user.getPath());
+
+        PasswordHistory history = new PasswordHistory(CONFIG);
+        for (String pw : PASSWORDS) {
+            assertTrue(history.updatePasswordHistory(userTree, pw));
+        }
+        assertEquals(10, getHistory(user).size());
+
+        // change configuration to a smaller size
+        history = new PasswordHistory(ConfigurationParameters.of(PARAM_PASSWORD_HISTORY_SIZE, 5));
+        List<String> oldPwds = getHistory(user);
+        assertEquals(10, oldPwds.size());
+
+        // only the configured max-size number of entries in the history must be
+        // checked. additional entries in the history must be ignored
+        Iterables.skip(oldPwds, 6);
+        history.updatePasswordHistory(userTree, oldPwds.iterator().next());
+
+        // after chaning the pwd again however the rep:pwdHistory property must
+        // only contain the max-size number of passwords
+        assertEquals(5, getHistory(user).size());
+
+        history = new PasswordHistory(CONFIG);
+        history.updatePasswordHistory(userTree, "newPwd");
+        assertEquals(6, getHistory(user).size());
+    }
+
+    @Test
+    public void testEnableDisable() throws Exception {
+        User user = getTestUser();
+        Tree userTree = root.getTree(user.getPath());
+
+        PasswordHistory history = new PasswordHistory(CONFIG);
+        for (String pw : PASSWORDS) {
+            assertTrue(history.updatePasswordHistory(userTree, pw));
+        }
+        assertEquals(10, getHistory(user).size());
+
+        // disable the password history : changing the pw now must not
+        // modify the rep:pwdHistory property.
+        history = new PasswordHistory(ConfigurationParameters.EMPTY);
+        history.updatePasswordHistory(userTree, PASSWORDS[8]);
+
+        assertEquals(10, getHistory(user).size());
+    }
+
+    @Test
+    public void testSingleTypeHistoryProperty() throws Exception {
+        Tree userTree = root.getTree(getTestUser().getPath());
+        Tree pwdNode = new NodeUtil(userTree).getOrAddChild(REP_PWD, NT_REP_PASSWORD).getTree();
+
+        pwdNode.setProperty(REP_PWD_HISTORY, "singleValuedProperty");
+        assertFalse(pwdNode.getProperty(REP_PWD_HISTORY).isArray());
+        assertFalse(pwdNode.getProperty(REP_PWD_HISTORY).getType().isArray());
+
+        PasswordHistory history = new PasswordHistory(CONFIG);
+        assertTrue(history.updatePasswordHistory(userTree, "anyOtherPassword"));
+
+        assertTrue(pwdNode.getProperty(REP_PWD_HISTORY).isArray());
+        assertTrue(pwdNode.getProperty(REP_PWD_HISTORY).getType().isArray());
+    }
+}

Modified: jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImplTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImplTest.java?rev=1691532&r1=1691531&r2=1691532&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImplTest.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImplTest.java Fri Jul 17 12:18:09 2015
@@ -41,6 +41,7 @@ public class UserConfigurationImplTest e
     private static final boolean SUPPORT_AUTOSAVE = true;
     private static final Integer MAX_AGE = 10;
     private static final boolean INITIAL_PASSWORD_CHANGE = true;
+    private static final Integer PASSWORD_HISTORY_SIZE = 12;
 
     @Override
     protected ConfigurationParameters getSecurityConfigParameters() {
@@ -71,6 +72,7 @@ public class UserConfigurationImplTest e
         assertEquals(parameters.getConfigValue(UserConstants.PARAM_SUPPORT_AUTOSAVE, false), SUPPORT_AUTOSAVE);
         assertEquals(parameters.getConfigValue(UserConstants.PARAM_PASSWORD_MAX_AGE, UserConstants.DEFAULT_PASSWORD_MAX_AGE), MAX_AGE);
         assertEquals(parameters.getConfigValue(UserConstants.PARAM_PASSWORD_INITIAL_CHANGE, UserConstants.DEFAULT_PASSWORD_INITIAL_CHANGE), INITIAL_PASSWORD_CHANGE);
+        assertEquals(parameters.getConfigValue(UserConstants.PARAM_PASSWORD_HISTORY_SIZE, UserConstants.PASSWORD_HISTORY_DISABLED_SIZE), PASSWORD_HISTORY_SIZE);
     }
 
     private ConfigurationParameters getParams() {
@@ -85,6 +87,7 @@ public class UserConfigurationImplTest e
             put(UserConstants.PARAM_SUPPORT_AUTOSAVE, SUPPORT_AUTOSAVE);
             put(UserConstants.PARAM_PASSWORD_MAX_AGE, MAX_AGE);
             put(UserConstants.PARAM_PASSWORD_INITIAL_CHANGE, INITIAL_PASSWORD_CHANGE);
+            put(UserConstants.PARAM_PASSWORD_HISTORY_SIZE, PASSWORD_HISTORY_SIZE);
         }});
         return params;
     }

Modified: jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/user/action/PasswordChangeActionTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/user/action/PasswordChangeActionTest.java?rev=1691532&r1=1691531&r2=1691532&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/user/action/PasswordChangeActionTest.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/user/action/PasswordChangeActionTest.java Fri Jul 17 12:18:09 2015
@@ -25,8 +25,6 @@ import org.apache.jackrabbit.oak.spi.sec
 import org.junit.Before;
 import org.junit.Test;
 
-import static org.junit.Assert.fail;
-
 public class PasswordChangeActionTest extends AbstractSecurityTest {
 
     private PasswordChangeAction pwChangeAction;
@@ -38,26 +36,16 @@ public class PasswordChangeActionTest ex
         pwChangeAction.init(getSecurityProvider(), ConfigurationParameters.EMPTY);
     }
 
-    @Test
+    @Test(expected = ConstraintViolationException.class)
     public void testNullPassword() throws Exception {
-        try {
-            pwChangeAction.onPasswordChange(getTestUser(), null, root, getNamePathMapper());
-            fail("ConstraintViolationException expected.");
-        } catch (ConstraintViolationException e) {
-            // success
-        }
+        pwChangeAction.onPasswordChange(getTestUser(), null, root, getNamePathMapper());
     }
 
-    @Test
+    @Test(expected = ConstraintViolationException.class)
     public void testSamePassword() throws Exception {
-        try {
-            User user = getTestUser();
-            String pw = user.getID();
-            pwChangeAction.onPasswordChange(user, pw, root, getNamePathMapper());
-            fail("ConstraintViolationException expected.");
-        } catch (ConstraintViolationException e) {
-            // success
-        }
+        User user = getTestUser();
+        String pw = user.getID();
+        pwChangeAction.onPasswordChange(user, pw, root, getNamePathMapper());
     }
 
     @Test
@@ -69,7 +57,10 @@ public class PasswordChangeActionTest ex
     public void testUserWithoutPassword() throws Exception {
         String uid = "testUser" + UUID.randomUUID();
         User user = getUserManager(root).createUser(uid, null);
-
-        pwChangeAction.onPasswordChange(user, "changedPassword", root, getNamePathMapper());
+        try {
+            pwChangeAction.onPasswordChange(user, "changedPassword", root, getNamePathMapper());
+        } finally {
+            user.remove();
+        }
     }
 }
\ No newline at end of file

Modified: jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user.md
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user.md?rev=1691532&r1=1691531&r2=1691532&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user.md (original)
+++ jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user.md Fri Jul 17 12:18:09 2015
@@ -241,6 +241,17 @@ are
 See section [Password Expiry and Force Initial Password Change](user/expiry.html)
 for details.
 
+#### Password History
+
+Since Oak 1.3.3 the default user management implementation provides password
+history support.
+
+By default this feature is disabled. The corresponding configuration option is
+
+- `PARAM_PASSWORD_HISTORY_SIZE`: number of changed passwords to remember.
+
+See section [Password History](user/history.html) for details.
+
 #### Utilities
 
 `org.apache.jackrabbit.oak.spi.security.user.*`
@@ -283,6 +294,7 @@ as of OAK 1.0:
 | `PARAM_IMPORT_BEHAVIOR`             | String ("abort", "ignore", "besteffort") | "ignore"    |
 | `PARAM_PASSWORD_MAX_AGE`            | int     | 0                                            |
 | `PARAM_PASSWORD_INITIAL_CHANGE`     | boolean | false                                        |
+| `PARAM_PASSWORD_HISTORY_SIZE`       | int (upper limit: 1000) | 0                            |
 | | | |
 
 The following configuration parameters present with the default implementation in Jackrabbit 2.x are no longer supported and will be ignored:
@@ -317,6 +329,7 @@ implementation on various levels:
 - [Authorizable Node Name](user/authorizablenodename.html)
 - [Searching Users and Groups](user/query.html)
 - [Password Expiry and Force Initial Password Change](user/expiry.html)
+- [Password History](user/history.html)
 
 <!-- hidden references -->
 [everyone]: /oak/docs/apidocs/org/apache/jackrabbit/oak/spi/security/principal/EveryonePrincipal.html#NAME

Modified: jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user/expiry.md
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user/expiry.md?rev=1691532&r1=1691531&r2=1691532&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user/expiry.md (original)
+++ jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user/expiry.md Fri Jul 17 12:18:09 2015
@@ -74,10 +74,10 @@ been introduced, leaving open future enh
         - * (UNDEFINED) protected
         - * (UNDEFINED) protected multiple
 
-##### Node rep:passwords and Property rep:passwordLastModified
+##### Node rep:pwd and Property rep:passwordLastModified
 
     [rep:User]  > rep:Authorizable, rep:Impersonatable
-        + rep:pw (rep:Password) = rep:Password protected
+        + rep:pwd (rep:Password) = rep:Password protected
         ...
         
 The _rep:pw_ node and the _rep:passwordLastModified_ property are defined

Added: jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user/history.md
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user/history.md?rev=1691532&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user/history.md (added)
+++ jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user/history.md Fri Jul 17 12:18:09 2015
@@ -0,0 +1,85 @@
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You 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.
+-->
+
+Password History
+--------------------------------------------------------------------------------
+
+### General
+
+Oak provides functionality to remember a configurable number of
+passwords after password changes and to prevent a password to
+be set during changing a user's password if found in said history.
+
+### Configuration
+
+An administrator may enable password history via the
+_org.apache.jackrabbit.oak.security.user.UserConfigurationImpl_
+OSGi configuration. By default the history is disabled.
+
+The following configuration option is supported:
+
+- Maximum Password History Size (_passwordHistorySize_, number of passwords): When greater 0 enables password
+  history and sets feature to remember the specified number of passwords for a user.
+
+Note, that the current implementation has a limit of at most 1000 passwords
+remembered in the history.
+
+### How it works
+
+#### Recording of Passwords
+
+If the feature is enabled, during a user changing her password, the old password
+hash is recorded in the password history.
+
+The old password hash is only recorded if a password was set (non-empty).
+Therefore setting a password for a user for the first time (i.e. during creation
+or if the user doesn't have a password set before) does not result in a history
+record, as there is no old password.
+
+The old password hash is copied to the password history *after* the provided new
+password has been validated but *before* the new password hash is written to the
+user's _rep:password_ property.
+
+The history operates as a FIFO list. A new password history record exceeding the
+configured max history size, results in the oldest recorded password from being
+removed from the history.
+
+Also, if the configuration parameter for the history size is changed to a non-zero
+but smaller value than before, upon the next password change the oldest records
+exceeding the new history size are removed.
+
+History password hashes are recorded in a multi-value property _rep:pwdHistory_ on
+the user's _rep:pwd_ node.
+        
+The _rep:pwdHistory_ property is defined protected in order to guard against the 
+user modifying (overcoming) her password history limitations.
+
+
+#### Evaluation of Password History
+
+Upon a user changing her password and if the password history feature is enabled
+(configured password history size > 0), implementation checks if the current
+password or  any of the password hashes recorded in the history matches the new
+password.
+
+If any record is a match, a _ConstraintViolationException_ is thrown and the
+user's password is *NOT* changed.
+
+#### Oak JCR XML Import
+
+When users are imported via the Oak JCR XML importer, password history is imported
+irrespective on whether the password history feature is enabled or not.

Copied: jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/UserImportHistoryTest.java (from r1691382, jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/UserImportPwExpiryTest.java)
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/UserImportHistoryTest.java?p2=jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/UserImportHistoryTest.java&p1=jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/UserImportPwExpiryTest.java&r1=1691382&r2=1691532&rev=1691532&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/UserImportPwExpiryTest.java (original)
+++ jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/UserImportHistoryTest.java Fri Jul 17 12:18:09 2015
@@ -16,16 +16,9 @@
  */
 package org.apache.jackrabbit.oak.jcr.security.user;
 
-import java.util.HashMap;
-import javax.annotation.CheckForNull;
 import javax.jcr.Node;
-import javax.jcr.Property;
-import javax.jcr.Session;
 
 import org.apache.jackrabbit.api.security.user.Authorizable;
-import org.apache.jackrabbit.api.security.user.User;
-import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
-import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration;
 import org.apache.jackrabbit.oak.spi.security.user.UserConstants;
 import org.junit.Test;
 
@@ -34,12 +27,10 @@ import static org.junit.Assert.assertTru
 
 /**
  * Testing user import with default {@link org.apache.jackrabbit.oak.spi.xml.ImportBehavior}
- * and pw-expiry content
- *
- * @see <a href="https://issues.apache.org/jira/browse/OAK-1922">OAK-1922</a>
- * @see <a href="https://issues.apache.org/jira/browse/OAK-1943">OAK-1943</a>
+ * and pw-history content: test that the history is imported irrespective of the
+ * configuration.
  */
-public class UserImportPwExpiryTest extends AbstractImportTest {
+public class UserImportHistoryTest extends AbstractImportTest {
 
     @Override
     protected String getTargetPath() {
@@ -51,91 +42,11 @@ public class UserImportPwExpiryTest exte
         return null;
     }
 
-    @CheckForNull
-    protected ConfigurationParameters getConfigurationParameters() {
-        HashMap<String, Object> userParams = new HashMap<String, Object>() {{
-            put(UserConstants.PARAM_PASSWORD_MAX_AGE, 10);
-        }};
-        return ConfigurationParameters.of(UserConfiguration.NAME, ConfigurationParameters.of(userParams));
-    }
-
-    /**
-     * @since Oak 1.1
-     */
-    @Test
-    public void testImportUserCreatesPasswordLastModified() throws Exception {
-        // import user
-        String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
-                "<sv:node sv:name=\"x\" xmlns:mix=\"http://www.jcp.org/jcr/mix/1.0\" xmlns:nt=\"http://www.jcp.org/jcr/nt/1.0\" xmlns:fn_old=\"http://www.w3.org/2004/10/xpath-functions\" xmlns:fn=\"http://www.w3.org/2005/xpath-functions\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:sv=\"http://www.jcp.org/jcr/sv/1.0\" xmlns:rep=\"internal\" xmlns:jcr=\"http://www.jcp.org/jcr/1.0\">" +
-                "   <sv:property sv:name=\"jcr:primaryType\" sv:type=\"Name\">" +
-                "      <sv:value>rep:User</sv:value>" +
-                "   </sv:property>" +
-                "   <sv:property sv:name=\"jcr:uuid\" sv:type=\"String\">" +
-                "      <sv:value>9dd4e461-268c-3034-b5c8-564e155c67a6</sv:value>" +
-                "   </sv:property>" +
-                "   <sv:property sv:name=\"rep:password\" sv:type=\"String\">" +
-                "      <sv:value>pw</sv:value>" +
-                "   </sv:property>" +
-                "   <sv:property sv:name=\"rep:principalName\" sv:type=\"String\">" +
-                "      <sv:value>xPrincipal</sv:value>" +
-                "   </sv:property>" +
-                "   <sv:node sv:name=\"" + UserConstants.REP_PWD + "\">" +
-                "      <sv:property sv:name=\"jcr:primaryType\" sv:type=\"Name\">" +
-                "         <sv:value>"+ UserConstants.NT_REP_PASSWORD +"</sv:value>" +
-                "      </sv:property>" +
-                "   </sv:node>" +
-                "</sv:node>";
-
-        doImport(USERPATH, xml);
-
-        Authorizable authorizable = getUserManager().getAuthorizable("x");
-        Node userNode = getImportSession().getNode(authorizable.getPath());
-        assertTrue(userNode.hasNode(UserConstants.REP_PWD));
-        Node pwdNode = userNode.getNode(UserConstants.REP_PWD);
-        assertTrue(pwdNode.getDefinition().isProtected());
-        assertTrue(pwdNode.hasProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED));
-        assertTrue(pwdNode.getProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED).getDefinition().isProtected());
-    }
-
-    /**
-     * @since Oak 1.1
-     */
-    @Test
-    public void testImportUserCreatesPasswordLastModified2() throws Exception {
-        // import user without rep:pwd child node defined
-        String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
-                "<sv:node sv:name=\"x\" xmlns:mix=\"http://www.jcp.org/jcr/mix/1.0\" xmlns:nt=\"http://www.jcp.org/jcr/nt/1.0\" xmlns:fn_old=\"http://www.w3.org/2004/10/xpath-functions\" xmlns:fn=\"http://www.w3.org/2005/xpath-functions\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:sv=\"http://www.jcp.org/jcr/sv/1.0\" xmlns:rep=\"internal\" xmlns:jcr=\"http://www.jcp.org/jcr/1.0\">" +
-                "   <sv:property sv:name=\"jcr:primaryType\" sv:type=\"Name\">" +
-                "      <sv:value>rep:User</sv:value>" +
-                "   </sv:property>" +
-                "   <sv:property sv:name=\"jcr:uuid\" sv:type=\"String\">" +
-                "      <sv:value>9dd4e461-268c-3034-b5c8-564e155c67a6</sv:value>" +
-                "   </sv:property>" +
-                "   <sv:property sv:name=\"rep:password\" sv:type=\"String\">" +
-                "      <sv:value>pw</sv:value>" +
-                "   </sv:property>" +
-                "   <sv:property sv:name=\"rep:principalName\" sv:type=\"String\">" +
-                "      <sv:value>xPrincipal</sv:value>" +
-                "   </sv:property>" +
-                "</sv:node>";
-
-        doImport(USERPATH, xml);
-
-        // verify that the pwd node has still been created
-        Authorizable authorizable = getUserManager().getAuthorizable("x");
-        Node userNode = getImportSession().getNode(authorizable.getPath());
-        assertTrue(userNode.hasNode(UserConstants.REP_PWD));
-        Node pwdNode = userNode.getNode(UserConstants.REP_PWD);
-        assertTrue(pwdNode.getDefinition().isProtected());
-        assertTrue(pwdNode.hasProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED));
-        assertTrue(pwdNode.getProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED).getDefinition().isProtected());
-    }
-
     /**
-     * @since Oak 1.1
+     * @since Oak 1.3.3
      */
     @Test
-    public void testImportUserWithPwdProperties() throws Exception {
+    public void testImportUserWithPwdHistory() throws Exception {
         // import user
         String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
                 "<sv:node sv:name=\"y\" xmlns:mix=\"http://www.jcp.org/jcr/mix/1.0\" xmlns:nt=\"http://www.jcp.org/jcr/nt/1.0\" xmlns:fn_old=\"http://www.w3.org/2004/10/xpath-functions\" xmlns:fn=\"http://www.w3.org/2005/xpath-functions\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:sv=\"http://www.jcp.org/jcr/sv/1.0\" xmlns:rep=\"internal\" xmlns:jcr=\"http://www.jcp.org/jcr/1.0\">" +
@@ -155,11 +66,8 @@ public class UserImportPwExpiryTest exte
                 "      <sv:property sv:name=\"jcr:primaryType\" sv:type=\"Name\">" +
                 "         <sv:value>" + UserConstants.NT_REP_PASSWORD + "</sv:value>" +
                 "      </sv:property>" +
-                "      <sv:property sv:name=\"" + UserConstants.REP_PASSWORD_LAST_MODIFIED + "\" sv:type=\"Long\">" +
-                "         <sv:value>1404036716000</sv:value>" +
-                "      </sv:property>" +
-                "      <sv:property sv:name=\"customProp\" sv:type=\"String\">" +
-                "         <sv:value>abc</sv:value>" +
+                "      <sv:property sv:name=\"" + UserConstants.REP_PWD_HISTORY + "\" sv:type=\"String\" sv:multiple=\"true\">" +
+                "         <sv:value>{sha1}8efd86fb78a56a5145ed7739dcb00c78581c5375</sv:value>" +
                 "      </sv:property>" +
                 "   </sv:node>" +
                 "</sv:node>";
@@ -171,55 +79,7 @@ public class UserImportPwExpiryTest exte
         assertTrue(userNode.hasNode(UserConstants.REP_PWD));
 
         Node pwdNode = userNode.getNode(UserConstants.REP_PWD);
-        assertTrue(pwdNode.hasProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED));
-        assertEquals(1404036716000L, pwdNode.getProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED).getLong());
-
-        assertTrue(pwdNode.hasProperty("customProp"));
-        Property custom = pwdNode.getProperty("customProp");
-        assertTrue(custom.getDefinition().isProtected());
-        assertEquals("abc", custom.getString());
-    }
-
-    /**
-     * @since Oak 1.1
-     */
-    @Test
-    public void testImportExistingUserWithoutExpiryProperty() throws Exception {
-
-        String uid = "existing";
-        User user = getUserManager().createUser(uid, uid);
-
-        Session s = getImportSession();
-        // change password to force existence of password last modified property
-        user.changePassword(uid);
-        s.save();
-
-        Node userNode = s.getNode(user.getPath());
-        assertTrue(userNode.hasNode(UserConstants.REP_PWD));
-        Node pwdNode = userNode.getNode(UserConstants.REP_PWD);
-        assertTrue(pwdNode.hasProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED));
-
-        // overwrite user via import
-        String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
-                "<sv:node sv:name=\"" + uid + "\" xmlns:mix=\"http://www.jcp.org/jcr/mix/1.0\" xmlns:nt=\"http://www.jcp.org/jcr/nt/1.0\" xmlns:fn_old=\"http://www.w3.org/2004/10/xpath-functions\" xmlns:fn=\"http://www.w3.org/2005/xpath-functions\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:sv=\"http://www.jcp.org/jcr/sv/1.0\" xmlns:rep=\"internal\" xmlns:jcr=\"http://www.jcp.org/jcr/1.0\">" +
-                "   <sv:property sv:name=\"jcr:primaryType\" sv:type=\"Name\">" +
-                "      <sv:value>rep:User</sv:value>" +
-                "   </sv:property>" +
-                "   <sv:property sv:name=\"rep:password\" sv:type=\"String\">" +
-                "      <sv:value>" + uid + "</sv:value>" +
-                "   </sv:property>" +
-                "   <sv:property sv:name=\"rep:principalName\" sv:type=\"String\">" +
-                "      <sv:value>" + uid + "Principal</sv:value>" +
-                "   </sv:property>" +
-                "</sv:node>";
-
-        doImport(USERPATH, xml);
-
-        Authorizable authorizable = getUserManager().getAuthorizable(uid);
-        userNode = s.getNode(authorizable.getPath());
-        assertTrue(userNode.hasNode(UserConstants.REP_PWD));
-
-        pwdNode = userNode.getNode(UserConstants.REP_PWD);
-        assertTrue(pwdNode.hasProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED));
+        assertTrue(pwdNode.hasProperty(UserConstants.REP_PWD_HISTORY));
+        assertEquals("{sha1}8efd86fb78a56a5145ed7739dcb00c78581c5375", pwdNode.getProperty(UserConstants.REP_PWD_HISTORY).getString());
     }
 }