diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPConfig.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPConfig.java
index 18c2d8fc3d..e8af5a9dd8 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPConfig.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPConfig.java
@@ -113,12 +113,6 @@ public class LDAPConfig {
return uuidAttrName;
}
- // TODO: Remove and use mapper instead?
- public boolean isUserAccountControlsAfterPasswordUpdate() {
- String userAccountCtrls = config.get(LDAPConstants.USER_ACCOUNT_CONTROLS_AFTER_PASSWORD_UPDATE);
- return userAccountCtrls==null ? false : Boolean.parseBoolean(userAccountCtrls);
- }
-
public boolean isPagination() {
String pagination = config.get(LDAPConstants.PAGINATION);
return pagination==null ? false : Boolean.parseBoolean(pagination);
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java
index 698a392c12..f93de4633b 100755
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java
@@ -41,6 +41,8 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
+import javax.naming.AuthenticationException;
+
/**
* @author Marek Posolda
* @author Bill Burke
@@ -366,7 +368,22 @@ public class LDAPFederationProvider implements UserFederationProvider {
} else {
// Use Naming LDAP API
LDAPObject ldapUser = loadAndValidateUser(realm, user);
- return ldapIdentityStore.validatePassword(ldapUser, password);
+
+ try {
+ ldapIdentityStore.validatePassword(ldapUser, password);
+ return true;
+ } catch (AuthenticationException ae) {
+
+ // Check if any mapper provides callback for handle LDAP AuthenticationException
+ Set federationMappers = realm.getUserFederationMappersByFederationProvider(getModel().getId());
+ boolean processed = false;
+ for (UserFederationMapperModel mapperModel : federationMappers) {
+ LDAPFederationMapper ldapMapper = getMapper(mapperModel);
+ processed = processed || ldapMapper.onAuthenticationFailure(mapperModel, this, ldapUser, user, ae, realm);
+ }
+
+ return processed;
+ }
}
}
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java
index 551d940acd..539b9c7b02 100755
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java
@@ -16,6 +16,7 @@ import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory;
import org.keycloak.federation.ldap.mappers.LDAPFederationMapper;
import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapper;
import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapperFactory;
+import org.keycloak.federation.ldap.mappers.msad.MSADUserAccountControlMapperFactory;
import org.keycloak.mappers.UserFederationMapper;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@@ -188,6 +189,12 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "false");
realm.addUserFederationMapper(mapperModel);
+
+ // MSAD specific mapper for account state propagation
+ if (activeDirectory) {
+ mapperModel = KeycloakModelUtils.createUserFederationMapperModel("MSAD account controls", newProviderModel.getId(), MSADUserAccountControlMapperFactory.PROVIDER_ID);
+ realm.addUserFederationMapper(mapperModel);
+ }
}
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java
index cbb28f96ed..a334ed0264 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java
@@ -66,6 +66,10 @@ public class LDAPObject {
readOnlyAttributeNames.add(readOnlyAttribute.toLowerCase());
}
+ public void removeReadOnlyAttributeName(String readOnlyAttribute) {
+ readOnlyAttributeNames.remove(readOnlyAttribute.toLowerCase());
+ }
+
public String getRdnAttributeName() {
return rdnAttributeName;
}
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LDAPQuery.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LDAPQuery.java
index 6939697db2..3019942206 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LDAPQuery.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LDAPQuery.java
@@ -189,4 +189,8 @@ public class LDAPQuery {
return this.conditions;
}
+ public LDAPFederationProvider getLdapProvider() {
+ return ldapFedProvider;
+ }
+
}
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/IdentityStore.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/IdentityStore.java
index 23c6d99eb5..77691b10b0 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/IdentityStore.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/IdentityStore.java
@@ -2,6 +2,8 @@ package org.keycloak.federation.ldap.idm.store;
import java.util.List;
+import javax.naming.AuthenticationException;
+
import org.keycloak.federation.ldap.LDAPConfig;
import org.keycloak.federation.ldap.idm.model.LDAPObject;
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
@@ -65,8 +67,9 @@ public interface IdentityStore {
*
* @param user Keycloak user
* @param password Ldap password
+ * @throws AuthenticationException if authentication is not successful
*/
- boolean validatePassword(LDAPObject user, String password);
+ void validatePassword(LDAPObject user, String password) throws AuthenticationException;
/**
* Updates the specified credential value.
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java
index 8193679be4..e217fe4e03 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java
@@ -11,6 +11,7 @@ import java.util.NoSuchElementException;
import java.util.Set;
import java.util.TreeSet;
+import javax.naming.AuthenticationException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
@@ -173,18 +174,14 @@ public class LDAPIdentityStore implements IdentityStore {
// *************** CREDENTIALS AND USER SPECIFIC STUFF
@Override
- public boolean validatePassword(LDAPObject user, String password) {
+ public void validatePassword(LDAPObject user, String password) throws AuthenticationException {
String userDN = user.getDn().toString();
if (logger.isTraceEnabled()) {
logger.tracef("Using DN [%s] for authentication of user", userDN);
}
- if (operationManager.authenticate(userDN, password)) {
- return true;
- }
-
- return false;
+ operationManager.authenticate(userDN, password);
}
@Override
@@ -225,15 +222,6 @@ public class LDAPIdentityStore implements IdentityStore {
List modItems = new ArrayList();
modItems.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, unicodePwd));
- // Used in ActiveDirectory to put account into "enabled" state (aka userAccountControl=512, see http://support.microsoft.com/kb/305144/en ) after password update. If value is -1, it's ignored
- // TODO: Remove and use mapper instead
- if (getConfig().isUserAccountControlsAfterPasswordUpdate()) {
- BasicAttribute userAccountControl = new BasicAttribute("userAccountControl", "512");
- modItems.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, userAccountControl));
-
- logger.debugf("Attribute userAccountControls will be switched to 512 after password update of user [%s]", userDN);
- }
-
operationManager.modifyAttributes(userDN, modItems.toArray(new ModificationItem[] {}));
} catch (Exception e) {
throw new ModelException(e);
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java
index 8a5b299437..3d0d22bd76 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java
@@ -12,6 +12,7 @@ import java.util.Map;
import java.util.Properties;
import java.util.Set;
+import javax.naming.AuthenticationException;
import javax.naming.Binding;
import javax.naming.Context;
import javax.naming.InitialContext;
@@ -320,15 +321,15 @@ public class LDAPOperationManager {
*
* @param dn
* @param password
+ * @throws AuthenticationException if authentication is not successful
*
- * @return
*/
- public boolean authenticate(String dn, String password) {
+ public void authenticate(String dn, String password) throws AuthenticationException {
InitialContext authCtx = null;
try {
if (password == null || password.isEmpty()) {
- throw new Exception("Empty password used");
+ throw new AuthenticationException("Empty password used");
}
Hashtable env = new Hashtable(this.connectionProperties);
@@ -342,13 +343,15 @@ public class LDAPOperationManager {
authCtx = new InitialLdapContext(env, null);
- return true;
- } catch (Exception e) {
+ } catch (AuthenticationException ae) {
if (logger.isDebugEnabled()) {
- logger.debugf(e, "Authentication failed for DN [%s]", dn);
+ logger.debugf(ae, "Authentication failed for DN [%s]", dn);
}
- return false;
+ throw ae;
+ } catch (Exception e) {
+ logger.errorf(e, "Unexpected exception when validating password of DN [%s]", dn);
+ throw new AuthenticationException("Unexpected exception when validating password of user");
} finally {
if (authCtx != null) {
try {
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java
index 2a79a48673..4990817037 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java
@@ -3,6 +3,8 @@ package org.keycloak.federation.ldap.mappers;
import java.util.Collections;
import java.util.List;
+import javax.naming.AuthenticationException;
+
import org.keycloak.federation.ldap.LDAPFederationProvider;
import org.keycloak.federation.ldap.idm.model.LDAPObject;
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
@@ -70,6 +72,10 @@ public abstract class AbstractLDAPFederationMapper {
return Collections.emptyList();
}
+ public boolean onAuthenticationFailure(LDAPObject ldapUser, UserModel user, AuthenticationException ldapException) {
+ return false;
+ }
+
public static boolean parseBooleanParameter(UserFederationMapperModel mapperModel, String paramName) {
String paramm = mapperModel.getConfig().get(paramName);
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java
index 8d5ab4ea86..a27b9bf1ba 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java
@@ -1,5 +1,7 @@
package org.keycloak.federation.ldap.mappers;
+import javax.naming.AuthenticationException;
+
import org.keycloak.federation.ldap.LDAPFederationProvider;
import org.keycloak.federation.ldap.idm.model.LDAPObject;
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
@@ -59,4 +61,17 @@ public interface LDAPFederationMapper extends UserFederationMapper {
* @param query
*/
void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query);
+
+ /**
+ * Called when LDAP authentication of specified user fails. If any mapper returns true from this method, AuthenticationException won't be rethrown!
+ *
+ * @param mapperModel
+ * @param ldapProvider
+ * @param realm
+ * @param user
+ * @param ldapUser
+ * @param ldapException
+ * @return true if mapper processed the AuthenticationException and did some actions based on that. In that case, AuthenticationException won't be rethrown!
+ */
+ boolean onAuthenticationFailure(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, AuthenticationException ldapException, RealmModel realm);
}
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapperBridge.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapperBridge.java
index b77bf3836f..3bc4451ff7 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapperBridge.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapperBridge.java
@@ -2,6 +2,8 @@ package org.keycloak.federation.ldap.mappers;
import java.util.List;
+import javax.naming.AuthenticationException;
+
import org.keycloak.federation.ldap.LDAPFederationProvider;
import org.keycloak.federation.ldap.idm.model.LDAPObject;
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
@@ -55,7 +57,7 @@ public class LDAPFederationMapperBridge implements LDAPFederationMapper {
@Override
public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
// Improve if needed
- getDelegate(mapperModel, null, null).beforeLDAPQuery(query);
+ getDelegate(mapperModel, query.getLdapProvider(), null).beforeLDAPQuery(query);
}
@@ -64,6 +66,11 @@ public class LDAPFederationMapperBridge implements LDAPFederationMapper {
return getDelegate(mapperModel, ldapProvider, realm).getGroupMembers(group, firstResult, maxResults);
}
+ @Override
+ public boolean onAuthenticationFailure(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, AuthenticationException ldapException, RealmModel realm) {
+ return getDelegate(mapperModel, ldapProvider, realm).onAuthenticationFailure(ldapUser, user, ldapException);
+ }
+
private AbstractLDAPFederationMapper getDelegate(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, RealmModel realm) {
LDAPFederationProvider ldapProvider = (LDAPFederationProvider) federationProvider;
return factory.createMapper(mapperModel, ldapProvider, realm);
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/MSADUserAccountControlMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/MSADUserAccountControlMapper.java
new file mode 100644
index 0000000000..cd233a1dc1
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/MSADUserAccountControlMapper.java
@@ -0,0 +1,249 @@
+package org.keycloak.federation.ldap.mappers.msad;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.naming.AuthenticationException;
+
+import org.jboss.logging.Logger;
+import org.keycloak.federation.ldap.LDAPFederationProvider;
+import org.keycloak.federation.ldap.idm.model.LDAPObject;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
+import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserFederationMapperModel;
+import org.keycloak.models.UserFederationProvider;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.UserModelDelegate;
+
+/**
+ * Mapper specific to MSAD. It's able to read the userAccountControl and pwdLastSet attributes and set actions in Keycloak based on that.
+ * It's also able to handle exception code from LDAP user authentication (See http://www-01.ibm.com/support/docview.wss?uid=swg21290631 )
+ *
+ * @author Marek Posolda
+ */
+public class MSADUserAccountControlMapper extends AbstractLDAPFederationMapper {
+
+ private static final Logger logger = Logger.getLogger(MSADUserAccountControlMapper.class);
+
+ private static final Pattern AUTH_EXCEPTION_REGEX = Pattern.compile(".*AcceptSecurityContext error, data ([0-9a-f]*), v.*");
+
+ public MSADUserAccountControlMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm) {
+ super(mapperModel, ldapProvider, realm);
+ }
+
+ @Override
+ public void beforeLDAPQuery(LDAPQuery query) {
+ query.addReturningLdapAttribute(LDAPConstants.PWD_LAST_SET);
+ query.addReturningLdapAttribute(LDAPConstants.USER_ACCOUNT_CONTROL);
+
+ // This needs to be read-only and can be set to writable just on demand
+ query.addReturningReadOnlyLdapAttribute(LDAPConstants.PWD_LAST_SET);
+
+ if (ldapProvider.getEditMode() != UserFederationProvider.EditMode.WRITABLE) {
+ query.addReturningReadOnlyLdapAttribute(LDAPConstants.USER_ACCOUNT_CONTROL);
+ }
+ }
+
+ @Override
+ public UserModel proxy(LDAPObject ldapUser, UserModel delegate) {
+ return new MSADUserModelDelegate(delegate, ldapUser);
+ }
+
+ @Override
+ public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser) {
+
+ }
+
+ @Override
+ public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate) {
+
+ }
+
+ @Override
+ public boolean onAuthenticationFailure(LDAPObject ldapUser, UserModel user, AuthenticationException ldapException) {
+ String exceptionMessage = ldapException.getMessage();
+ Matcher m = AUTH_EXCEPTION_REGEX.matcher(exceptionMessage);
+ if (m.matches()) {
+ String errorCode = m.group(1);
+ return processAuthErrorCode(errorCode, user);
+ } else {
+ return false;
+ }
+ }
+
+ protected boolean processAuthErrorCode(String errorCode, UserModel user) {
+ logger.debugf("MSAD Error code is '%s' after failed LDAP login of user", errorCode, user.getUsername());
+
+ if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE) {
+ if (errorCode.equals("532") || errorCode.equals("773")) {
+ // User needs to change his MSAD password. Allow him to login, but add UPDATE_PASSWORD required action
+ user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
+ return true;
+ } else if (errorCode.equals("533")) {
+ // User is disabled in MSAD. Set him to disabled in KC as well
+ user.setEnabled(false);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+
+ public class MSADUserModelDelegate extends UserModelDelegate {
+
+ private final LDAPObject ldapUser;
+
+ public MSADUserModelDelegate(UserModel delegate, LDAPObject ldapUser) {
+ super(delegate);
+ this.ldapUser = ldapUser;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ boolean kcEnabled = super.isEnabled();
+
+ if (getPwdLastSet() > 0) {
+ // Merge KC and MSAD
+ return kcEnabled && !getUserAccountControl().has(UserAccountControl.ACCOUNTDISABLE);
+ } else {
+ // If new MSAD user is created and pwdLastSet is still 0, MSAD account is in disabled state. So read just from Keycloak DB. User is not able to login via MSAD anyway
+ return kcEnabled;
+ }
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ // Always update DB
+ super.setEnabled(enabled);
+
+ if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && getPwdLastSet() > 0) {
+ logger.debugf("Going to propagate enabled=%s for ldapUser '%s' to MSAD", enabled, ldapUser.getDn().toString());
+
+ UserAccountControl control = getUserAccountControl();
+ if (enabled) {
+ control.remove(UserAccountControl.ACCOUNTDISABLE);
+ } else {
+ control.add(UserAccountControl.ACCOUNTDISABLE);
+ }
+
+ updateUserAccountControl(control);
+ }
+ }
+
+ @Override
+ public void updateCredential(UserCredentialModel cred) {
+ // Update LDAP password first
+ super.updateCredential(cred);
+
+ if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && cred.getType().equals(UserCredentialModel.PASSWORD)) {
+ logger.debugf("Going to update userAccountControl for ldap user '%s' after successful password update", ldapUser.getDn().toString());
+
+ // Normally it's read-only
+ ldapUser.removeReadOnlyAttributeName(LDAPConstants.PWD_LAST_SET);
+
+ ldapUser.setSingleAttribute(LDAPConstants.PWD_LAST_SET, "-1");
+
+ UserAccountControl control = getUserAccountControl();
+ control.remove(UserAccountControl.PASSWD_NOTREQD);
+ control.remove(UserAccountControl.PASSWORD_EXPIRED);
+
+ if (super.isEnabled()) {
+ control.remove(UserAccountControl.ACCOUNTDISABLE);
+ }
+
+ updateUserAccountControl(control);
+ }
+ }
+
+ @Override
+ public void addRequiredAction(RequiredAction action) {
+ String actionName = action.name();
+ addRequiredAction(actionName);
+ }
+
+ @Override
+ public void addRequiredAction(String action) {
+ // Always update DB
+ super.addRequiredAction(action);
+
+ if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && RequiredAction.UPDATE_PASSWORD.toString().equals(action)) {
+ logger.debugf("Going to propagate required action UPDATE_PASSWORD to MSAD for ldap user '%s' ", ldapUser.getDn().toString());
+
+ // Normally it's read-only
+ ldapUser.removeReadOnlyAttributeName(LDAPConstants.PWD_LAST_SET);
+
+ ldapUser.setSingleAttribute(LDAPConstants.PWD_LAST_SET, "0");
+ ldapProvider.getLdapIdentityStore().update(ldapUser);
+ }
+ }
+
+ @Override
+ public void removeRequiredAction(RequiredAction action) {
+ String actionName = action.name();
+ removeRequiredAction(actionName);
+ }
+
+ @Override
+ public void removeRequiredAction(String action) {
+ // Always update DB
+ super.removeRequiredAction(action);
+
+ if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && RequiredAction.UPDATE_PASSWORD.toString().equals(action)) {
+
+ // Don't set pwdLastSet in MSAD when it is new user
+ UserAccountControl accountControl = getUserAccountControl();
+ if (accountControl.getValue() != 0 && !accountControl.has(UserAccountControl.PASSWD_NOTREQD)) {
+ logger.debugf("Going to remove required action UPDATE_PASSWORD from MSAD for ldap user '%s' ", ldapUser.getDn().toString());
+
+ // Normally it's read-only
+ ldapUser.removeReadOnlyAttributeName(LDAPConstants.PWD_LAST_SET);
+
+ ldapUser.setSingleAttribute(LDAPConstants.PWD_LAST_SET, "-1");
+ ldapProvider.getLdapIdentityStore().update(ldapUser);
+ }
+ }
+ }
+
+ @Override
+ public Set getRequiredActions() {
+ Set requiredActions = super.getRequiredActions();
+
+ if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE) {
+ if (getPwdLastSet() == 0 || getUserAccountControl().has(UserAccountControl.PASSWORD_EXPIRED)) {
+ requiredActions = new HashSet<>(requiredActions);
+ requiredActions.add(RequiredAction.UPDATE_PASSWORD.toString());
+ return requiredActions;
+ }
+ }
+
+ return requiredActions;
+ }
+
+ protected long getPwdLastSet() {
+ String pwdLastSet = ldapUser.getAttributeAsString(LDAPConstants.PWD_LAST_SET);
+ return pwdLastSet == null ? 0 : Long.parseLong(pwdLastSet);
+ }
+
+ protected UserAccountControl getUserAccountControl() {
+ String userAccountControl = ldapUser.getAttributeAsString(LDAPConstants.USER_ACCOUNT_CONTROL);
+ long longValue = userAccountControl == null ? 0 : Long.parseLong(userAccountControl);
+ return new UserAccountControl(longValue);
+ }
+
+ // Update user in LDAP
+ protected void updateUserAccountControl(UserAccountControl accountControl) {
+ String userAccountControlValue = String.valueOf(accountControl.getValue());
+ logger.debugf("Updating userAccountControl of user '%s' to value '%s'", ldapUser.getDn().toString(), userAccountControlValue);
+
+ ldapUser.setSingleAttribute(LDAPConstants.USER_ACCOUNT_CONTROL, userAccountControlValue);
+ ldapProvider.getLdapIdentityStore().update(ldapUser);
+ }
+ }
+
+}
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/MSADUserAccountControlMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/MSADUserAccountControlMapperFactory.java
new file mode 100644
index 0000000000..cacce00879
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/MSADUserAccountControlMapperFactory.java
@@ -0,0 +1,68 @@
+package org.keycloak.federation.ldap.mappers.msad;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.keycloak.federation.ldap.LDAPFederationProvider;
+import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper;
+import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapperFactory;
+import org.keycloak.mappers.MapperConfigValidationException;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserFederationMapperModel;
+import org.keycloak.models.UserFederationProviderModel;
+import org.keycloak.provider.ProviderConfigProperty;
+
+/**
+ * @author Marek Posolda
+ */
+public class MSADUserAccountControlMapperFactory extends AbstractLDAPFederationMapperFactory {
+
+ public static final String PROVIDER_ID = LDAPConstants.MSAD_USER_ACCOUNT_CONTROL_MAPPER;
+ protected static final List configProperties = new ArrayList();
+
+ static {
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Mapper specific to MSAD. It's able to integrate the MSAD user account state into Keycloak account state (account enabled, password is expired etc). It's using userAccountControl and pwdLastSet MSAD attributes for that. " +
+ "For example if pwdLastSet is 0, the Keycloak user is required to update password, if userAccountControl is 514 (disabled account) the Keycloak user is disabled as well etc. Mapper is also able to handle exception code from LDAP user authentication.";
+ }
+
+ @Override
+ public String getDisplayCategory() {
+ return ATTRIBUTE_MAPPER_CATEGORY;
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "MSAD User Account Control";
+ }
+
+ @Override
+ public List getConfigProperties() {
+ return configProperties;
+ }
+
+ @Override
+ public Map getDefaultConfig(UserFederationProviderModel providerModel) {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public void validateConfig(RealmModel realm, UserFederationMapperModel mapperModel) throws MapperConfigValidationException {
+ }
+
+ @Override
+ protected AbstractLDAPFederationMapper createMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider federationProvider, RealmModel realm) {
+ return new MSADUserAccountControlMapper(mapperModel, federationProvider, realm);
+ }
+}
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/UserAccountControl.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/UserAccountControl.java
new file mode 100644
index 0000000000..c7f831715c
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/UserAccountControl.java
@@ -0,0 +1,58 @@
+package org.keycloak.federation.ldap.mappers.msad;
+
+/**
+ * See https://support.microsoft.com/en-us/kb/305144
+ *
+ * @author Marek Posolda
+ */
+public class UserAccountControl {
+
+ public static final long SCRIPT = 0x0001l;
+ public static final long ACCOUNTDISABLE = 0x0002l;
+ public static final long HOMEDIR_REQUIRED = 0x0008l;
+ public static final long LOCKOUT = 0x0010l;
+ public static final long PASSWD_NOTREQD = 0x0020l;
+ public static final long PASSWD_CANT_CHANGE = 0x0040l;
+ public static final long ENCRYPTED_TEXT_PWD_ALLOWED = 0x0080l;
+ public static final long TEMP_DUPLICATE_ACCOUNT = 0x0100l;
+ public static final long NORMAL_ACCOUNT = 0x0200l;
+ public static final long INTERDOMAIN_TRUST_ACCOUNT = 0x0800l;
+ public static final long WORKSTATION_TRUST_ACCOUNT = 0x1000l;
+ public static final long SERVER_TRUST_ACCOUNT = 0x2000l;
+ public static final long DONT_EXPIRE_PASSWORD = 0x10000l;
+ public static final long MNS_LOGON_ACCOUNT = 0x20000l;
+ public static final long SMARTCARD_REQUIRED = 0x40000l;
+ public static final long TRUSTED_FOR_DELEGATION = 0x80000l;
+ public static final long NOT_DELEGATED = 0x100000l;
+ public static final long USE_DES_KEY_ONLY = 0x200000l;
+ public static final long DONT_REQ_PREAUTH = 0x400000l;
+ public static final long PASSWORD_EXPIRED = 0x800000l;
+ public static final long TRUSTED_TO_AUTH_FOR_DELEGATION = 0x1000000l;
+ public static final long PARTIAL_SECRETS_ACCOUNT = 0x04000000l;
+
+ private long value;
+
+ public UserAccountControl(long value) {
+ this.value = value;
+ }
+
+ public boolean has(long feature) {
+ return (this.value & feature) > 0;
+ }
+
+ public void add(long feature) {
+ if (!has(feature)) {
+ this.value += feature;
+ }
+ }
+
+ public void remove(long feature) {
+ if (has(feature)) {
+ this.value -= feature;
+ }
+ }
+
+ public long getValue() {
+ return value;
+ }
+}
diff --git a/federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory b/federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory
index ac130a551e..0575a65042 100644
--- a/federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory
+++ b/federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory
@@ -2,4 +2,5 @@ org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapperFactory
org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory
org.keycloak.federation.ldap.mappers.HardcodedLDAPRoleMapperFactory
org.keycloak.federation.ldap.mappers.membership.role.RoleLDAPFederationMapperFactory
-org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapperFactory
\ No newline at end of file
+org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapperFactory
+org.keycloak.federation.ldap.mappers.msad.MSADUserAccountControlMapperFactory
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-ldap.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-ldap.html
index 6263f3d9f6..1ddf888015 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-ldap.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-ldap.html
@@ -183,14 +183,6 @@
Does the LDAP server support pagination.
-
-
-
-
-
- Useful just for Active Directory. If enabled, then Keycloak will always set
- Active Directory userAccountControl attribute to 512 after password update. This would mean that particular user will be enabled in Active Directory
-