Add ActiveDirectory fix to properly support sAMAccountName as username
This commit is contained in:
parent
bb8ac0000e
commit
767e6a9783
3 changed files with 213 additions and 33 deletions
|
@ -0,0 +1,94 @@
|
|||
package org.keycloak.picketlink.idm;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import javax.naming.directory.BasicAttributes;
|
||||
|
||||
import org.keycloak.models.utils.reflection.Reflections;
|
||||
import org.picketlink.idm.config.LDAPMappingConfiguration;
|
||||
import org.picketlink.idm.ldap.internal.LDAPIdentityStore;
|
||||
import org.picketlink.idm.ldap.internal.LDAPOperationManager;
|
||||
import org.picketlink.idm.model.AttributedType;
|
||||
import org.picketlink.idm.model.basic.User;
|
||||
import org.picketlink.idm.query.IdentityQuery;
|
||||
import org.picketlink.idm.spi.IdentityContext;
|
||||
|
||||
import static org.picketlink.common.constants.LDAPConstants.CN;
|
||||
import static org.picketlink.common.constants.LDAPConstants.COMMA;
|
||||
import static org.picketlink.common.constants.LDAPConstants.EQUAL;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class KeycloakLDAPIdentityStore extends LDAPIdentityStore {
|
||||
|
||||
public static Method GET_BINDING_DN_METHOD;
|
||||
public static Method GET_OPERATION_MANAGER_METHOD;
|
||||
public static Method CREATE_SEARCH_FILTER_METHOD;
|
||||
public static Method EXTRACT_ATTRIBUTES_METHOD;
|
||||
public static Method GET_ENTRY_IDENTIFIER_METHOD;
|
||||
|
||||
public static final String SAM_ACCOUNT_NAME = "sAMAccountName";
|
||||
|
||||
static {
|
||||
GET_BINDING_DN_METHOD = getMethodOnLDAPStore("getBindingDN", AttributedType.class);
|
||||
GET_OPERATION_MANAGER_METHOD = getMethodOnLDAPStore("getOperationManager");
|
||||
CREATE_SEARCH_FILTER_METHOD = getMethodOnLDAPStore("createIdentityTypeSearchFilter", IdentityQuery.class, LDAPMappingConfiguration.class);
|
||||
EXTRACT_ATTRIBUTES_METHOD = getMethodOnLDAPStore("extractAttributes", AttributedType.class, boolean.class);
|
||||
GET_ENTRY_IDENTIFIER_METHOD = getMethodOnLDAPStore("getEntryIdentifier", AttributedType.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAttributedType(IdentityContext context, AttributedType attributedType) {
|
||||
LDAPMappingConfiguration userMappingConfig = getConfig().getMappingConfig(attributedType.getClass());
|
||||
String ldapUsernameAttrName = userMappingConfig.getMappedProperties().get(userMappingConfig.getIdProperty().getName());
|
||||
|
||||
if (getConfig().isActiveDirectory() && SAM_ACCOUNT_NAME.equals(ldapUsernameAttrName)) {
|
||||
// TODO: pain, but everything in picketlink is private... Improve if possible...
|
||||
LDAPOperationManager operationManager = Reflections.invokeMethod(false, GET_OPERATION_MANAGER_METHOD, LDAPOperationManager.class, this);
|
||||
|
||||
String cn = getCommonName(attributedType);
|
||||
String bindingDN = CN + EQUAL + cn + COMMA + userMappingConfig.getBaseDN();
|
||||
|
||||
BasicAttributes ldapAttributes = Reflections.invokeMethod(false, EXTRACT_ATTRIBUTES_METHOD, BasicAttributes.class, this, attributedType, true);
|
||||
ldapAttributes.put(CN, cn);
|
||||
|
||||
operationManager.createSubContext(bindingDN, ldapAttributes);
|
||||
|
||||
String ldapId = Reflections.invokeMethod(false, GET_ENTRY_IDENTIFIER_METHOD, String.class, this, attributedType);
|
||||
attributedType.setId(ldapId);
|
||||
} else {
|
||||
super.addAttributedType(context, attributedType);
|
||||
}
|
||||
}
|
||||
|
||||
// Hack as methods are protected on LDAPIdentityStore :/
|
||||
public static Method getMethodOnLDAPStore(String methodName, Class... classes) {
|
||||
try {
|
||||
Method m = LDAPIdentityStore.class.getDeclaredMethod(methodName, classes);
|
||||
m.setAccessible(true);
|
||||
return m;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected String getCommonName(AttributedType aType) {
|
||||
User user = (User)aType;
|
||||
String fullName;
|
||||
if (user.getFirstName() != null && user.getLastName() != null) {
|
||||
fullName = user.getFirstName() + " " + user.getLastName();
|
||||
} else if (user.getFirstName() != null && user.getFirstName().trim().length() > 0) {
|
||||
fullName = user.getFirstName();
|
||||
} else {
|
||||
fullName = user.getLastName();
|
||||
}
|
||||
|
||||
// Fallback to loginName
|
||||
if (fullName == null || fullName.trim().length() == 0) {
|
||||
fullName = user.getLoginName();
|
||||
}
|
||||
|
||||
return fullName;
|
||||
}
|
||||
}
|
|
@ -1,21 +1,27 @@
|
|||
package org.keycloak.picketlink.idm;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import javax.naming.NamingException;
|
||||
import javax.naming.directory.BasicAttribute;
|
||||
import javax.naming.directory.DirContext;
|
||||
import javax.naming.directory.ModificationItem;
|
||||
import javax.naming.directory.SearchResult;
|
||||
|
||||
import org.keycloak.models.utils.reflection.Reflections;
|
||||
import org.picketlink.idm.IdentityManager;
|
||||
import org.picketlink.idm.config.LDAPMappingConfiguration;
|
||||
import org.picketlink.idm.credential.Credentials;
|
||||
import org.picketlink.idm.credential.Password;
|
||||
import org.picketlink.idm.credential.UsernamePasswordCredentials;
|
||||
import org.picketlink.idm.ldap.internal.LDAPIdentityStore;
|
||||
import org.picketlink.idm.ldap.internal.LDAPOperationManager;
|
||||
import org.picketlink.idm.ldap.internal.LDAPPlainTextPasswordCredentialHandler;
|
||||
import org.picketlink.idm.model.Account;
|
||||
import org.picketlink.idm.model.AttributedType;
|
||||
import org.picketlink.idm.model.basic.User;
|
||||
import org.picketlink.idm.query.IdentityQuery;
|
||||
import org.picketlink.idm.spi.IdentityContext;
|
||||
|
||||
import static org.picketlink.idm.IDMLog.CREDENTIAL_LOGGER;
|
||||
|
@ -26,19 +32,12 @@ import static org.picketlink.idm.model.basic.BasicModel.getUser;
|
|||
*/
|
||||
public class LDAPKeycloakCredentialHandler extends LDAPPlainTextPasswordCredentialHandler {
|
||||
|
||||
private static Method GET_BINDING_DN_METHOD;
|
||||
private static Method GET_OPERATION_MANAGER_METHOD;
|
||||
|
||||
static {
|
||||
GET_BINDING_DN_METHOD = getMethodOnLDAPStore("getBindingDN", AttributedType.class);
|
||||
GET_OPERATION_MANAGER_METHOD = getMethodOnLDAPStore("getOperationManager");
|
||||
}
|
||||
|
||||
// Used just 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
|
||||
private String userAccountControlAfterPasswordUpdate;
|
||||
|
||||
@Override
|
||||
public void setup(LDAPIdentityStore store) {
|
||||
// TODO: Don't setup it here once PLIDM-508 is fixed
|
||||
if (store.getConfig().isActiveDirectory() || Boolean.getBoolean("keycloak.ldap.ad.skipUserAccountControlAfterPasswordUpdate")) {
|
||||
String userAccountControlProp = System.getProperty("keycloak.ldap.ad.userAccountControlAfterPasswordUpdate");
|
||||
this.userAccountControlAfterPasswordUpdate = userAccountControlProp!=null ? userAccountControlProp : "512";
|
||||
|
@ -48,7 +47,7 @@ public class LDAPKeycloakCredentialHandler extends LDAPPlainTextPasswordCredenti
|
|||
|
||||
// Overridden as in Keycloak, we don't have Agents
|
||||
@Override
|
||||
protected Account getAccount(IdentityContext context, String loginName) {
|
||||
protected User getAccount(IdentityContext context, String loginName) {
|
||||
IdentityManager identityManager = getIdentityManager(context);
|
||||
|
||||
if (CREDENTIAL_LOGGER.isDebugEnabled()) {
|
||||
|
@ -60,34 +59,111 @@ public class LDAPKeycloakCredentialHandler extends LDAPPlainTextPasswordCredenti
|
|||
|
||||
@Override
|
||||
public void update(IdentityContext context, Account account, Password password, LDAPIdentityStore store, Date effectiveDate, Date expiryDate) {
|
||||
super.update(context, account, password, store, effectiveDate, expiryDate);
|
||||
if (!store.getConfig().isActiveDirectory()) {
|
||||
super.update(context, account, password, store, effectiveDate, expiryDate);
|
||||
} else {
|
||||
User user = (User)account;
|
||||
LDAPOperationManager operationManager = Reflections.invokeMethod(false, KeycloakLDAPIdentityStore.GET_OPERATION_MANAGER_METHOD, LDAPOperationManager.class, store);
|
||||
IdentityManager identityManager = getIdentityManager(context);
|
||||
String userDN = getDNOfUser(user, identityManager, store, operationManager);
|
||||
|
||||
if (userAccountControlAfterPasswordUpdate != null) {
|
||||
ModificationItem[] mods = new ModificationItem[1];
|
||||
BasicAttribute mod0 = new BasicAttribute("userAccountControl", userAccountControlAfterPasswordUpdate);
|
||||
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod0);
|
||||
updateADPassword(userDN, new String(password.getValue()), operationManager);
|
||||
|
||||
try {
|
||||
String bindingDN = (String) GET_BINDING_DN_METHOD.invoke(store, account);
|
||||
LDAPOperationManager operationManager = (LDAPOperationManager) GET_OPERATION_MANAGER_METHOD.invoke(store);
|
||||
operationManager.modifyAttribute(bindingDN, mod0);
|
||||
} catch (IllegalAccessException iae) {
|
||||
throw new RuntimeException(iae);
|
||||
} catch (InvocationTargetException ite) {
|
||||
Throwable cause = ite.getTargetException() != null ? ite.getTargetException() : ite;
|
||||
throw new RuntimeException(cause);
|
||||
if (userAccountControlAfterPasswordUpdate != null) {
|
||||
ModificationItem[] mods = new ModificationItem[1];
|
||||
BasicAttribute mod0 = new BasicAttribute("userAccountControl", userAccountControlAfterPasswordUpdate);
|
||||
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod0);
|
||||
operationManager.modifyAttribute(userDN, mod0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hack as methods are protected on LDAPIdentityStore :/
|
||||
private static Method getMethodOnLDAPStore(String methodName, Class... classes) {
|
||||
protected void updateADPassword(String userDN, String password, LDAPOperationManager operationManager) {
|
||||
try {
|
||||
Method m = LDAPIdentityStore.class.getDeclaredMethod(methodName, classes);
|
||||
m.setAccessible(true);
|
||||
return m;
|
||||
} catch (Exception e) {
|
||||
// Replace the "unicdodePwd" attribute with a new value
|
||||
// Password must be both Unicode and a quoted string
|
||||
String newQuotedPassword = "\"" + password + "\"";
|
||||
byte[] newUnicodePassword = newQuotedPassword.getBytes("UTF-16LE");
|
||||
BasicAttribute unicodePwd = new BasicAttribute("unicodePwd", newUnicodePassword);
|
||||
operationManager.modifyAttribute(userDN, unicodePwd);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(IdentityContext context, UsernamePasswordCredentials credentials,
|
||||
LDAPIdentityStore store) {
|
||||
credentials.setStatus(Credentials.Status.INVALID);
|
||||
credentials.setValidatedAccount(null);
|
||||
|
||||
if (CREDENTIAL_LOGGER.isDebugEnabled()) {
|
||||
CREDENTIAL_LOGGER.debugf("Validating credentials [%s][%s] using identity store [%s] and credential handler [%s].", credentials.getClass(), credentials, store, this);
|
||||
}
|
||||
|
||||
User account = getAccount(context, credentials.getUsername());
|
||||
|
||||
// If the user for the provided username cannot be found we fail validation
|
||||
if (account != null) {
|
||||
if (CREDENTIAL_LOGGER.isDebugEnabled()) {
|
||||
CREDENTIAL_LOGGER.debugf("Found account [%s] from credentials [%s].", account, credentials);
|
||||
}
|
||||
|
||||
if (account.isEnabled()) {
|
||||
if (CREDENTIAL_LOGGER.isDebugEnabled()) {
|
||||
CREDENTIAL_LOGGER.debugf("Account [%s] is ENABLED.", account, credentials);
|
||||
}
|
||||
|
||||
char[] password = credentials.getPassword().getValue();
|
||||
|
||||
// String bindingDN = store.getBindingDN(account);
|
||||
|
||||
LDAPOperationManager operationManager = Reflections.invokeMethod(false, KeycloakLDAPIdentityStore.GET_OPERATION_MANAGER_METHOD, LDAPOperationManager.class, store);
|
||||
String bindingDN = getDNOfUser(account, getIdentityManager(context), store, operationManager);
|
||||
|
||||
if (operationManager.authenticate(bindingDN, new String(password))) {
|
||||
credentials.setValidatedAccount(account);
|
||||
credentials.setStatus(Credentials.Status.VALID);
|
||||
}
|
||||
} else {
|
||||
if (CREDENTIAL_LOGGER.isDebugEnabled()) {
|
||||
CREDENTIAL_LOGGER.debugf("Account [%s] is DISABLED.", account, credentials);
|
||||
}
|
||||
credentials.setStatus(Credentials.Status.ACCOUNT_DISABLED);
|
||||
}
|
||||
} else {
|
||||
if (CREDENTIAL_LOGGER.isDebugEnabled()) {
|
||||
CREDENTIAL_LOGGER.debugf("Account NOT FOUND for credentials [%s][%s].", credentials.getClass(), credentials);
|
||||
}
|
||||
}
|
||||
|
||||
if (CREDENTIAL_LOGGER.isDebugEnabled()) {
|
||||
CREDENTIAL_LOGGER.debugf("Credential [%s][%s] validated using identity store [%s] and credential handler [%s]. Status [%s]. Validated Account [%s]",
|
||||
credentials.getClass(), credentials, store, this, credentials.getStatus(), credentials.getValidatedAccount());
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove later... It's needed just because LDAPIdentityStore.getBindingName, which always uses idProperty as first part of DN, but in AD it doesn't work as we may have idProperty 'sAMAccountName'
|
||||
// but DN like: cn=John Doe,OU=foo,DC=bar
|
||||
protected String getDNOfUser(User user, IdentityManager identityManager, LDAPIdentityStore ldapStore, LDAPOperationManager operationManager) {
|
||||
|
||||
LDAPMappingConfiguration ldapEntryConfig = ldapStore.getConfig().getMappingConfig(User.class);
|
||||
IdentityQuery<User> identityQuery = identityManager.createIdentityQuery(User.class)
|
||||
.setParameter(User.LOGIN_NAME, user.getLoginName());
|
||||
StringBuilder filter = Reflections.invokeMethod(false, KeycloakLDAPIdentityStore.CREATE_SEARCH_FILTER_METHOD, StringBuilder.class, ldapStore, identityQuery, ldapEntryConfig);
|
||||
|
||||
List<SearchResult> search = null;
|
||||
try {
|
||||
search = operationManager.search(ldapEntryConfig.getBaseDN(), filter.toString(), ldapEntryConfig);
|
||||
} catch (NamingException ne) {
|
||||
throw new RuntimeException(ne);
|
||||
}
|
||||
|
||||
if (search.size() > 0) {
|
||||
SearchResult sr1 = search.get(0);
|
||||
return sr1.getNameInNamespace();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
package org.keycloak.picketlink.realm;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.picketlink.idm.KeycloakLDAPIdentityStore;
|
||||
import org.keycloak.picketlink.idm.LDAPKeycloakCredentialHandler;
|
||||
import org.keycloak.picketlink.idm.LdapConstants;
|
||||
import org.picketlink.idm.PartitionManager;
|
||||
import org.picketlink.idm.config.AbstractIdentityStoreConfiguration;
|
||||
import org.picketlink.idm.config.IdentityConfiguration;
|
||||
import org.picketlink.idm.config.IdentityConfigurationBuilder;
|
||||
import org.picketlink.idm.config.IdentityStoreConfiguration;
|
||||
import org.picketlink.idm.config.LDAPIdentityStoreConfiguration;
|
||||
import org.picketlink.idm.internal.DefaultPartitionManager;
|
||||
import org.picketlink.idm.model.basic.User;
|
||||
|
@ -104,7 +109,12 @@ public class PartitionManagerRegistry {
|
|||
.attribute("lastName", ldapLastName)
|
||||
.attribute("email", ldapEmail);
|
||||
|
||||
return new DefaultPartitionManager(builder.buildAll());
|
||||
// Workaround to override the LDAPIdentityStore with our own :/
|
||||
List<IdentityConfiguration> identityConfigs = builder.buildAll();
|
||||
IdentityStoreConfiguration identityStoreConfig = identityConfigs.get(0).getStoreConfiguration().get(0);
|
||||
((AbstractIdentityStoreConfiguration)identityStoreConfig).setIdentityStoreType(KeycloakLDAPIdentityStore.class);
|
||||
|
||||
return new DefaultPartitionManager(identityConfigs);
|
||||
}
|
||||
|
||||
private void checkSystemProperty(String name, String defaultValue) {
|
||||
|
|
Loading…
Reference in a new issue