Index: oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/util/PasswordUtil.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/PasswordUtil.java (revision 1605678) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/util/PasswordUtil.java (revision ) @@ -56,6 +56,8 @@ public static final String DEFAULT_ALGORITHM = "SHA-256"; public static final int DEFAULT_SALT_SIZE = 8; public static final int DEFAULT_ITERATIONS = 1000; + public static final int DEFAULT_PASSWORD_MAX_AGE = 0; + public static final boolean DEFAULT_PASSWORD_INITIAL_CHANGE = false; /** * Avoid instantiation @@ -184,7 +186,7 @@ } return false; } - + //------------------------------------------------------------< private >--- /** * Compare two strings. The comparison is constant time: it will always loop \ No newline at end of file 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,24 +14,35 @@ * 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; +import java.util.HashMap; import java.util.List; import java.util.Set; import javax.annotation.Nonnull; import javax.jcr.Credentials; import javax.jcr.GuestCredentials; import javax.jcr.SimpleCredentials; +import javax.security.auth.login.AccountNotFoundException; +import javax.security.auth.login.CredentialExpiredException; +import javax.security.auth.login.FailedLoginException; import javax.security.auth.login.LoginException; +import com.google.common.collect.ImmutableMap; import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials; import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.api.security.user.User; import org.apache.jackrabbit.oak.AbstractSecurityTest; import org.apache.jackrabbit.oak.api.AuthInfo; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.security.SecurityProviderImpl; +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.user.UserConfiguration; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; import org.junit.Before; import org.junit.Test; @@ -45,24 +56,20 @@ public class UserAuthenticationTest extends AbstractSecurityTest { private String userId; - private UserAuthentication authentication; + private Authentication authentication; + private UserAuthenticationFactoryImpl userAuthenticationFactory = new UserAuthenticationFactoryImpl((UserConfigurationImpl) getUserConfiguration()); @Before public void before() throws Exception { super.before(); userId = getTestUser().getID(); - authentication = new UserAuthentication(userId, getUserManager(root)); + userAuthenticationFactory = new UserAuthenticationFactoryImpl((UserConfigurationImpl) getUserConfiguration()); + authentication = userAuthenticationFactory.getAuthentication(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 authentication = userAuthenticationFactory.getAuthentication(root, null); assertFalse(authentication.authenticate(new SimpleCredentials(userId, userId.toCharArray()))); } @@ -80,29 +87,31 @@ @Test public void testAuthenticateCannotResolveUser() throws Exception { SimpleCredentials sc = new SimpleCredentials("unknownUser", "pw".toCharArray()); - Authentication a = new UserAuthentication(sc.getUserID(), getUserManager(root)); - - assertFalse(a.authenticate(sc)); + Authentication a = userAuthenticationFactory.getAuthentication(root, sc.getUserID()); + try { + a.authenticate(sc); + fail("expected account not found exception"); + } catch (AccountNotFoundException e) { + // success - } + } + } @Test 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 = userAuthenticationFactory.getAuthentication(root, sc.getUserID()); try { a.authenticate(sc); fail("Authenticating Group should fail"); - } catch (LoginException e) { + } catch (AccountNotFoundException e) { // success } finally { - if (g != null) { - g.remove(); - root.commit(); - } - } + g.remove(); + root.commit(); + } + } - } @Test public void testAuthenticateInvalidSimpleCredentials() throws Exception { @@ -115,7 +124,7 @@ try { authentication.authenticate(creds); fail("LoginException expected"); - } catch (LoginException e) { + } catch (FailedLoginException e) { // success } } @@ -166,7 +175,114 @@ assertTrue(authentication.authenticate(new ImpersonationCredentials(sc, new TestAuthInfo()))); } + @Test + public void testAuthenticatePasswordExpiredNewUser() throws Exception { + UserConfigurationImpl c = setCustomConfiguration( + ConfigurationParameters.of(new HashMap() {{ + put(UserConstants.PARAM_PASSWORD_MAX_AGE, 10); + }}) + ); + Authentication a = getCustomAuthentication(c, 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 { + UserConfigurationImpl c = setCustomConfiguration( + ConfigurationParameters.of(new HashMap() {{ + put(UserConstants.PARAM_PASSWORD_MAX_AGE, 10); + }}) + ); + Authentication a = getCustomAuthentication(c, userId); + // set password last modified to beginning of epoch + root.getTree(getTestUser().getPath()).getChild(UserConstants.NN_REP_PW).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 testAuthenticatePasswordExpiredChangePassword() throws Exception { + UserConfigurationImpl c = setCustomConfiguration( + ConfigurationParameters.of(new HashMap() {{ + put(UserConstants.PARAM_PASSWORD_MAX_AGE, 10); + }}) + ); + Authentication a = getCustomAuthentication(c, userId); + // set password last modified to beginning of epoch + root.getTree(getTestUser().getPath()).getChild(UserConstants.NN_REP_PW).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 + } + // changing the password should reset the pw last mod and the pw no longer be expired + getTestUser().changePassword(userId); + assertTrue(a.authenticate(new SimpleCredentials(userId, userId.toCharArray()))); + } + + @Test + public void testAuthenticateForcePasswordChange() throws Exception { + ConfigurationParameters params = ConfigurationParameters.of(new HashMap() {{ + put(UserConstants.PARAM_PASSWORD_INITIAL_CHANGE, true); + }}); + UserConfigurationImpl userConfig = setCustomConfiguration(params); + UserManagerImpl usrMgr = getCustomUserManager(params); + User newuser = createUser("newuser", usrMgr); + Authentication a = getCustomAuthentication(userConfig, newuser.getID()); + try { + a.authenticate(new SimpleCredentials(newuser.getID(), newuser.getID().toCharArray())); + fail("Credentials should be expired"); + } catch (CredentialExpiredException e) { + // success + } + } + + @Test + public void testAuthenticateForcePasswordChangeChangedPassword() throws Exception { + ConfigurationParameters params = ConfigurationParameters.of(new HashMap() {{ + put(UserConstants.PARAM_PASSWORD_INITIAL_CHANGE, true); + }}); + UserConfigurationImpl userConfig = setCustomConfiguration(params); + UserManagerImpl usrMgr = getCustomUserManager(params); + User newuser = createUser("newuser", usrMgr); + Authentication a = getCustomAuthentication(userConfig, newuser.getID()); + try { + a.authenticate(new SimpleCredentials(newuser.getID(), newuser.getID().toCharArray())); + fail("Credentials should be expired"); + } catch (CredentialExpiredException e) { + // success + } + newuser.changePassword(newuser.getID()); + a.authenticate(new SimpleCredentials(newuser.getID(), newuser.getID().toCharArray())); + } + //-------------------------------------------------------------------------- + private UserConfigurationImpl setCustomConfiguration(ConfigurationParameters params) { + SecurityProviderImpl sp = (SecurityProviderImpl) getSecurityProvider(); + UserConfigurationImpl config = new UserConfigurationImpl(sp); + config.setParameters(params); + UserConfigurationImpl c = (UserConfigurationImpl) sp.getConfiguration(UserConfiguration.class); + c.setParameters(params); + return config; + } + + private Authentication getCustomAuthentication(UserConfigurationImpl config, String userId) { + UserAuthenticationFactoryImpl factory = new UserAuthenticationFactoryImpl(config); + return factory.getAuthentication(root, userId); + } + + private UserManagerImpl getCustomUserManager(ConfigurationParameters params) { + ConfigurationParameters cParams = ConfigurationParameters.of(ImmutableMap.of(UserConfiguration.NAME, params)); + return new UserManagerImpl(root, NamePathMapper.DEFAULT, new SecurityProviderImpl(cParams)); + } private final class TestAuthInfo implements AuthInfo { \ No newline at end of file Index: oak-core/src/test/java/org/apache/jackrabbit/oak/AbstractSecurityTest.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/test/java/org/apache/jackrabbit/oak/AbstractSecurityTest.java (revision 1605678) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/AbstractSecurityTest.java (revision ) @@ -50,6 +50,7 @@ import org.apache.jackrabbit.oak.plugins.nodetype.write.InitialContent; import org.apache.jackrabbit.oak.plugins.value.ValueFactoryImpl; import org.apache.jackrabbit.oak.security.SecurityProviderImpl; +import org.apache.jackrabbit.oak.security.user.UserManagerImpl; import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; import org.apache.jackrabbit.oak.spi.security.SecurityProvider; import org.apache.jackrabbit.oak.spi.security.authentication.ConfigurationUtil; @@ -198,11 +199,17 @@ protected User getTestUser() throws Exception { if (testUser == null) { - String uid = "testUser" + UUID.randomUUID(); - testUser = getUserManager(root).createUser(uid, uid); - root.commit(); + testUser = createUser("testUser", null); } return testUser; + } + + protected User createUser(String userId, UserManagerImpl usrMgr) throws Exception{ + String uid = userId + UUID.randomUUID(); + UserManager mgr = (null != usrMgr) ? usrMgr : getUserManager(root); + User user = mgr.createUser(uid, uid); + root.commit(); + return user; } protected ContentSession createTestSession() throws Exception { 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,11 +42,14 @@ 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 NN_REP_PW = "rep:pw"; + Collection GROUP_PROPERTY_NAMES = ImmutableSet.of( REP_PRINCIPAL_NAME, REP_AUTHORIZABLE_ID, @@ -90,6 +94,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 +181,16 @@ * 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"; + + /** + * 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"; } 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 javax.annotation.Nonnull; + +public class UserAuthenticationFactoryImpl implements UserAuthenticationFactory { + + private UserConfigurationImpl config; + + public UserAuthenticationFactoryImpl(UserConfigurationImpl config) { + this.config = config; + } + + @Nonnull + @Override + public Authentication getAuthentication(@Nonnull Root root, String userId) { + return new UserAuthentication(config, root, userId); + } +} 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 ) @@ -18,6 +18,7 @@ import java.security.Principal; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -90,20 +91,40 @@ @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 = PasswordUtil.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.", + boolValue = PasswordUtil.DEFAULT_PASSWORD_INITIAL_CHANGE) }) public class UserConfigurationImpl extends ConfigurationBase implements UserConfiguration, SecurityConfiguration { + private final UserAuthenticationFactoryImpl factory = new UserAuthenticationFactoryImpl(this); + public UserConfigurationImpl() { super(); + ConfigurationParameters params = ConfigurationParameters.of(new HashMap() {{ + put(UserConstants.PARAM_USER_AUTHENTICATION_FACTORY, factory); + }}); + setParameters(params); } - public UserConfigurationImpl(SecurityProvider securityProvider) { + public UserConfigurationImpl(final SecurityProvider securityProvider) { super(securityProvider, securityProvider.getParameters(NAME)); + ConfigurationParameters params = ConfigurationParameters.of(new HashMap() {{ + putAll(securityProvider.getParameters(NAME)); + put(UserConstants.PARAM_USER_AUTHENTICATION_FACTORY, factory); + }}); + setParameters(params); } @Activate private void activate(Map properties) { + properties.put(UserConstants.PARAM_USER_AUTHENTICATION_FACTORY, factory); setParameters(ConfigurationParameters.of(properties)); } 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 ) @@ -39,7 +39,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; @@ -97,6 +99,8 @@ private static final Logger log = LoggerFactory.getLogger(LoginModuleImpl.class); + private static final Credentials PRE_AUTHENTICATED = new Credentials() { }; + protected static final Set SUPPORTED_CREDENTIALS = new HashSet(3); static { SUPPORTED_CREDENTIALS.add(SimpleCredentials.class); @@ -112,32 +116,40 @@ @Override public boolean login() throws LoginException { - final boolean success; + boolean success = false; credentials = getCredentials(); + UserAuthenticationFactory factory = getUserAuthenticationFactory(); + if (factory != null) { + - // check if we have a pre authenticated login from a previous login module - PreAuthenticatedLogin preAuthLogin = getSharedPreAuthLogin(); + // check if we have a pre authenticated login from a previous login module + PreAuthenticatedLogin preAuthLogin = getSharedPreAuthLogin(); - if (preAuthLogin != null) { - userId = preAuthLogin.getUserId(); - Authentication authentication = new UserAuthentication(userId, getUserManager()); - success = authentication.authenticate(UserAuthentication.PRE_AUTHENTICATED); - } else { - userId = getUserId(); - Authentication authentication = new UserAuthentication(userId, getUserManager()); - success = authentication.authenticate(credentials); - } + userId = (preAuthLogin != null) + ? preAuthLogin.getUserId() + : getUserId(); + Credentials creds = (preAuthLogin != null) + ? PRE_AUTHENTICATED + : credentials; + Authentication authentication = factory.getAuthentication(getRoot(), userId); + success = authentication.authenticate(creds); + - if (success) { - principals = getPrincipals(userId); + if (success) { + principals = getPrincipals(userId); - log.debug("Adding Credentials to shared state."); - //noinspection unchecked - sharedState.put(SHARED_KEY_CREDENTIALS, credentials); + log.debug("Adding Credentials to shared state."); + //noinspection unchecked + sharedState.put(SHARED_KEY_CREDENTIALS, credentials); - log.debug("Adding login name to shared state."); - //noinspection unchecked - sharedState.put(SHARED_KEY_LOGIN_NAME, userId); - } + log.debug("Adding login name to shared state."); + //noinspection unchecked + sharedState.put(SHARED_KEY_LOGIN_NAME, userId); + } + + } else { + log.error("No user authentication factory configured in user configuration."); + } + return success; } @@ -215,6 +227,13 @@ ConfigurationParameters params = sp.getConfiguration(UserConfiguration.class).getParameters(); return UserUtil.getAnonymousId(params); } + } + + private UserAuthenticationFactory getUserAuthenticationFactory() { + return getSecurityProvider() + .getConfiguration(UserConfiguration.class) + .getParameters() + .getConfigValue(UserConstants.PARAM_USER_AUTHENTICATION_FACTORY, null, UserAuthenticationFactory.class); } private AuthInfo createAuthInfo() { 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,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.spi.security.user; + +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.spi.security.authentication.Authentication; + +import javax.annotation.Nonnull; + +/** + * Provides {@link org.apache.jackrabbit.oak.security.user.UserAuthentication}s to {@link javax.security.auth.spi.LoginModule}s, as a means of preventing the need for implementation casting across package boundaries. + */ +public interface UserAuthenticationFactory { + + /** + * Returns the implementation's {@link org.apache.jackrabbit.oak.security.user.UserAuthentication}. + * + * @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 {@link org.apache.jackrabbit.oak.security.user.UserAuthentication} specific to the provided user. + */ + @Nonnull + Authentication getAuthentication(@Nonnull Root root, String userId); +} 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 ) @@ -19,6 +19,7 @@ import java.io.UnsupportedEncodingException; import java.security.NoSuchAlgorithmException; import java.security.Principal; +import java.util.Calendar; import java.util.Iterator; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; @@ -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; @@ -155,7 +157,11 @@ setPrincipal(userTree, principal); if (password != null) { setPassword(userTree, password, true); + + if (forceInitialPasswordChange()) { + userTree.getChild(UserConstants.NN_REP_PW).removeProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED); - } + } + } User user = new UserImpl(userID, userTree, this); onCreate(user, password); @@ -372,6 +378,8 @@ pwHash = password; } userTree.setProperty(UserConstants.REP_PASSWORD, pwHash); + + setPasswordLastModified(userTree, Calendar.getInstance().getTimeInMillis()); } private UserQueryManager getQueryManager() { @@ -379,5 +387,17 @@ queryManager = new UserQueryManager(this, namePathMapper, config, root); } return queryManager; + } + + private boolean forceInitialPasswordChange() { + return config.getConfigValue(UserConstants.PARAM_PASSWORD_INITIAL_CHANGE, false); + } + + private void setPasswordLastModified(Tree userTree, long lastModified) throws RepositoryException { + NodeUtil parent = new NodeUtil(userTree); + NodeUtil pwNode = (parent.hasChild(UserConstants.NN_REP_PW)) + ? parent.getChild(UserConstants.NN_REP_PW) + : parent.addChild(UserConstants.NN_REP_PW, UserConstants.NT_REP_PASSWORD); + pwNode.setDate(UserConstants.REP_PASSWORD_LAST_MODIFIED, lastModified); } } \ No newline at end of file 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,37 @@ * 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.Calendar; import java.util.Collections; +import java.util.concurrent.TimeUnit; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; import javax.jcr.Credentials; import javax.jcr.GuestCredentials; import javax.jcr.RepositoryException; import javax.jcr.SimpleCredentials; import javax.security.auth.Subject; +import javax.security.auth.login.AccountLockedException; +import javax.security.auth.login.AccountNotFoundException; +import javax.security.auth.login.CredentialExpiredException; +import javax.security.auth.login.FailedLoginException; 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.Tree; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; 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.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; @@ -62,44 +76,55 @@ private static final Logger log = LoggerFactory.getLogger(UserAuthentication.class); + private final UserConfiguration config; + private final Root root; private final String userId; - private final UserManager userManager; - UserAuthentication(String userId, UserManager userManager) { + UserAuthentication(@Nonnull UserConfigurationImpl config, @Nonnull Root root, 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) { + + UserManager userManager = config.getUserManager(root, NamePathMapper.DEFAULT); + + if (credentials == null || userId == null) { return false; } - boolean success = false; + boolean success; try { Authorizable authorizable = userManager.getAuthorizable(userId); if (authorizable == null) { - return false; + throw new AccountNotFoundException("Could not find user " + userId); } if (authorizable.isGroup()) { - throw new LoginException("Not a user " + userId); + throw new AccountNotFoundException("Not a user " + userId); } User user = (User) authorizable; if (user.isDisabled()) { - throw new LoginException("User with ID " + userId + " has been disabled: "+ user.getDisabledReason()); + throw new AccountLockedException("User with ID " + userId + " has been disabled: "+ user.getDisabledReason()); } if (credentials instanceof SimpleCredentials) { - SimpleCredentials creds = (SimpleCredentials) credentials; + + if (isPasswordExpired(user)) { + throw new CredentialExpiredException("User password has expired"); + } + Credentials userCreds = user.getCredentials(); - if (userId.equals(creds.getUserID()) && userCreds instanceof CredentialsImpl) { - success = PasswordUtil.isSame(((CredentialsImpl) userCreds).getPasswordHash(), creds.getPassword()); + if (!PasswordUtil.isSame(((CredentialsImpl) userCreds).getPasswordHash(), ((SimpleCredentials) credentials).getPassword())) { + throw new FailedLoginException("UserId/Password mismatch."); } - checkSuccess(success, "UserId/Password mismatch."); + + success = true; + } else if (credentials instanceof ImpersonationCredentials) { ImpersonationCredentials ipCreds = (ImpersonationCredentials) credentials; AuthInfo info = ipCreds.getImpersonatorInfo(); @@ -141,5 +166,46 @@ log.debug("Error while validating impersonation", e.getMessage()); } return false; + } + + @CheckForNull + private Long getPasswordLastModified(Tree userTree) { + PropertyState property = userTree.getChild(UserConstants.NN_REP_PW).getProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED); + return null != property ? property.getValue(Type.LONG, 0) : null; + } + + private boolean isPasswordExpired(@Nonnull User user) throws RepositoryException { + + Tree userTree = root.getTree(user.getPath()); + + // the password of the "admin" user never expires + String adminId = config.getParameters().getConfigValue(UserConstants.PARAM_ADMIN_ID, UserConstants.DEFAULT_ADMIN_ID); + if (adminId.equalsIgnoreCase(user.getID())) { + return false; + } + + final boolean expired; + + Long passwordLastModified = getPasswordLastModified(userTree); + int maxAge = config.getParameters().getConfigValue(UserConstants.PARAM_PASSWORD_MAX_AGE, 0); + boolean forceInitialPasswordChange = config.getParameters().getConfigValue(UserConstants.PARAM_PASSWORD_INITIAL_CHANGE, false); + + if (maxAge > 0) { + if (passwordLastModified == null) { + // password expiry is enabled, but no expiry property exists (yet) => expire! + expired = true; + } else { + // password expiry is enabled, calculate expiry time (pw last mod + pw max age) and compare + long expiryTime = passwordLastModified + TimeUnit.MILLISECONDS.convert(maxAge, TimeUnit.DAYS); + expired = expiryTime < Calendar.getInstance().getTimeInMillis(); + } + + } else { + // a password is defined as never having changed (after initial setting, usually during user + // creation) when no pw last modified property exists. + expired = forceInitialPasswordChange && passwordLastModified == null; + } + + return expired; } } \ 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