package org.apereo.cas.authentication;
import org.apache.commons.lang3.StringUtils;
import org.apereo.cas.authentication.handler.support.AbstractUsernamePasswordAuthenticationHandler;
import org.apereo.cas.authentication.principal.Principal;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.authentication.support.LdapPasswordPolicyConfiguration;
import org.apereo.cas.services.ServicesManager;
import org.ldaptive.LdapAttribute;
import org.ldaptive.LdapEntry;
import org.ldaptive.LdapException;
import org.ldaptive.ReturnAttributes;
import org.ldaptive.auth.AuthenticationRequest;
import org.ldaptive.auth.AuthenticationResponse;
import org.ldaptive.auth.AuthenticationResultCode;
import org.ldaptive.auth.Authenticator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.PostConstruct;
import javax.security.auth.login.AccountNotFoundException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* LDAP authentication handler that uses the ldaptive {@code Authenticator} component underneath.
* This handler provides simple attribute resolution machinery by reading attributes from the entry
* corresponding to the DN of the bound user (in the bound security context) upon successful authentication.
* Principal resolution is controlled by the following properties:
* <ul>
* <li>{@link #setPrincipalIdAttribute(String)}</li>
* <li>{@link #setPrincipalAttributeMap(java.util.Map)}</li>
* </ul>
*
* @author Marvin S. Addison
* @since 4.0.0
*/
public class LdapAuthenticationHandler extends AbstractUsernamePasswordAuthenticationHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(LdapAuthenticationHandler.class);
/**
* Mapping of LDAP attribute name to principal attribute name.
*/
protected Map<String, String> principalAttributeMap = Collections.emptyMap();
/**
* Performs LDAP authentication given username/password.
**/
private final Authenticator authenticator;
/**
* Name of attribute to be used for resolved principal.
*/
private String principalIdAttribute;
/**
* Flag indicating whether multiple values are allowed fo principalIdAttribute.
*/
private boolean allowMultiplePrincipalAttributeValues;
/**
* Flag to indicate whether CAS should block authentication
* if a specific/configured principal id attribute is not found.
*/
private boolean allowMissingPrincipalAttributeValue = true;
/**
* Set of LDAP attributes fetch from an entry as part of the authentication process.
*/
private String[] authenticatedEntryAttributes = ReturnAttributes.NONE.value();
/**
* Creates a new authentication handler that delegates to the given authenticator.
*
* @param name the name
* @param servicesManager the services manager
* @param principalFactory the principal factory
* @param order the order
* @param authenticator Ldaptive authenticator component.
*/
public LdapAuthenticationHandler(final String name, final ServicesManager servicesManager, final PrincipalFactory principalFactory,
final Integer order,
final Authenticator authenticator) {
super(name, servicesManager, principalFactory, order);
this.authenticator = authenticator;
}
/**
* Sets the name of the LDAP principal attribute whose value should be used for the
* principal ID.
*
* @param attributeName LDAP attribute name.
*/
public void setPrincipalIdAttribute(final String attributeName) {
this.principalIdAttribute = attributeName;
}
/**
* Sets a flag that determines whether multiple values are allowed for the {@link #principalIdAttribute}.
* This flag only has an effect if {@link #principalIdAttribute} is configured. If multiple values are detected
* when the flag is false, the first value is used and a warning is logged. If multiple values are detected
* when the flag is true, an exception is raised.
*
* @param allowed True to allow multiple principal ID attribute values, false otherwise.
*/
public void setAllowMultiplePrincipalAttributeValues(final boolean allowed) {
this.allowMultiplePrincipalAttributeValues = allowed;
}
/**
* Sets the mapping of additional principal attributes where the key is the LDAP attribute
* name and the value is the principal attribute name. The key set defines the set of
* attributes read from the LDAP entry at authentication time. Note that the principal ID attribute
* should not be listed among these attributes.
*
* @param attributeNameMap Map of LDAP attribute name to principal attribute name.
*/
public void setPrincipalAttributeMap(final Map<String, String> attributeNameMap) {
this.principalAttributeMap = attributeNameMap;
}
/**
* Sets the mapping of additional principal attributes where the key and value is the LDAP attribute
* name. Note that the principal ID attribute
* should not be listed among these attributes.
*
* @param attributeList List of LDAP attribute names
*/
public void setPrincipalAttributeList(final List<String> attributeList) {
this.principalAttributeMap = attributeList.stream().collect(Collectors.toMap(Object::toString, Function.identity()));
}
@Override
protected HandlerResult authenticateUsernamePasswordInternal(final UsernamePasswordCredential upc,
final String originalPassword) throws GeneralSecurityException, PreventedException {
final AuthenticationResponse response;
try {
LOGGER.debug("Attempting LDAP authentication for [{}]. Authenticator pre-configured attributes are [{}], "
+ "additional requested attributes for this authentication request are [{}]",
upc, authenticator.getReturnAttributes(), authenticatedEntryAttributes);
final AuthenticationRequest request = new AuthenticationRequest(upc.getUsername(),
new org.ldaptive.Credential(upc.getPassword()), authenticatedEntryAttributes);
response = authenticator.authenticate(request);
} catch (final LdapException e) {
LOGGER.trace(e.getMessage(), e);
throw new PreventedException("Unexpected LDAP error", e);
}
LOGGER.debug("LDAP response: [{}]", response);
final List<MessageDescriptor> messageList;
final LdapPasswordPolicyConfiguration ldapPasswordPolicyConfiguration = (LdapPasswordPolicyConfiguration) super.getPasswordPolicyConfiguration();
if (ldapPasswordPolicyConfiguration != null) {
LOGGER.debug("Applying password policy to [{}]", response);
messageList = ldapPasswordPolicyConfiguration.getAccountStateHandler().handle(response, ldapPasswordPolicyConfiguration);
} else {
LOGGER.debug("No ldap password policy configuration is defined");
messageList = Collections.emptyList();
}
if (response.getResult()) {
LOGGER.debug("LDAP response returned a result. Creating the final LDAP principal");
return createHandlerResult(upc, createPrincipal(upc.getUsername(), response.getLdapEntry()), messageList);
}
if (AuthenticationResultCode.DN_RESOLUTION_FAILURE == response.getAuthenticationResultCode()) {
LOGGER.warn("DN resolution failed. [{}]", response.getMessage());
throw new AccountNotFoundException(upc.getUsername() + " not found.");
}
throw new FailedLoginException("Invalid credentials");
}
/**
* Creates a CAS principal with attributes if the LDAP entry contains principal attributes.
*
* @param username Username that was successfully authenticated which is used for principal ID when
* {@link #setPrincipalIdAttribute(String)} is not specified.
* @param ldapEntry LDAP entry that may contain principal attributes.
* @return Principal if the LDAP entry contains at least a principal ID attribute value, null otherwise.
* @throws LoginException On security policy errors related to principal creation.
*/
protected Principal createPrincipal(final String username, final LdapEntry ldapEntry) throws LoginException {
LOGGER.debug("Creating LDAP principal for [{}] based on [{}] and attributes [{}]",
username, ldapEntry.getDn(), ldapEntry.getAttributeNames());
final String id = getLdapPrincipalIdentifier(username, ldapEntry);
final Map<String, Object> attributeMap = new LinkedHashMap<>(this.principalAttributeMap.size());
this.principalAttributeMap.forEach((key, principalAttrName) -> {
final LdapAttribute attr = ldapEntry.getAttribute(key);
if (attr != null) {
LOGGER.debug("Found principal attribute: [{}]", attr);
if (attr.size() > 1) {
LOGGER.debug("Principal attribute: [{}] is multivalued", attr);
attributeMap.put(principalAttrName, attr.getStringValues());
} else {
attributeMap.put(principalAttrName, attr.getStringValue());
}
} else {
LOGGER.warn("Requested LDAP attribute [{}] could not be found on the resolved LDAP entry for [{}]",
key, ldapEntry.getDn());
}
});
final String dnAttribute = getName().concat(".").concat(username);
LOGGER.debug("Recording principal DN attribute as [{}]", dnAttribute);
attributeMap.put(dnAttribute, ldapEntry.getDn());
LOGGER.debug("Created LDAP principal for id [{}] and [{}] attributes", id, attributeMap.size());
return this.principalFactory.createPrincipal(id, attributeMap);
}
/**
* Gets ldap principal identifier. If the principal id attribute is defined, it's retrieved.
* If no attribute value is found, a warning is generated and the provided username is used instead.
* If no attribute is defined, username is used instead.
*
* @param username the username
* @param ldapEntry the ldap entry
* @return the ldap principal identifier
* @throws LoginException in case the principal id cannot be determined.
*/
protected String getLdapPrincipalIdentifier(final String username, final LdapEntry ldapEntry) throws LoginException {
if (StringUtils.isNotBlank(this.principalIdAttribute)) {
final LdapAttribute principalAttr = ldapEntry.getAttribute(this.principalIdAttribute);
if (principalAttr == null || principalAttr.size() == 0) {
if (this.allowMissingPrincipalAttributeValue) {
LOGGER.warn("The principal id attribute [{}] is not found. CAS cannot construct the final authenticated principal "
+ "if it's unable to locate the attribute that is designated as the principal id. "
+ "Attributes available on the LDAP entry are [{}]. Since principal id attribute is not available, CAS will "
+ "fall back to construct the principal based on the provided user id: [{}]",
this.principalIdAttribute, ldapEntry.getAttributes(), username);
return username;
}
LOGGER.error("The principal id attribute [{}] is not found. CAS is configured to disallow missing principal attributes",
this.principalIdAttribute);
throw new LoginException("Principal id attribute is not found for " + principalAttr);
}
if (principalAttr.size() > 1) {
if (!this.allowMultiplePrincipalAttributeValues) {
throw new LoginException("Multiple principal values are not allowed: " + principalAttr);
}
LOGGER.warn("Found multiple values for principal id attribute: [{}]. Using first value=[{}].", principalAttr, principalAttr.getStringValue());
}
LOGGER.debug("Retrieved principal id attribute [{}]", principalAttr.getStringValue());
return principalAttr.getStringValue();
}
LOGGER.debug("Principal id attribute is not defined. Using the default provided user id [{}]", username);
return username;
}
public void setAllowMissingPrincipalAttributeValue(final boolean allowMissingPrincipalAttributeValue) {
this.allowMissingPrincipalAttributeValue = allowMissingPrincipalAttributeValue;
}
/**
* Initialize the handler, setup the authentication entry attributes.
*/
@PostConstruct
public void initialize() {
/*
* Use a set to ensure we ignore duplicates.
*/
final Set<String> attributes = new HashSet<>();
LOGGER.debug("Initializing LDAP attribute configuration...");
if (StringUtils.isNotBlank(this.principalIdAttribute)) {
LOGGER.debug("Configured to retrieve principal id attribute [{}]", this.principalIdAttribute);
attributes.add(this.principalIdAttribute);
}
if (this.principalAttributeMap != null && !this.principalAttributeMap.isEmpty()) {
final Set<String> attrs = this.principalAttributeMap.keySet();
attributes.addAll(attrs);
LOGGER.debug("Configured to retrieve principal attribute collection of [{}]", attrs);
}
if (authenticator.getReturnAttributes() != null) {
final List<String> authenticatorAttributes = Arrays.asList(authenticator.getReturnAttributes());
if (authenticatorAttributes != null && !authenticatorAttributes.isEmpty()) {
LOGGER.debug("Filtering authentication entry attributes [{}] based on authenticator attributes [{}]",
authenticatedEntryAttributes, authenticatorAttributes);
attributes.removeIf(authenticatorAttributes::contains);
}
}
this.authenticatedEntryAttributes = attributes.toArray(new String[attributes.size()]);
LOGGER.debug("LDAP authentication entry attributes for the authentication request are [{}]",
(Object[]) this.authenticatedEntryAttributes);
}
}