Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserAuthentication.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserAuthentication.java (revision 1628650) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserAuthentication.java (revision ) @@ -36,6 +36,7 @@ import org.apache.jackrabbit.api.security.user.User; import org.apache.jackrabbit.api.security.user.UserManager; import org.apache.jackrabbit.oak.api.AuthInfo; +import org.apache.jackrabbit.oak.api.CommitFailedException; import org.apache.jackrabbit.oak.api.PropertyState; import org.apache.jackrabbit.oak.api.Root; import org.apache.jackrabbit.oak.api.Tree; @@ -119,8 +120,12 @@ checkSuccess(success, "UserId/Password mismatch."); if (isPasswordExpired(user)) { + // change the password if the credentials object has the UserConstants.CREDENTIALS_ATTRIBUTE_NEWPASSWORD + // attribute set + if (!changePassword(user, creds)) { - throw new CredentialExpiredException("User password has expired"); - } + throw new CredentialExpiredException("User password has expired"); + } + } } else if (credentials instanceof ImpersonationCredentials) { ImpersonationCredentials ipCreds = (ImpersonationCredentials) credentials; AuthInfo info = ipCreds.getImpersonatorInfo(); @@ -133,6 +138,7 @@ } catch (RepositoryException e) { throw new LoginException(e.getMessage()); } + return success; } @@ -141,6 +147,27 @@ if (!success) { throw new FailedLoginException(msg); } + } + + private boolean changePassword(User user, SimpleCredentials credentials) throws RepositoryException { + Object newPasswordObject = credentials.getAttribute(CREDENTIALS_ATTRIBUTE_NEWPASSWORD); + if (null != newPasswordObject) { + if (newPasswordObject instanceof String) { + user.changePassword((String) newPasswordObject); + try { + root.commit(); + log.debug("User " + user.getID() + ": changed user password"); + return true; + } catch (CommitFailedException e) { + log.error("Failed to change password for user [" + user.getID() + "]" + e.getMessage()); + } + + } else { + log.warn("Aborted password change for user [" + user.getID() + "]: provided new password is of" + + " incompatible type " + newPasswordObject.getClass().getName()); + } + } + return false; } private boolean equalUserId(ImpersonationCredentials creds) { \ No newline at end of file Index: oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/ResetExpiredPasswordTest.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/ResetExpiredPasswordTest.java (revision ) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/ResetExpiredPasswordTest.java (revision ) @@ -0,0 +1,135 @@ +/* + * 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 com.google.common.collect.ImmutableMap; +import org.apache.jackrabbit.oak.AbstractSecurityTest; +import org.apache.jackrabbit.oak.api.Root; +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.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.spi.security.user.util.PasswordUtil; +import org.junit.Before; +import org.junit.Test; + +import javax.jcr.SimpleCredentials; +import javax.security.auth.login.CredentialExpiredException; +import javax.security.auth.login.LoginException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @see OAK-2156 + */ +public class ResetExpiredPasswordTest extends AbstractSecurityTest { + + private String userId; + + @Before + public void before() throws Exception { + super.before(); + userId = getTestUser().getID(); + } + + @Override + protected ConfigurationParameters getSecurityConfigParameters() { + ConfigurationParameters userConfig = ConfigurationParameters.of(UserConstants.PARAM_PASSWORD_MAX_AGE, 10); + return ConfigurationParameters.of(ImmutableMap.of(UserConfiguration.NAME, userConfig)); + } + + @Test + public void testPasswordChangePersisted() throws Exception { + 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.commit(); + SimpleCredentials creds = new SimpleCredentials(userId, userId.toCharArray()); + creds.setAttribute(UserConstants.CREDENTIALS_ATTRIBUTE_NEWPASSWORD, "newPw"); + + try { + a.authenticate(creds); + } catch (CredentialExpiredException e) { + // ignore, see next test + } + + // check that the password has been persisted and has the value of the new password + Root rootBasedOnSeparateSession = login(getAdminCredentials()).getLatestRoot(); + Tree userTree = rootBasedOnSeparateSession.getTree(getTestUser().getPath()); + assertTrue(PasswordUtil.isSame(userTree.getProperty(UserConstants.REP_PASSWORD).getValue(Type.STRING), "newPw")); + } + + @Test + public void testAuthenticatePasswordExpiredThenChanged() throws Exception { + 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.commit(); + try { + SimpleCredentials creds = new SimpleCredentials(userId, userId.toCharArray()); + creds.setAttribute(UserConstants.CREDENTIALS_ATTRIBUTE_NEWPASSWORD, userId); + a.authenticate(creds); + // success + } catch (CredentialExpiredException e) { + fail("Credentials should not be expired"); + } + } + + @Test + public void testChangeWithWrongPw() throws Exception { + 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.commit(); + try { + SimpleCredentials creds = new SimpleCredentials(userId, "wrongPw".toCharArray()); + creds.setAttribute(UserConstants.CREDENTIALS_ATTRIBUTE_NEWPASSWORD, "newPw"); + a.authenticate(creds); + fail("Authentication with wrong expired password should fail and should not reset pw."); + } catch (LoginException e) { + // success + } finally { + Tree userTree = root.getTree(getTestUser().getPath()); + assertTrue(PasswordUtil.isSame(userTree.getProperty(UserConstants.REP_PASSWORD).getValue(Type.STRING), userId)); + assertEquals(0, userTree.getChild(UserConstants.REP_PWD).getProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED).getValue(Type.LONG).longValue()); + } + } + + @Test + public void testChangeWithNonStringAttribute() throws Exception { + 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.commit(); + try { + SimpleCredentials creds = new SimpleCredentials(userId, userId.toCharArray()); + creds.setAttribute(UserConstants.CREDENTIALS_ATTRIBUTE_NEWPASSWORD, new Long("1")); + a.authenticate(creds); + fail("Authentication with non-string attribute should fail."); + } catch (CredentialExpiredException e) { + // success + } finally { + Tree userTree = root.getTree(getTestUser().getPath()); + assertTrue(PasswordUtil.isSame(userTree.getProperty(UserConstants.REP_PASSWORD).getValue(Type.STRING), userId)); + assertEquals(0, userTree.getChild(UserConstants.REP_PWD).getProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED).getValue(Type.LONG).longValue()); + } + } +} Index: oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/UserConstants.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/UserConstants.java (revision 1628650) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/UserConstants.java (revision ) @@ -222,4 +222,10 @@ * Default value for {@link #PARAM_PASSWORD_INITIAL_CHANGE} */ boolean DEFAULT_PASSWORD_INITIAL_CHANGE = false; + + /** + * Name of the {@link javax.jcr.SimpleCredentials} attribute containing the new password. + * This may be used change the password via the credentials object. + */ + String CREDENTIALS_ATTRIBUTE_NEWPASSWORD = "user.newpassword"; }