/* * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work * for additional information regarding copyright ownership. * Jasig 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 the following location: * * 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.jasig.cas.authentication; import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.annotation.PostConstruct; import javax.security.auth.login.AccountNotFoundException; import javax.security.auth.login.FailedLoginException; import javax.security.auth.login.LoginException; import javax.validation.constraints.NotNull; import org.jasig.cas.Message; import org.jasig.cas.authentication.handler.support.AbstractUsernamePasswordAuthenticationHandler; import org.jasig.cas.authentication.principal.Principal; import org.jasig.cas.authentication.principal.SimplePrincipal; import org.jasig.cas.authentication.support.LdapPasswordPolicyConfiguration; import org.ldaptive.LdapAttribute; import org.ldaptive.LdapEntry; import org.ldaptive.LdapException; import org.ldaptive.auth.AuthenticationRequest; import org.ldaptive.auth.AuthenticationResponse; import org.ldaptive.auth.AuthenticationResultCode; import org.ldaptive.auth.Authenticator; /** * LDAP authentication handler that uses the ldaptive <code>Authenticator</code> 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> * <ol>{@link #setPrincipalIdAttribute(String)}</ol> * <ol>{@link #setPrincipalAttributeMap(java.util.Map)}</ol> * </ul> * * @author Marvin S. Addison * @since 4.0 */ public class LdapAuthenticationHandler extends AbstractUsernamePasswordAuthenticationHandler { /** Performs LDAP authentication given username/password. */ @NotNull private final Authenticator authenticator; /** Component name. */ @NotNull private String name = LdapAuthenticationHandler.class.getSimpleName(); /** Name of attribute to be used for resolved principal. */ private String principalIdAttribute; /** Flag indicating whether multiple values are allowed fo principalIdAttribute. */ private boolean allowMultiplePrincipalAttributeValues = false; /** Mapping of LDAP attribute name to principal attribute name. */ @NotNull protected Map<String, String> principalAttributeMap = Collections.emptyMap(); /** List of additional attributes to be fetched but are not principal attributes. */ @NotNull protected List<String> additionalAttributes = Collections.emptyList(); /** Set of LDAP attributes fetch from an entry as part of the authentication process. */ private String[] authenticatedEntryAttributes; /** * Creates a new authentication handler that delegates to the given authenticator. * * @param authenticator Ldaptive authenticator component. */ public LdapAuthenticationHandler(@NotNull final Authenticator authenticator) { this.authenticator = authenticator; } /** * Sets the component name. Defaults to simple class name. * * @param name Authentication handler name. */ public void setName(final String name) { this.name = name; } /** * 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 list of additional attributes to be fetched from the user entry during authentication. * These attributes are <em>not</em> bound to the principal. * <p> * A common use case for these attributes is to support password policy machinery. * * @param additionalAttributes List of operational attributes to fetch when resolving an entry. */ public void setAdditionalAttributes(final List<String> additionalAttributes) { this.additionalAttributes = additionalAttributes; } @Override protected HandlerResult authenticateUsernamePasswordInternal(final UsernamePasswordCredential upc) throws GeneralSecurityException, PreventedException { final AuthenticationResponse response; try { logger.debug("Attempting LDAP authentication for {}", upc); final AuthenticationRequest request = new AuthenticationRequest(upc.getUsername(), new org.ldaptive.Credential(upc.getPassword()), this.authenticatedEntryAttributes); response = this.authenticator.authenticate(request); } catch (final LdapException e) { throw new PreventedException("Unexpected LDAP error", e); } logger.debug("LDAP response: {}", response); final List<Message> messageList; final LdapPasswordPolicyConfiguration ldapPasswordPolicyConfiguration = (LdapPasswordPolicyConfiguration) super.getPasswordPolicyConfiguration(); if (ldapPasswordPolicyConfiguration != null) { logger.debug("Applying password policy to {}", response); messageList = ldapPasswordPolicyConfiguration.getAccountStateHandler().handle( response, ldapPasswordPolicyConfiguration); } else { messageList = Collections.emptyList(); } if (response.getResult()) { return createHandlerResult(upc, createPrincipal(upc.getUsername(), response.getLdapEntry()), messageList); } if (AuthenticationResultCode.DN_RESOLUTION_FAILURE == response.getAuthenticationResultCode()) { throw new AccountNotFoundException(upc.getUsername() + " not found."); } throw new FailedLoginException("Invalid credentials."); } @Override public boolean supports(final Credential credential) { return credential instanceof UsernamePasswordCredential; } @Override public String getName() { return this.name; } /** * 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 { final String id; if (this.principalIdAttribute != null) { final LdapAttribute principalAttr = ldapEntry.getAttribute(this.principalIdAttribute); if (principalAttr == null || principalAttr.size() == 0) { throw new LoginException(this.principalIdAttribute + " attribute not found for " + username); } if (principalAttr.size() > 1) { if (this.allowMultiplePrincipalAttributeValues) { logger.warn( "Found multiple values for principal ID attribute: {}. Using first value={}.", principalAttr, principalAttr.getStringValue()); } else { throw new LoginException("Multiple principal values not allowed: " + principalAttr); } } id = principalAttr.getStringValue(); } else { id = username; } final Map<String, Object> attributeMap = new LinkedHashMap<String, Object>(this.principalAttributeMap.size()); for (String ldapAttrName : this.principalAttributeMap.keySet()) { final LdapAttribute attr = ldapEntry.getAttribute(ldapAttrName); if (attr != null) { logger.debug("Found principal attribute: {}", attr); final String principalAttrName = this.principalAttributeMap.get(ldapAttrName); if (attr.size() > 1) { attributeMap.put(principalAttrName, attr.getStringValues()); } else { attributeMap.put(principalAttrName, attr.getStringValue()); } } } return new SimplePrincipal(id, attributeMap); } @PostConstruct public void initialize() { final List<String> attributes = new ArrayList<String>(); if (this.principalIdAttribute != null) { attributes.add(this.principalIdAttribute); } attributes.addAll(this.principalAttributeMap.keySet()); attributes.addAll(this.additionalAttributes); this.authenticatedEntryAttributes = attributes.toArray(new String[attributes.size()]); } }