Index: oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImplTest.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/UserConfigurationImplTest.java (revision ) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImplTest.java (revision ) @@ -0,0 +1,92 @@ +/* + * 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.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.spi.xml.ImportBehavior; +import org.apache.jackrabbit.oak.spi.xml.ProtectedItemImporter; +import org.junit.Test; + +import java.util.HashMap; + +import static org.junit.Assert.assertEquals; + +public class UserConfigurationImplTest extends AbstractSecurityTest { + + private static final String USER_PATH = "/this/is/a/user/test"; + private static final String GROUP_PATH = "/this/is/a/group/test"; + private static final Integer DEFAULT_DEPTH = 10; + private static final String IMPORT_BEHAVIOR = ImportBehavior.NAME_BESTEFFORT; + private static final String HASH_ALGORITHM = "MD5"; + private static final Integer HASH_ITERATIONS = 500; + private static final Integer SALT_SIZE = 6; + private static final boolean SUPPORT_AUTOSAVE = true; + private static final Integer MAX_AGE = 10; + private static final boolean INITIAL_PASSWORD_CHANGE = true; + + @Override + protected ConfigurationParameters getSecurityConfigParameters() { + return ConfigurationParameters.of(ImmutableMap.of(UserConfiguration.NAME, getParams())); + } + + @Test + public void testUserConfigurationWithConstructor() throws Exception { + UserConfigurationImpl userConfiguration = new UserConfigurationImpl(getSecurityProvider()); + testConfigurationParameters(userConfiguration.getParameters()); + } + + @Test + public void testUserConfigurationWithSetParameters() throws Exception { + UserConfigurationImpl userConfiguration = new UserConfigurationImpl(); + userConfiguration.setParameters(getParams()); + testConfigurationParameters(userConfiguration.getParameters()); + } + + private void testConfigurationParameters(ConfigurationParameters parameters) throws Exception { + assertEquals(parameters.getConfigValue(UserConstants.PARAM_USER_PATH, UserConstants.DEFAULT_USER_PATH), USER_PATH); + assertEquals(parameters.getConfigValue(UserConstants.PARAM_GROUP_PATH, UserConstants.DEFAULT_GROUP_PATH), GROUP_PATH); + assertEquals(parameters.getConfigValue(UserConstants.PARAM_DEFAULT_DEPTH, UserConstants.DEFAULT_DEPTH), DEFAULT_DEPTH); + assertEquals(parameters.getConfigValue(ProtectedItemImporter.PARAM_IMPORT_BEHAVIOR, ImportBehavior.NAME_IGNORE), IMPORT_BEHAVIOR); + assertEquals(parameters.getConfigValue(UserConstants.PARAM_PASSWORD_HASH_ALGORITHM, PasswordUtil.DEFAULT_ALGORITHM), HASH_ALGORITHM); + assertEquals(parameters.getConfigValue(UserConstants.PARAM_PASSWORD_HASH_ITERATIONS, PasswordUtil.DEFAULT_ITERATIONS), HASH_ITERATIONS); + assertEquals(parameters.getConfigValue(UserConstants.PARAM_PASSWORD_SALT_SIZE, PasswordUtil.DEFAULT_SALT_SIZE), SALT_SIZE); + 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); + } + + private ConfigurationParameters getParams() { + ConfigurationParameters params = ConfigurationParameters.of(new HashMap() {{ + put(UserConstants.PARAM_USER_PATH, USER_PATH); + put(UserConstants.PARAM_GROUP_PATH, GROUP_PATH); + put(UserConstants.PARAM_DEFAULT_DEPTH, DEFAULT_DEPTH); + put(ProtectedItemImporter.PARAM_IMPORT_BEHAVIOR, IMPORT_BEHAVIOR); + put(UserConstants.PARAM_PASSWORD_HASH_ALGORITHM, HASH_ALGORITHM); + put(UserConstants.PARAM_PASSWORD_HASH_ITERATIONS, HASH_ITERATIONS); + put(UserConstants.PARAM_PASSWORD_SALT_SIZE, SALT_SIZE); + put(UserConstants.PARAM_SUPPORT_AUTOSAVE, SUPPORT_AUTOSAVE); + put(UserConstants.PARAM_PASSWORD_MAX_AGE, MAX_AGE); + put(UserConstants.PARAM_PASSWORD_INITIAL_CHANGE, INITIAL_PASSWORD_CHANGE); + }}); + return params; + } +} Index: oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/UserImportPwExpiryTest.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/UserImportPwExpiryTest.java (revision ) +++ oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/UserImportPwExpiryTest.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.jcr.security.user; + +import javax.annotation.CheckForNull; +import javax.jcr.Node; + +import com.google.common.collect.ImmutableMap; +import org.apache.jackrabbit.api.security.user.Authorizable; +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.Ignore; +import org.junit.Test; + +import java.util.HashMap; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Testing user import with default {@link org.apache.jackrabbit.oak.spi.xml.ImportBehavior} + * and pw-expiry content + */ +public class UserImportPwExpiryTest extends AbstractImportTest { + + @Override + protected String getTargetPath() { + return USERPATH; + } + + @Override + protected String getImportBehavior() { + return null; + } + + @CheckForNull + protected ConfigurationParameters getConfigurationParameters() { + HashMap userParams = new HashMap() {{ + put(UserConstants.PARAM_PASSWORD_MAX_AGE, Long.valueOf(10)); + }}; + return ConfigurationParameters.of(ImmutableMap.of(UserConfiguration.NAME, ConfigurationParameters.of(userParams))); + } + + /** + * @since Oak 1.1 + */ + @Test + @Ignore("refer to OAK-???") + public void testImportUser() throws Exception { + // import user + String xml = "\n" + + "" + + " " + + " rep:User" + + " " + + " " + + " 9dd4e461-268c-3034-b5c8-564e155c67a6" + + " " + + " " + + " pw" + + " " + + " " + + " xPrincipal" + + " " + + " " + + " " + + " rep:Password" + + " " + + " " + + ""; + + doImport(USERPATH, xml); + + Authorizable authorizable = userMgr.getAuthorizable("x"); + Node userNode = adminSession.getNode(authorizable.getPath()); + assertTrue(userNode.hasNode(UserConstants.REP_PWD)); + assertTrue(userNode.getNode(UserConstants.REP_PWD).hasProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED)); + } + + /** + * @since Oak 1.1 + */ + @Test + @Ignore("refer to OAK-???") + public void testImportUserWithCustomPwdProperties() throws Exception { + // import user + String xml = "\n" + + "" + + " " + + " rep:User" + + " " + + " " + + " 41529076-9594-360e-ae48-5922904f345d" + + " " + + " " + + " pw" + + " " + + " " + + " yPrincipal" + + " " + + " " + + " " + + " rep:Password" + + " " + + " " + + " 1404036716000" + + " " + + " " + + ""; + + doImport(USERPATH, xml); + + Authorizable authorizable = userMgr.getAuthorizable("y"); + Node userNode = adminSession.getNode(authorizable.getPath()); + assertTrue(userNode.hasNode(UserConstants.REP_PWD)); + assertTrue(userNode.getNode(UserConstants.REP_PWD).hasProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED)); + assertEquals(1404036716000L, userNode.getProperty(UserConstants.REP_PWD + "/" + UserConstants.REP_PASSWORD_LAST_MODIFIED).getLong()); + } +} Index: oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/user/UserAuthenticationTest.java =================================================================== --- oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/user/UserAuthenticationTest.java (revision 1605678) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserAuthenticationTest.java (revision ) @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.oak.security.authentication.user; +package org.apache.jackrabbit.oak.security.user; import java.security.Principal; import java.util.ArrayList; @@ -51,18 +51,12 @@ public void before() throws Exception { super.before(); userId = getTestUser().getID(); - authentication = new UserAuthentication(userId, getUserManager(root)); + authentication = new UserAuthentication(getUserConfiguration(), root, userId); } @Test - public void testAuthenticateWithoutUserManager() throws Exception { - UserAuthentication authentication = new UserAuthentication(userId, null); - assertFalse(authentication.authenticate(new SimpleCredentials(userId, userId.toCharArray()))); - } - - @Test public void testAuthenticateWithoutUserId() throws Exception { - UserAuthentication authentication = new UserAuthentication(null, getUserManager(root)); + authentication = new UserAuthentication(getUserConfiguration(), root, null); assertFalse(authentication.authenticate(new SimpleCredentials(userId, userId.toCharArray()))); } @@ -80,7 +74,7 @@ @Test public void testAuthenticateCannotResolveUser() throws Exception { SimpleCredentials sc = new SimpleCredentials("unknownUser", "pw".toCharArray()); - Authentication a = new UserAuthentication(sc.getUserID(), getUserManager(root)); + Authentication a = new UserAuthentication(getUserConfiguration(), root, sc.getUserID()); assertFalse(a.authenticate(sc)); } @@ -89,7 +83,7 @@ public void testAuthenticateResolvesToGroup() throws Exception { Group g = getUserManager(root).createGroup("g1"); SimpleCredentials sc = new SimpleCredentials(g.getID(), "pw".toCharArray()); - Authentication a = new UserAuthentication(sc.getUserID(), getUserManager(root)); + Authentication a = new UserAuthentication(getUserConfiguration(), root, sc.getUserID()); try { a.authenticate(sc); \ No newline at end of file Index: oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordForceInitialPasswordChangeTest.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/PasswordForceInitialPasswordChangeTest.java (revision ) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordForceInitialPasswordChangeTest.java (revision ) @@ -0,0 +1,91 @@ +/* + * 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.api.security.user.User; +import org.apache.jackrabbit.oak.AbstractSecurityTest; +import org.apache.jackrabbit.oak.api.PropertyState; +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.junit.Before; +import org.junit.Test; + +import javax.jcr.SimpleCredentials; +import javax.security.auth.login.CredentialExpiredException; +import java.util.UUID; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class PasswordForceInitialPasswordChangeTest 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_INITIAL_CHANGE, true); + return ConfigurationParameters.of(ImmutableMap.of(UserConfiguration.NAME, userConfig)); + } + + @Test + public void testCreateUser() throws Exception { + String newUserId = "newuser" + UUID.randomUUID(); + User user = getUserManager(root).createUser(newUserId, newUserId); + root.commit(); + PropertyState property = root.getTree(user.getPath()).getChild(UserConstants.REP_PWD).getProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED); + assertNull(property); + } + + @Test + public void testAuthenticateMustChangePassword() throws Exception { + Authentication a = new UserAuthentication(getUserConfiguration(), root, userId); + try { + a.authenticate(new SimpleCredentials(userId, userId.toCharArray())); + fail("Credentials should be expired"); + } catch (CredentialExpiredException e) { + // success + } + } + + @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); + assertNull(p1); + user.changePassword(userId); + root.commit(); + PropertyState p2 = root.getTree(user.getPath()).getChild(UserConstants.REP_PWD).getProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED); + assertNotNull(p2); + assertTrue(p2.getValue(Type.LONG) > 0); + + // after password change, authentication must succeed + Authentication a = new UserAuthentication(getUserConfiguration(), root, userId); + a.authenticate(new SimpleCredentials(userId, userId.toCharArray())); + } +} Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserImporter.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/UserImporter.java (revision 1605678) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserImporter.java (revision ) @@ -283,7 +283,7 @@ } String pw = propInfo.getTextValue().getString(); - userManager.setPassword(parent, pw, false); + userManager.setPassword(parent, a.getID(), pw, false); /* Execute authorizable actions for a NEW user at this point after \ No newline at end of file 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 1605678) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/UserConstants.java (revision ) @@ -30,6 +30,7 @@ String NT_REP_AUTHORIZABLE = "rep:Authorizable"; String NT_REP_AUTHORIZABLE_FOLDER = "rep:AuthorizableFolder"; String NT_REP_USER = "rep:User"; + String NT_REP_PASSWORD = "rep:Password"; String NT_REP_GROUP = "rep:Group"; @Deprecated String NT_REP_MEMBERS = "rep:Members"; @@ -41,10 +42,12 @@ String REP_PRINCIPAL_NAME = "rep:principalName"; String REP_AUTHORIZABLE_ID = "rep:authorizableId"; String REP_PASSWORD = "rep:password"; + String REP_PASSWORD_LAST_MODIFIED = "rep:passwordLastModified"; String REP_DISABLED = "rep:disabled"; String REP_MEMBERS = "rep:members"; String REP_MEMBERS_LIST = "rep:membersList"; String REP_IMPERSONATORS = "rep:impersonators"; + String REP_PWD = "rep:pwd"; Collection GROUP_PROPERTY_NAMES = ImmutableSet.of( REP_PRINCIPAL_NAME, @@ -90,6 +93,11 @@ String DEFAULT_ANONYMOUS_ID = "anonymous"; /** + * Mandatory configuration option denoting the user {@link org.apache.jackrabbit.oak.spi.security.authentication.Authentication} implementation to use in the login module. + */ + String PARAM_USER_AUTHENTICATION_FACTORY = "userAuthenticationFactory"; + + /** * Configuration option to define the path underneath which user nodes * are being created. */ @@ -172,4 +180,26 @@ * be reviewed and adjusted accordingly.

*/ String PARAM_SUPPORT_AUTOSAVE = "supportAutoSave"; + + /** + * Optional configuration parameter indicating the maximum age in days a password may have + * before it expires. If the value specified is > 0, password expiry is implicitly enabled. + */ + String PARAM_PASSWORD_MAX_AGE = "passwordMaxAge"; + + /** + * Default value for {@link #PARAM_PASSWORD_MAX_AGE} + */ + int DEFAULT_PASSWORD_MAX_AGE = 0; + + /** + * Optional configuration parameter indicating whether users must change their passwords + * on first login. If enabled, passwords are immediately expired upon user creation. + */ + String PARAM_PASSWORD_INITIAL_CHANGE = "initialPasswordChange"; + + /** + * Default value for {@link #PARAM_PASSWORD_INITIAL_CHANGE} + */ + boolean DEFAULT_PASSWORD_INITIAL_CHANGE = false; } Index: oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordExpiryAndForceInitialChangeTest.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/PasswordExpiryAndForceInitialChangeTest.java (revision ) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordExpiryAndForceInitialChangeTest.java (revision ) @@ -0,0 +1,93 @@ +/* + * 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.api.security.user.User; +import org.apache.jackrabbit.oak.AbstractSecurityTest; +import org.apache.jackrabbit.oak.api.PropertyState; +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.junit.Before; +import org.junit.Test; + +import javax.jcr.SimpleCredentials; +import javax.security.auth.login.CredentialExpiredException; +import java.util.HashMap; +import java.util.UUID; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class PasswordExpiryAndForceInitialChangeTest extends AbstractSecurityTest { + + private String userId; + + @Before + public void before() throws Exception { + super.before(); + userId = getTestUser().getID(); + } + + @Override + protected ConfigurationParameters getSecurityConfigParameters() { + ConfigurationParameters parameters = ConfigurationParameters.of(new HashMap() {{ + put(UserConstants.PARAM_PASSWORD_MAX_AGE, 10); + put(UserConstants.PARAM_PASSWORD_INITIAL_CHANGE, true); + }}); + return ConfigurationParameters.of(ImmutableMap.of(UserConfiguration.NAME, parameters)); + } + + @Test + public void testCreateUser() throws Exception { + String newUserId = "newuser" + UUID.randomUUID(); + User user = getUserManager(root).createUser(newUserId, newUserId); + root.commit(); + PropertyState property = root.getTree(user.getPath()).getChild(UserConstants.REP_PWD).getProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED); + assertNull(property); + } + + @Test + public void testAuthenticateMustChangePassword() throws Exception { + Authentication a = new UserAuthentication(getUserConfiguration(), root, userId); + try { + // the user should need to change the password on first login + a.authenticate(new SimpleCredentials(userId, userId.toCharArray())); + fail("Credentials should be expired"); + } catch (CredentialExpiredException e) { + // success + } + } + + @Test + public void testChangePassword() throws Exception { + // once the user changes the password, the login should succeed + User user = getTestUser(); + user.changePassword(userId); + root.commit(); + PropertyState p = root.getTree(user.getPath()).getChild(UserConstants.REP_PWD).getProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED); + long newModTime = p.getValue(Type.LONG, 0); + assertTrue(newModTime > 0); + 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())); + } +} Index: oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserManagerImplTest.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/UserManagerImplTest.java (revision 1605678) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserManagerImplTest.java (revision ) @@ -100,14 +100,14 @@ Tree userTree = root.getTree(user.getPath()); for (String pw : pwds) { - userMgr.setPassword(userTree, pw, true); + userMgr.setPassword(userTree, testUserId, pw, true); String pwHash = userTree.getProperty(UserConstants.REP_PASSWORD).getValue(Type.STRING); assertNotNull(pwHash); assertTrue(PasswordUtil.isSame(pwHash, pw)); } for (String pw : pwds) { - userMgr.setPassword(userTree, pw, false); + userMgr.setPassword(userTree, testUserId, pw, false); String pwHash = userTree.getProperty(UserConstants.REP_PASSWORD).getValue(Type.STRING); assertNotNull(pwHash); if (!pw.startsWith("{")) { @@ -126,19 +126,21 @@ Tree userTree = root.getTree(user.getPath()); try { - userMgr.setPassword(userTree, null, true); + userMgr.setPassword(userTree, testUserId, null, true); fail("setting null password should fail"); } catch (NullPointerException e) { // expected } try { - userMgr.setPassword(userTree, null, false); + userMgr.setPassword(userTree, testUserId, null, false); fail("setting null password should fail"); } catch (NullPointerException e) { // expected } } + + // TODO: add test-cases for setPassword with expiry and force-initial-pw-change @Test public void testGetPasswordHash() throws Exception { \ No newline at end of file Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.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/UserConfigurationImpl.java (revision 1605678) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java (revision ) @@ -42,6 +42,7 @@ import org.apache.jackrabbit.oak.spi.security.Context; import org.apache.jackrabbit.oak.spi.security.SecurityConfiguration; import org.apache.jackrabbit.oak.spi.security.SecurityProvider; +import org.apache.jackrabbit.oak.spi.security.user.UserAuthenticationFactory; 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; @@ -90,10 +91,20 @@ @Property(name = UserConstants.PARAM_SUPPORT_AUTOSAVE, label = "Autosave Support", description = "Configuration option to enable autosave behavior. Note: this config option is present for backwards compatibility with Jackrabbit 2.x and should only be used for broken code that doesn't properly verify the autosave behavior (see Jackrabbit API). If this option is turned on autosave will be enabled by default; otherwise autosave is not supported.", - boolValue = false) + boolValue = false), + @Property(name = UserConstants.PARAM_PASSWORD_MAX_AGE, + label = "Maximum Password Age", + description = "Maximum age in days a password may have. Values greater 0 will implicitly enable password expiry. A value of 0 indicates unlimited password age.", + intValue = UserConstants.DEFAULT_PASSWORD_MAX_AGE), + @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. Note that a maximum password age must be set to a value > 0 in order to make this option functional.", + boolValue = UserConstants.DEFAULT_PASSWORD_INITIAL_CHANGE) }) public class UserConfigurationImpl extends ConfigurationBase implements UserConfiguration, SecurityConfiguration { + private final UserAuthenticationFactory defaultAuthFactory = new UserAuthenticationFactoryImpl(); + public UserConfigurationImpl() { super(); } @@ -112,6 +123,19 @@ @Override public String getName() { return NAME; + } + + @Nonnull + @Override + public ConfigurationParameters getParameters() { + ConfigurationParameters params = super.getParameters(); + if (!params.containsKey(UserConstants.PARAM_USER_AUTHENTICATION_FACTORY)) { + return ConfigurationParameters.of( + params, + ConfigurationParameters.of(UserConstants.PARAM_USER_AUTHENTICATION_FACTORY, defaultAuthFactory)); + } else { + return params; + } } @Nonnull Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/user/LoginModuleImpl.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/user/LoginModuleImpl.java (revision 1605678) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/user/LoginModuleImpl.java (revision ) @@ -23,6 +23,7 @@ import java.util.Map; import java.util.Set; import javax.annotation.CheckForNull; +import javax.annotation.Nullable; import javax.jcr.Credentials; import javax.jcr.GuestCredentials; import javax.jcr.SimpleCredentials; @@ -32,6 +33,7 @@ import javax.security.auth.login.LoginException; import org.apache.jackrabbit.oak.api.AuthInfo; +import org.apache.jackrabbit.oak.api.Root; import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; import org.apache.jackrabbit.oak.spi.security.SecurityProvider; import org.apache.jackrabbit.oak.spi.security.authentication.AbstractLoginModule; @@ -39,7 +41,9 @@ import org.apache.jackrabbit.oak.spi.security.authentication.Authentication; import org.apache.jackrabbit.oak.spi.security.authentication.ImpersonationCredentials; import org.apache.jackrabbit.oak.spi.security.authentication.PreAuthenticatedLogin; +import org.apache.jackrabbit.oak.spi.security.user.UserAuthenticationFactory; 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.UserUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -119,12 +123,12 @@ PreAuthenticatedLogin preAuthLogin = getSharedPreAuthLogin(); if (preAuthLogin != null) { userId = preAuthLogin.getUserId(); - Authentication authentication = new UserAuthentication(userId, getUserManager()); - success = authentication.authenticate(UserAuthentication.PRE_AUTHENTICATED); + Authentication authentication = getUserAuthentication(userId); + success = authentication != null && authentication.authenticate(PreAuthenticatedLogin.PRE_AUTHENTICATED); } else { userId = getUserId(); - Authentication authentication = new UserAuthentication(userId, getUserManager()); - success = authentication.authenticate(credentials); + Authentication authentication = getUserAuthentication(userId); + success = authentication != null && authentication.authenticate(credentials); } if (success) { @@ -215,6 +219,22 @@ ConfigurationParameters params = sp.getConfiguration(UserConfiguration.class).getParameters(); return UserUtil.getAnonymousId(params); } + } + + @CheckForNull + private Authentication getUserAuthentication(@Nullable String userId) { + SecurityProvider securityProvider = getSecurityProvider(); + Root root = getRoot(); + if (securityProvider != null && root != null) { + UserConfiguration uc = securityProvider.getConfiguration(UserConfiguration.class); + UserAuthenticationFactory factory = uc.getParameters().getConfigValue(UserConstants.PARAM_USER_AUTHENTICATION_FACTORY, null, UserAuthenticationFactory.class); + if (factory != null) { + return factory.getAuthentication(uc, root, userId); + } else { + log.error("No user authentication factory configured in user configuration."); + } + } + return null; } private AuthInfo createAuthInfo() { Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/user/UserAuthentication.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/user/UserAuthentication.java (revision 1605678) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserAuthentication.java (revision ) @@ -14,23 +14,36 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.oak.security.authentication.user; +package org.apache.jackrabbit.oak.security.user; import java.util.Collections; +import java.util.concurrent.TimeUnit; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import javax.jcr.Credentials; import javax.jcr.GuestCredentials; import javax.jcr.RepositoryException; import javax.jcr.SimpleCredentials; +import javax.jcr.Value; import javax.security.auth.Subject; +import javax.security.auth.login.CredentialExpiredException; import javax.security.auth.login.LoginException; import org.apache.jackrabbit.api.security.user.Authorizable; 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.security.user.CredentialsImpl; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; import org.apache.jackrabbit.oak.spi.security.authentication.Authentication; import org.apache.jackrabbit.oak.spi.security.authentication.ImpersonationCredentials; +import org.apache.jackrabbit.oak.spi.security.authentication.PreAuthenticatedLogin; +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.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,29 +69,32 @@ * will return {@code false} indicating that this implementation is not able * to verify their validity. */ -class UserAuthentication implements Authentication { +class UserAuthentication implements Authentication, UserConstants { - static final Credentials PRE_AUTHENTICATED = new Credentials() { }; - private static final Logger log = LoggerFactory.getLogger(UserAuthentication.class); + private static final String LAST_MOD_PATH = REP_PWD + "/" + REP_PASSWORD_LAST_MODIFIED; + + private final UserConfiguration config; + private final Root root; private final String userId; - private final UserManager userManager; - UserAuthentication(String userId, UserManager userManager) { + UserAuthentication(@Nonnull UserConfiguration config, @Nonnull Root root, @Nullable String userId) { + this.config = config; + this.root = root; this.userId = userId; - this.userManager = userManager; } //-----------------------------------------------------< Authentication >--- @Override public boolean authenticate(Credentials credentials) throws LoginException { - if (userId == null || userManager == null || credentials == null) { + if (credentials == null || userId == null) { return false; } boolean success = false; try { + UserManager userManager = config.getUserManager(root, NamePathMapper.DEFAULT); Authorizable authorizable = userManager.getAuthorizable(userId); if (authorizable == null) { return false; @@ -100,6 +116,10 @@ success = PasswordUtil.isSame(((CredentialsImpl) userCreds).getPasswordHash(), creds.getPassword()); } checkSuccess(success, "UserId/Password mismatch."); + + if (isPasswordExpired(user)) { + throw new CredentialExpiredException("User password has expired"); + } } else if (credentials instanceof ImpersonationCredentials) { ImpersonationCredentials ipCreds = (ImpersonationCredentials) credentials; AuthInfo info = ipCreds.getImpersonatorInfo(); @@ -107,7 +127,7 @@ checkSuccess(success, "Impersonation not allowed."); } else { // guest login is allowed if an anonymous user exists in the content (see get user above) - success = (credentials instanceof GuestCredentials) || credentials == PRE_AUTHENTICATED; + success = (credentials instanceof GuestCredentials) || credentials == PreAuthenticatedLogin.PRE_AUTHENTICATED; } } catch (RepositoryException e) { throw new LoginException(e.getMessage()); @@ -141,5 +161,50 @@ log.debug("Error while validating impersonation", e.getMessage()); } return false; + } + + @CheckForNull + private Long getPasswordLastModified(User user) throws RepositoryException { + if (user instanceof UserImpl) { + PropertyState property = ((UserImpl) user).getTree().getChild(REP_PWD).getProperty(REP_PASSWORD_LAST_MODIFIED); + return (property != null) ? property.getValue(Type.LONG) : null; + } else { + Value[] lastMod = user.getProperty(LAST_MOD_PATH); + if (lastMod != null && lastMod.length > 0) { + return lastMod[0].getLong(); + } else { + return null; + } + } + } + + private boolean isPasswordExpired(@Nonnull User user) throws RepositoryException { + // the password of the "admin" user never expires + if (user.isAdmin()) { + return false; + } + + boolean expired = false; + ConfigurationParameters params = config.getParameters(); + int maxAge = params.getConfigValue(PARAM_PASSWORD_MAX_AGE, DEFAULT_PASSWORD_MAX_AGE); + boolean forceInitialPwChange = params.getConfigValue(PARAM_PASSWORD_INITIAL_CHANGE, DEFAULT_PASSWORD_INITIAL_CHANGE); + if (maxAge > 0) { + // password expiry is enabled + Long passwordLastModified = getPasswordLastModified(user); + if (passwordLastModified == null) { + // no pw last modified property exists (yet) => expire! + expired = true; + } else { + // calculate expiry time (pw last mod + pw max age) and compare + long expiryTime = passwordLastModified + TimeUnit.MILLISECONDS.convert(maxAge, TimeUnit.DAYS); + // System.currentTimeMillis() may be inaccurate on windows. This is accepted for this feature. + expired = expiryTime < System.currentTimeMillis(); + } + } else if (forceInitialPwChange) { + Long passwordLastModified = getPasswordLastModified(user); + // no pw last modified property exists (yet) => expire! + expired = (null == passwordLastModified); + } + return expired; } } \ No newline at end of file Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserImpl.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/UserImpl.java (revision 1605678) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserImpl.java (revision ) @@ -44,7 +44,7 @@ UserImpl(String id, Tree tree, UserManagerImpl userManager) throws RepositoryException { super(id, tree, userManager); - isAdmin = UserUtil.getAdminId(userManager.getConfig()).equals(id); + isAdmin = UserUtil.isAdmin(userManager.getConfig(), id); } //---------------------------------------------------< AuthorizableImpl >--- @@ -96,7 +96,7 @@ } UserManagerImpl userManager = getUserManager(); userManager.onPasswordChange(this, password); - userManager.setPassword(getTree(), password, true); + userManager.setPassword(getTree(), getID(), password, true); } @Override \ No newline at end of file Index: oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/util/UserUtil.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/util/UserUtil.java (revision 1605678) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/util/UserUtil.java (revision ) @@ -40,6 +40,11 @@ } @Nonnull + public static boolean isAdmin(@Nonnull ConfigurationParameters parameters, @Nonnull String userId) { + return getAdminId(parameters).equals(userId); + } + + @Nonnull public static String getAdminId(@Nonnull ConfigurationParameters parameters) { return parameters.getConfigValue(PARAM_ADMIN_ID, DEFAULT_ADMIN_ID); } \ No newline at end of file Index: oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/InitialPasswordChangeTest.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/InitialPasswordChangeTest.java (revision ) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/InitialPasswordChangeTest.java (revision ) @@ -0,0 +1,89 @@ +/* + * 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.SimpleCredentials; +import javax.security.auth.login.CredentialExpiredException; + +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.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.junit.Before; +import org.junit.Test; + +import java.util.UUID; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class InitialPasswordChangeTest extends AbstractSecurityTest { + + private String userId; + private UserAuthentication authentication; + + @Before + public void before() throws Exception { + super.before(); + userId = getTestUser().getID(); + authentication = new UserAuthentication(getUserConfiguration(), root, userId); + } + + @Override + protected ConfigurationParameters getSecurityConfigParameters() { + ConfigurationParameters userConfig = ConfigurationParameters.of(ImmutableMap.of( + UserConstants.PARAM_PASSWORD_INITIAL_CHANGE, Boolean.TRUE, + UserConstants.PARAM_PASSWORD_MAX_AGE, 10)); + return ConfigurationParameters.of(ImmutableMap.of(UserConfiguration.NAME, userConfig)); + } + + @Test + public void testCreateUser() throws Exception { + String newUserId = "newuser" + UUID.randomUUID(); + User user = getUserManager(root).createUser(newUserId, newUserId); + root.commit(); + PropertyState property = root.getTree(user.getPath()).getChild(UserConstants.REP_PWD).getProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED); + assertNull(property); + } + + @Test + public void testAuthenticateForcePasswordChange() throws Exception { + try { + Authentication a = new UserAuthentication(getUserConfiguration(), root, userId); + a.authenticate(new SimpleCredentials(userId, userId.toCharArray())); + fail("Credentials should be expired"); + } catch (CredentialExpiredException e) { + // success + } + } + + @Test + public void testAuthenticateAfterPasswordChange() throws Exception { + User user = getTestUser(); + user.changePassword(userId); + root.commit(); + + Authentication a = new UserAuthentication(getUserConfiguration(), root, userId); + a.authenticate(new SimpleCredentials(userId, userId.toCharArray())); + } +} Index: oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/PreAuthenticatedLogin.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/authentication/PreAuthenticatedLogin.java (revision 1605678) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/PreAuthenticatedLogin.java (revision ) @@ -16,12 +16,16 @@ */ package org.apache.jackrabbit.oak.spi.security.authentication; +import javax.jcr.Credentials; + /** * {@code PreAuthenticatedLogin} is used as marker in the shared map of the login context. it indicates that the * respective user is pre authenticated on an external system. Note that is class is only used internally by the * login modules and cannot be "abused" from outside. */ final public class PreAuthenticatedLogin { + + public static final Credentials PRE_AUTHENTICATED = new Credentials() { }; private final String userId; \ No newline at end of file Index: oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/ConfigurationParameters.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/ConfigurationParameters.java (revision 1605678) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/ConfigurationParameters.java (revision ) @@ -30,6 +30,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import org.apache.jackrabbit.oak.commons.PropertiesUtil; import org.slf4j.Logger; @@ -142,6 +143,19 @@ options.put(String.valueOf(e.getKey()), e.getValue()); } return new ConfigurationParameters(options); + } + + /** + * Creates new a single valued configuration parameters instance from the + * given key and value. + * + * @param key The key + * @param value The value + * @return a new instance of configuration parameters. + */ + @Nonnull + public static ConfigurationParameters of(@Nonnull String key, @Nonnull Object value) { + return new ConfigurationParameters(ImmutableMap.of(key, value)); } /** \ No newline at end of file Index: oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserAuthenticationFactoryImplTest.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/UserAuthenticationFactoryImplTest.java (revision ) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserAuthenticationFactoryImplTest.java (revision ) @@ -0,0 +1,45 @@ +/* + * 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 org.apache.jackrabbit.oak.AbstractSecurityTest; +import org.apache.jackrabbit.oak.spi.security.authentication.Authentication; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class UserAuthenticationFactoryImplTest extends AbstractSecurityTest { + + private String userId; + private UserAuthenticationFactoryImpl factory; + + @Before + public void before() throws Exception { + super.before(); + factory = new UserAuthenticationFactoryImpl(); + userId = getTestUser().getID(); + } + + @Test + public void testGetAuthentication() throws Exception { + Authentication authentication = factory.getAuthentication(getUserConfiguration(), root, userId); + assertNotNull(authentication); + assertTrue(authentication instanceof UserAuthentication); + } +} Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserAuthenticationFactoryImpl.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/UserAuthenticationFactoryImpl.java (revision ) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserAuthenticationFactoryImpl.java (revision ) @@ -0,0 +1,38 @@ +/* + * 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 org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.spi.security.authentication.Authentication; +import org.apache.jackrabbit.oak.spi.security.user.UserAuthenticationFactory; +import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class UserAuthenticationFactoryImpl implements UserAuthenticationFactory { + + public UserAuthenticationFactoryImpl() { + } + + @Nonnull + @Override + public Authentication getAuthentication(UserConfiguration userConfiguration, @Nonnull Root root, @Nullable String userId) { + return new UserAuthentication(userConfiguration, root, userId); + } +} Index: oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/UserAuthenticationFactory.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/UserAuthenticationFactory.java (revision ) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/UserAuthenticationFactory.java (revision ) @@ -0,0 +1,46 @@ +/* + * 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.spi.security.user; + +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.spi.security.authentication.Authentication; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Provides a user management specific implementation of the + * {@link org.apache.jackrabbit.oak.spi.security.authentication.Authentication} + * interface to those {@link javax.security.auth.spi.LoginModule}s that verify + * a given authentication request by evaluation information exposed by the + * Jackrabbit user management API. + */ +public interface UserAuthenticationFactory { + + /** + * Returns an implementation {@link org.apache.jackrabbit.oak.spi.security.authentication.Authentication} + * for the specified {@code userId}. + * + * @param userConfiguration The user configuration. + * @param root The {@link org.apache.jackrabbit.oak.api.Root} that provides repository access. + * @param userId The userId for which a user authentication is provided. + * @return The authentication object specific to the provided user. + */ + @Nonnull + Authentication getAuthentication(@Nonnull UserConfiguration userConfiguration, @Nonnull Root root, @Nullable String userId); +} Index: oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordExpiryTest.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/PasswordExpiryTest.java (revision ) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordExpiryTest.java (revision ) @@ -0,0 +1,127 @@ +/* + * 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.api.security.user.User; +import org.apache.jackrabbit.oak.AbstractSecurityTest; +import org.apache.jackrabbit.oak.api.PropertyState; +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.junit.Before; +import org.junit.Test; + +import javax.jcr.SimpleCredentials; +import javax.security.auth.login.CredentialExpiredException; +import javax.security.auth.login.LoginException; +import java.util.UUID; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class PasswordExpiryTest 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 testCreateUser() throws Exception { + String newUserId = "newuser" + UUID.randomUUID(); + User user = getUserManager(root).createUser(newUserId, newUserId); + root.commit(); + PropertyState property = root.getTree(user.getPath()).getChild(UserConstants.REP_PWD).getProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED); + assertNotNull(property); + assertTrue(property.getValue(Type.LONG, 0) > 0); + } + + @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); + 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())); + } + + @Test + public void testAuthenticatePasswordExpired() 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 { + a.authenticate(new SimpleCredentials(userId, userId.toCharArray())); + fail("Credentials should be expired"); + } catch (CredentialExpiredException e) { + // success + } + } + + @Test + public void testAuthenticateBeforePasswordExpired() 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 { + a.authenticate(new SimpleCredentials(userId, "wrong".toCharArray())); + } catch (CredentialExpiredException e) { + fail("Login should fail before expiry"); + } catch (LoginException e) { + // success - userId/pw mismatch takes precedence over expiry + } + } + + @Test + public void testAuthenticatePasswordExpiredChangePassword() 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(); + + // 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()))); + } +} Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserManagerImpl.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/UserManagerImpl.java (revision 1605678) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserManagerImpl.java (revision ) @@ -35,6 +35,7 @@ import org.apache.jackrabbit.api.security.user.UserManager; 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.namepath.NamePathMapper; import org.apache.jackrabbit.oak.plugins.nodetype.ReadOnlyNodeTypeManager; import org.apache.jackrabbit.oak.security.user.query.UserQueryManager; @@ -51,6 +52,7 @@ import org.apache.jackrabbit.oak.spi.security.user.action.DefaultAuthorizableActionProvider; import org.apache.jackrabbit.oak.spi.security.user.util.PasswordUtil; import org.apache.jackrabbit.oak.spi.security.user.util.UserUtil; +import org.apache.jackrabbit.oak.util.NodeUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -154,7 +156,7 @@ Tree userTree = userProvider.createUser(userID, intermediatePath); setPrincipal(userTree, principal); if (password != null) { - setPassword(userTree, password, true); + setPassword(userTree, userID, password, true); } User user = new UserImpl(userID, userTree, this); @@ -358,7 +360,7 @@ authorizableTree.setProperty(UserConstants.REP_PRINCIPAL_NAME, principal.getName()); } - void setPassword(Tree userTree, String password, boolean forceHash) throws RepositoryException { + void setPassword(Tree userTree, String userId, String password, boolean forceHash) throws RepositoryException { String pwHash; if (forceHash || PasswordUtil.isPlainTextPassword(password)) { try { @@ -372,6 +374,34 @@ pwHash = password; } userTree.setProperty(UserConstants.REP_PASSWORD, pwHash); + + // set last-modified property if pw-expiry is enabled, the user is not + // admin. if initial-pw-change is enabled, we don't set the last modified + // for new users, in order to force a pw change upon the next login + + boolean expiryEnabled = passwordExpiryEnabled(); + boolean forceInitialPwChange = forceInitialPasswordChangeEnabled(userTree); + boolean isNewUser = userTree.getStatus() == Tree.Status.NEW; + + if (!UserUtil.isAdmin(config, userId) + // only expiry is enabled, set in all cases + && ((expiryEnabled && !forceInitialPwChange) + // as soon as force initial pw is enabled, we set in all cases except new users, + // irrespective of password expiry being enabled or not + || (forceInitialPwChange && !isNewUser))) { + + Tree pwTree = new NodeUtil(userTree).getOrAddChild(UserConstants.REP_PWD, UserConstants.NT_REP_PASSWORD).getTree(); + // System.currentTimeMillis() may be inaccurate on windows. This is accepted for this feature. + pwTree.setProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED, System.currentTimeMillis(), Type.LONG); + } + } + + private boolean passwordExpiryEnabled() { + return config.getConfigValue(UserConstants.PARAM_PASSWORD_MAX_AGE, UserConstants.DEFAULT_PASSWORD_MAX_AGE) > 0; + } + + private boolean forceInitialPasswordChangeEnabled(@Nonnull Tree userTree) { + return config.getConfigValue(UserConstants.PARAM_PASSWORD_INITIAL_CHANGE, UserConstants.DEFAULT_PASSWORD_INITIAL_CHANGE); } private UserQueryManager getQueryManager() { \ No newline at end of file Index: oak-core/src/main/resources/org/apache/jackrabbit/oak/plugins/nodetype/write/builtin_nodetypes.cnd IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/resources/org/apache/jackrabbit/oak/plugins/nodetype/write/builtin_nodetypes.cnd (revision 1605678) +++ oak-core/src/main/resources/org/apache/jackrabbit/oak/plugins/nodetype/write/builtin_nodetypes.cnd (revision ) @@ -707,7 +707,12 @@ mixin - rep:impersonators (STRING) protected multiple +[rep:Password] + - * (UNDEFINED) protected + - * (UNDEFINED) protected multiple + [rep:User] > rep:Authorizable, rep:Impersonatable + + rep:pw (rep:Password) = rep:Password protected - rep:password (STRING) protected - rep:disabled (STRING) protected