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 dj...@apache.org on 2015/10/07 15:44:08 UTC

svn commit: r1707304 - in /jackrabbit/oak/trunk: oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/ oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/ oak-doc/src/site/markdown/security/user/

Author: dj
Date: Wed Oct  7 13:44:08 2015
New Revision: 1707304

URL: http://svn.apache.org/viewvc?rev=1707304&view=rev
Log:
OAK-3463: Communicate Password Change Failure Reason During Expiry + Pw History

Added:
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/PasswordHistoryException.java   (with props)
    jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordExpiryHistoryTest.java
      - copied, changed from r1707063, jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordExpiryTest.java
Modified:
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/PasswordHistory.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserAuthentication.java
    jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user/expiry.md

Modified: 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=1707304&r1=1707303&r2=1707304&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/PasswordHistory.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/PasswordHistory.java Wed Oct  7 13:44:08 2015
@@ -111,7 +111,7 @@ final class PasswordHistory implements U
      */
     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.");
+            throw new PasswordHistoryException("New password is identical to the current password.");
         }
         Tree pwTree = getPasswordTree(userTree, false);
         if (pwTree.exists()) {
@@ -119,7 +119,7 @@ final class PasswordHistory implements U
             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.");
+                        throw new PasswordHistoryException("New password was found in password history.");
                     }
                 }
             }

Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/PasswordHistoryException.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/PasswordHistoryException.java?rev=1707304&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/PasswordHistoryException.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/PasswordHistoryException.java Wed Oct  7 13:44:08 2015
@@ -0,0 +1,26 @@
+/*
+ * 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 javax.jcr.nodetype.ConstraintViolationException;
+
+class PasswordHistoryException extends ConstraintViolationException {
+
+    PasswordHistoryException(String message) {
+        super(message);
+    }
+}

Propchange: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/PasswordHistoryException.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserAuthentication.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserAuthentication.java?rev=1707304&r1=1707303&r2=1707304&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserAuthentication.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserAuthentication.java Wed Oct  7 13:44:08 2015
@@ -168,6 +168,9 @@ class UserAuthentication implements Auth
                             + newPasswordObject.getClass().getName());
                 }
             }
+        } catch (PasswordHistoryException e) {
+            credentials.setAttribute(e.getClass().getName(), e.getMessage());
+            log.error("Failed to change password for user " + userId, e.getMessage());
         } catch (RepositoryException e) {
             log.error("Failed to change password for user " + userId, e.getMessage());
         } catch (CommitFailedException e) {

Copied: jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordExpiryHistoryTest.java (from r1707063, jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordExpiryTest.java)
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordExpiryHistoryTest.java?p2=jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordExpiryHistoryTest.java&p1=jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordExpiryTest.java&r1=1707063&r2=1707304&rev=1707304&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordExpiryTest.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordExpiryHistoryTest.java Wed Oct  7 13:44:08 2015
@@ -16,36 +16,34 @@
  */
 package org.apache.jackrabbit.oak.security.user;
 
-import java.util.UUID;
-import javax.jcr.SimpleCredentials;
-import javax.security.auth.login.CredentialExpiredException;
-import javax.security.auth.login.LoginException;
-
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import org.apache.jackrabbit.api.security.user.User;
 import org.apache.jackrabbit.oak.AbstractSecurityTest;
-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.plugins.nodetype.NodeTypeConstants;
-import org.apache.jackrabbit.oak.plugins.nodetype.ReadOnlyNodeTypeManager;
 import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
+import org.apache.jackrabbit.oak.spi.security.SecurityProvider;
 import org.apache.jackrabbit.oak.spi.security.authentication.Authentication;
 import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration;
 import org.apache.jackrabbit.oak.spi.security.user.UserConstants;
-import org.apache.jackrabbit.oak.util.TreeUtil;
+import org.apache.jackrabbit.oak.spi.security.user.action.AuthorizableAction;
+import org.apache.jackrabbit.oak.spi.security.user.action.AuthorizableActionProvider;
+import org.apache.jackrabbit.oak.spi.security.user.action.PasswordValidationAction;
 import org.junit.Before;
 import org.junit.Test;
 
+import javax.annotation.Nonnull;
+import javax.jcr.SimpleCredentials;
+import javax.security.auth.login.CredentialExpiredException;
+import java.util.List;
+
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.fail;
 
 /**
- * @see <a href="https://issues.apache.org/jira/browse/OAK-1922">OAK-1922</a>
+ * @see <a href="https://issues.apache.org/jira/browse/OAK-3463">OAK-3463</a>
  */
-public class PasswordExpiryTest extends AbstractSecurityTest {
+public class PasswordExpiryHistoryTest extends AbstractSecurityTest {
 
     private String userId;
 
@@ -57,101 +55,112 @@ public class PasswordExpiryTest extends
 
     @Override
     protected ConfigurationParameters getSecurityConfigParameters() {
-        ConfigurationParameters userConfig = ConfigurationParameters.of(UserConstants.PARAM_PASSWORD_MAX_AGE, 10);
-        return ConfigurationParameters.of(UserConfiguration.NAME, userConfig);
-    }
-
-    @Test
-    public void testCreateUser() throws Exception {
-        String newUserId = "newuser" + UUID.randomUUID();
-        User user = null;
-
-        try {
-            user = getUserManager(root).createUser(newUserId, newUserId);
-            root.commit();
-
-            Tree pwdTree = root.getTree(user.getPath()).getChild(UserConstants.REP_PWD);
-            assertTrue(pwdTree.exists());
-            assertTrue(TreeUtil.isNodeType(pwdTree, UserConstants.NT_REP_PASSWORD, root.getTree(NodeTypeConstants.NODE_TYPES_PATH)));
-
-            ReadOnlyNodeTypeManager ntMgr = ReadOnlyNodeTypeManager.getInstance(root, getNamePathMapper());
-            assertTrue(ntMgr.getDefinition(pwdTree.getParent(), pwdTree).isProtected());
-
-            PropertyState property = pwdTree.getProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED);
-            assertNotNull(property);
-            assertEquals(Type.LONG, property.getType());
-            assertTrue(property.getValue(Type.LONG, 0) > 0);
-
-            // protected properties must not be exposed by User#hasProperty
-            assertFalse(user.hasProperty(UserConstants.REP_PWD + "/" + UserConstants.REP_PASSWORD_LAST_MODIFIED));
-        } finally {
-            if (user != null) {
-                user.remove();
-                root.commit();
+        final PasswordValidationAction pwAction = new PasswordValidationAction();
+        pwAction.init(null, ConfigurationParameters.of(
+                PasswordValidationAction.CONSTRAINT, "^.*(?=.{4,}).*"
+        ));
+        final AuthorizableActionProvider actionProvider = new AuthorizableActionProvider() {
+            @Nonnull
+            @Override
+            public List<? extends AuthorizableAction> getAuthorizableActions(@Nonnull SecurityProvider securityProvider) {
+                return ImmutableList.of(pwAction);
             }
-        }
-    }
-
-    @Test
-    public void testChangePassword() throws Exception {
-        User user = getTestUser();
-        PropertyState p1 = root.getTree(user.getPath()).getChild(UserConstants.REP_PWD).getProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED);
-        long oldModTime = p1.getValue(Type.LONG, 0);
-        assertTrue(oldModTime > 0);
-        waitForSystemTimeIncrement(oldModTime);
-        user.changePassword(userId);
-        root.commit();
-        PropertyState p2 = root.getTree(user.getPath()).getChild(UserConstants.REP_PWD).getProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED);
-        long newModTime = p2.getValue(Type.LONG, 0);
-        assertTrue(newModTime > oldModTime);
-    }
+        };
 
-    @Test
-    public void testAuthenticatePasswordExpiredNewUser() throws Exception {
-        Authentication a = new UserAuthentication(getUserConfiguration(), root, userId);
-        // during user creation pw last modified is set, thus it shouldn't expire
-        a.authenticate(new SimpleCredentials(userId, userId.toCharArray()));
+        ConfigurationParameters userConfig = ConfigurationParameters.of(
+                ImmutableMap.of(
+                        UserConstants.PARAM_AUTHORIZABLE_ACTION_PROVIDER, actionProvider,
+                        UserConstants.PARAM_PASSWORD_MAX_AGE, 10,
+                        UserConstants.PARAM_PASSWORD_HISTORY_SIZE, 10
+                )
+        );
+        return ConfigurationParameters.of(UserConfiguration.NAME, userConfig);
     }
 
     @Test
-    public void testAuthenticatePasswordExpired() throws Exception {
+    public void testAuthenticatePasswordExpiredAndSame() throws Exception {
+        User user = getTestUser();
         Authentication a = new UserAuthentication(getUserConfiguration(), root, userId);
         // set password last modified to beginning of epoch
-        root.getTree(getTestUser().getPath()).getChild(UserConstants.REP_PWD).setProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED, 0);
+        root.getTree(user.getPath()).getChild(UserConstants.REP_PWD).setProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED, 0);
         root.commit();
         try {
             a.authenticate(new SimpleCredentials(userId, userId.toCharArray()));
             fail("Credentials should be expired");
         } catch (CredentialExpiredException e) {
-            // success
+            // success, credentials are expired
+
+            // try to change password to the same one, this should fail due pw history
+            SimpleCredentials pwChangeCreds = new SimpleCredentials(userId, userId.toCharArray());
+            try {
+                pwChangeCreds.setAttribute(UserConstants.CREDENTIALS_ATTRIBUTE_NEWPASSWORD, user.getID());
+                a.authenticate(pwChangeCreds);
+                fail("User password changed in spite of enabled pw history");
+            } catch (CredentialExpiredException c) {
+                // success, pw found in history
+                Object attr = pwChangeCreds.getAttribute(PasswordHistoryException.class.getName());
+                assertEquals(
+                        "credentials should contain pw change failure reason",
+                        "New password is identical to the current password.",
+                        attr);
+            }
         }
     }
 
     @Test
-    public void testAuthenticateBeforePasswordExpired() throws Exception {
+    public void testAuthenticatePasswordExpiredAndInHistory() throws Exception {
+        User user = getTestUser();
+        user.changePassword("pw12345678");
         Authentication a = new UserAuthentication(getUserConfiguration(), root, userId);
         // set password last modified to beginning of epoch
-        root.getTree(getTestUser().getPath()).getChild(UserConstants.REP_PWD).setProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED, 0);
+        root.getTree(user.getPath()).getChild(UserConstants.REP_PWD).setProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED, 0);
         root.commit();
         try {
-            a.authenticate(new SimpleCredentials(userId, "wrong".toCharArray()));
+            a.authenticate(new SimpleCredentials(userId, "pw12345678".toCharArray()));
+            fail("Credentials should be expired");
         } catch (CredentialExpiredException e) {
-            fail("Login should fail before expiry");
-        } catch (LoginException e) {
-            // success - userId/pw mismatch takes precedence over expiry
+            // success, credentials are expired
+
+            // try to change password to the same one, this should fail due pw history
+            SimpleCredentials pwChangeCreds = new SimpleCredentials(userId, "pw12345678".toCharArray());
+            try {
+                pwChangeCreds.setAttribute(UserConstants.CREDENTIALS_ATTRIBUTE_NEWPASSWORD, user.getID());
+                a.authenticate(pwChangeCreds);
+                fail("User password changed in spite of enabled pw history");
+            } catch (CredentialExpiredException c) {
+                // success, pw found in history
+                Object attr = pwChangeCreds.getAttribute(PasswordHistoryException.class.getName());
+                assertEquals(
+                        "credentials should contain pw change failure reason",
+                        "New password was found in password history.",
+                        attr);
+            }
         }
     }
 
     @Test
-    public void testAuthenticatePasswordExpiredChangePassword() throws Exception {
+    public void testAuthenticatePasswordExpiredAndValidationFailure() throws Exception {
+        User user = getTestUser();
         Authentication a = new UserAuthentication(getUserConfiguration(), root, userId);
         // set password last modified to beginning of epoch
-        root.getTree(getTestUser().getPath()).getChild(UserConstants.REP_PWD).setProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED, 0);
+        root.getTree(user.getPath()).getChild(UserConstants.REP_PWD).setProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED, 0);
         root.commit();
+        try {
+            a.authenticate(new SimpleCredentials(userId, userId.toCharArray()));
+            fail("Credentials should be expired");
+        } catch (CredentialExpiredException e) {
+            // success, credentials are expired
 
-        // changing the password should reset the pw last mod and the pw no longer be expired
-        getTestUser().changePassword(userId);
-        root.commit();
-        assertTrue(a.authenticate(new SimpleCredentials(userId, userId.toCharArray())));
+            // try to change password to the same one, this should fail due pw history
+            SimpleCredentials pwChangeCreds = new SimpleCredentials(userId, userId.toCharArray());
+            try {
+                pwChangeCreds.setAttribute(UserConstants.CREDENTIALS_ATTRIBUTE_NEWPASSWORD, "2");
+                a.authenticate(pwChangeCreds);
+                fail("User password changed in spite of expected validation failure");
+            } catch (CredentialExpiredException c) {
+                // success, pw found in history
+                assertNull(pwChangeCreds.getAttribute(PasswordHistoryException.class.getName()));
+            }
+        }
     }
 }

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=1707304&r1=1707303&r2=1707304&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 Wed Oct  7 13:44:08 2015
@@ -159,3 +159,15 @@ This way the user can change the passwor
 This method of changing password via the normal login call only works if a
 user's password is in fact expired and cannot be used for regular password
 changes (attribute is ignored, use _User#changePassword_ directly instead).
+
+Should the [Password History feature](history.html) be enabled, and - for the
+above password change - a password already in the history be used, the change
+will fail and the login still throw a _CredentialExpiredException_. In order
+for consumers of the exception to become aware that the credentials are
+still considered expired, and that the password was not changed due to the 
+new password having been found in the password history, the credentials object
+is fitted with an additional attribute with name _PasswordHistoryException_.
+This attribute may contain the following two values:
+
+_"New password was found in password history."_ or 
+_""New password is identical to the current password."_