Hi Daniel,

Thanks for your response.

Please find the attached source code.


On Thursday, May 6, 2021 at 12:44:22 AM UTC+5:30 dfisher wrote:

> On Wed, May 5, 2021 at 1:03 PM Morning Star <[email protected]> wrote:
>
>> private ConnectionFactory searchFactory; 
>> private AbstractLdapSearchProperties ldapProperties;
>> @Autowired
>> private CasConfigurationProperties casProperties;
>> try{
>> val ldap = casProperties.getAuthn().getLdap();
>> this.ldapProperties = ldap.get(0);
>> this.searchFactory = LdapUtils.newLdaptiveConnectionFactory(ldap.get(0));
>> val filter = 
>> LdapUtils.newLdaptiveSearchFilter(ldapProperties.getSearchFilter(), 
>> LdapUtils.LDAP_SEARCH_FILTER_DEFAULT_PARAM_NAME, 
>> Collections.singletonList(upc.getUsername()));
>> searchResponse = LdapUtils.executeSearchOperation(searchFactory, 
>> ldapProperties.getBaseDn(), filter, this.ldapProperties.getPageSize());
>> }
>> finally
>> {
>> if(searchFactory != null)
>> {
>> *searchFactory.close()*;
>> log.info(CLASS_NAME + METHOD_NAME + "Connection Factory Closed 
>> Successfully");
>> }
>> }
>>
>
> Without seeing the entire class it's hard to say, but there are probably 
> some thread safety issues here. It's likely new connection factories are 
> getting created before close is getting invoked. I'd recommend initializing 
> searchFactory as part of the bean's initialization. Otherwise you should 
> change searchFactory to be a local variable. (It will be fairly inefficient 
> to create a pooled connection factory for each search operation.)
>
> --Daniel Fisher
>

-- 
- Website: https://apereo.github.io/cas
- Gitter Chatroom: https://gitter.im/apereo/cas
- List Guidelines: https://goo.gl/1VRrw7
- Contributions: https://goo.gl/mh7qDG
--- 
You received this message because you are subscribed to the Google Groups "CAS 
Community" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion on the web visit 
https://groups.google.com/a/apereo.org/d/msgid/cas-user/886f693a-beea-4c0c-9f44-2f4c732d89dfn%40apereo.org.
package com.example.sso.authentication;

import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import javax.security.auth.login.AccountNotFoundException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apereo.cas.authentication.AuthenticationHandlerExecutionResult;
import org.apereo.cas.authentication.AuthenticationPasswordPolicyHandlingStrategy;
import org.apereo.cas.authentication.PreventedException;
import org.apereo.cas.authentication.credential.UsernamePasswordCredential;
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.configuration.CasConfigurationProperties;
import org.apereo.cas.configuration.model.support.ldap.AbstractLdapSearchProperties;
import org.apereo.cas.services.ServicesManager;
import org.apereo.cas.util.CollectionUtils;
import org.apereo.cas.util.LdapUtils;
import org.apereo.cas.web.support.WebUtils;
import org.ldaptive.ConnectionFactory;
import org.ldaptive.Credential;
import org.ldaptive.LdapEntry;
import org.ldaptive.LdapException;
import org.ldaptive.ReturnAttributes;
import org.ldaptive.SearchResponse;
import org.ldaptive.auth.AuthenticationRequest;
import org.ldaptive.auth.AuthenticationResponse;
import org.ldaptive.auth.AuthenticationResultCode;
import org.ldaptive.auth.Authenticator;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.google.common.collect.Maps;
import com.example.sso.config.exampleApplicationPropertyConfiguration;
import com.example.sso.exception.AccountLockException;
import com.example.sso.exception.CaptchaAnswerEmptyException;
import com.example.sso.exception.CaptchaAnswerIncorrectException;
import com.example.sso.exception.CaptchaWidgetException;
import com.example.sso.response.CaptchaResponse;
import com.example.sso.service.CaptchaService;
import com.example.sso.service.DataFieldCorelationService;
import com.example.sso.service.UserLockService;

import lombok.Getter;
import lombok.Setter;
import lombok.val;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Setter
@Getter
@Component
public class LdapHandlerAuthentication extends AbstractUsernamePasswordAuthenticationHandler	implements DisposableBean 
{
	
	private final String CLASS_NAME = exampleLdapHandlerAuthentication.class.getSimpleName();

	protected Map<String, Object> principalAttributeMap = new HashMap<>(0);
	private final Authenticator authenticator;
	private String principalIdAttribute;
	private boolean allowMultiplePrincipalAttributeValues;
	private boolean allowMissingPrincipalAttributeValue = true;
	private String[] authenticatedEntryAttributes = ReturnAttributes.NONE.value();
	private boolean collectDnAttribute;
	private AbstractLdapSearchProperties ldapProperties;
	private String principalDnAttributeName = "principalLdapDn";
	private AuthenticationResponse response;
	private ConnectionFactory searchFactory;  //TODO - to close the connection - 03/03/2021
	private SearchResponse searchResponse;
	private CaptchaService captchaService = null;
	private UserLockService userLockService = null;

	@Autowired
	private CasConfigurationProperties casProperties;
	
	@Autowired
	private exampleApplicationPropertyConfiguration  applicationProps;

	@SuppressWarnings("rawtypes")
	public exampleLdapHandlerAuthentication(final String name, final ServicesManager servicesManager,
			final PrincipalFactory principalFactory, final Integer order, final Authenticator authenticator,
			final AuthenticationPasswordPolicyHandlingStrategy strategy) {
		super(name, servicesManager, principalFactory, order);
		this.authenticator = authenticator;
		this.passwordPolicyHandlingStrategy = strategy;
	}

	@Override
	public void destroy() {
		authenticator.close();
	}

	/**
	 * Initialize the handler, setup the authentication entry attributes.
	 */
	public void initialize() 
	{
		String METHOD_NAME = "| initialize() | ";
		val attributes = new HashSet<String>();
		log.info(CLASS_NAME + METHOD_NAME+ "Initializing LDAP attribute configuration...");
		
		if (StringUtils.isNotBlank(this.principalIdAttribute)) 
		{
			log.info(CLASS_NAME + METHOD_NAME + "Configured to retrieve principal id attribute [{}]", this.principalIdAttribute);
			attributes.add(this.principalIdAttribute);
		}
		
		if (this.principalAttributeMap != null && !this.principalAttributeMap.isEmpty()) 
		{
			val attrs = this.principalAttributeMap.keySet();
			attributes.addAll(attrs);
			log.info(CLASS_NAME + METHOD_NAME + "Configured to retrieve principal attribute collection of [{}]", attrs);
		}
		this.authenticatedEntryAttributes = attributes.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
		log.info(CLASS_NAME + METHOD_NAME + "LDAP authentication entry attributes for the authentication request are [{}]", (Object[]) this.authenticatedEntryAttributes);
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	@Override
	protected AuthenticationHandlerExecutionResult authenticateUsernamePasswordInternal(
			final UsernamePasswordCredential upc, final String originalPassword)
					throws GeneralSecurityException, PreventedException {
		String METHOD_NAME = "| authenticateUsernamePasswordInternal(...) | ";
		try
		{
			

			String captchaWidget = "";
			String displayCaptcha = "";
			String status = "";
			boolean isMobileApp = false;
			boolean isValidateCaptcha = false;
			captchaService = new CaptchaService();
			userLockService = new UserLockService();
			CaptchaResponse captchaResponse = null;
			String mobileAppDisplay = "false";
			String answer = null;

			final HttpServletRequest request = WebUtils.getHttpServletRequestFromExternalWebflowContext();

			if(request != null)
			{
				displayCaptcha = request.getParameter("displayCaptcha");
				answer = request.getParameter("nucaptcha-answer");
				mobileAppDisplay = request.getParameter("mobileApp");
			}
			else
			{
				mobileAppDisplay = "true";
			}

			log.info(CLASS_NAME + METHOD_NAME + "Display Captcha : " + displayCaptcha+ ", nuCaptcha Answer : "+ answer + ", mobile App Display : "+mobileAppDisplay );
			if (mobileAppDisplay != null && mobileAppDisplay != "") 
			{
				isMobileApp = Boolean.parseBoolean(mobileAppDisplay);
			} 
			else 
			{
				isMobileApp = true;
			}
			log.info(CLASS_NAME + METHOD_NAME + "Before Backend - Display Captcha : " + displayCaptcha+ ", nuCaptcha Answer : "+ answer + ", mobile App Display : "+mobileAppDisplay );

			try 
			{
				val ldap = casProperties.getAuthn().getLdap();
				this.ldapProperties = ldap.get(0);
				this.searchFactory = LdapUtils.newLdaptiveConnectionFactory(ldap.get(0));
				val filter = LdapUtils.newLdaptiveSearchFilter(ldapProperties.getSearchFilter(), LdapUtils.LDAP_SEARCH_FILTER_DEFAULT_PARAM_NAME, Collections.singletonList(upc.getUsername()));
				searchResponse = LdapUtils.executeSearchOperation(searchFactory, ldapProperties.getBaseDn(), filter, this.ldapProperties.getPageSize());

			} 
			catch (LdapException e) 
			{
				log.error(CLASS_NAME + METHOD_NAME + e.getLocalizedMessage());
				throw new GeneralSecurityException();
			}
			response = getLdapAuthenticationResponse(upc);
			log.info(CLASS_NAME + METHOD_NAME +"LDAP response: [{}]", response);

			if (!isMobileApp) 
			{
				// call to NuCaptcha for rendering and validating the captcha
				captchaResponse = captchaService.renderCaptchaWidget(request, upc, answer,applicationProps);

				if (captchaResponse != null) 
				{
					captchaWidget = captchaResponse.getCaptchaWidget();
					isValidateCaptcha = captchaResponse.isCaptchaValid();
				}

				// If the captcha has been rendered or configured for user,if block executes
				if (captchaWidget != null && captchaWidget != "") 
				{
					if (answer != null && answer != "" && !isValidateCaptcha) 
					{
						request.setAttribute("captcha", captchaWidget);
						throw new CaptchaAnswerIncorrectException("Captcha Answer Incorrect");
					} 
					else 
					{
						if (displayCaptcha != null && displayCaptcha != "" && !isValidateCaptcha) 
						{
							request.setAttribute("captcha", captchaWidget);
							throw new CaptchaAnswerEmptyException("Captcha Answer Empty");
						}

					}
					if (!isValidateCaptcha) 
					{
						request.setAttribute("captcha", captchaWidget);
						throw new CaptchaWidgetException("Captcha Widget");
					}
				}
			}

			if (!passwordPolicyHandlingStrategy.supports(response)) 
			{
				log.warn(CLASS_NAME + METHOD_NAME +"Authentication has failed because LDAP password policy handling strategy [{}] cannot handle [{}].", response, passwordPolicyHandlingStrategy.getClass().getSimpleName());
				status = "invalidPassword";

				if (!isMobileApp) 
				{
					request.setAttribute("captcha", captchaWidget);
					captchaService.updateCaptcha(request, upc, answer, status,applicationProps);
				}
				throw new FailedLoginException("Invalid credentials");
			}
			log.info(CLASS_NAME + METHOD_NAME +"Attempting to examine and handle LDAP password policy via [{}]", passwordPolicyHandlingStrategy.getClass().getSimpleName());
			val messageList = passwordPolicyHandlingStrategy.handle(response, getPasswordPolicyConfiguration());
			if (response.isSuccess()) 
			{
				log.info(CLASS_NAME + METHOD_NAME +"LDAP response returned a result [{}], creating the final LDAP principal", response.getLdapEntry());

				if (userLockService.isUserLock(searchFactory, searchResponse)) 
				{
					log.info(CLASS_NAME + METHOD_NAME + "User is Locked");
					if (!isMobileApp) request.setAttribute("captcha", captchaWidget);
					status = "accountLocked";
					if (!isMobileApp)  captchaService.updateCaptcha(request, upc, answer, status,applicationProps);
					throw new AccountLockException("Customer account is locked");
				}
				if(searchResponse != null 
						&&  searchResponse.getEntry() != null 
						&& searchResponse.getEntry().getAttribute("userStatus") != null 
						&& ("INACTIVE").equalsIgnoreCase(searchResponse.getEntry().getAttribute("userStatus").getStringValue()))
				{
					log.info("MOBILE USER STATUS INACTIVE : " + searchResponse.getEntry().getAttribute("cn").getStringValue());
					DataFieldCorelationService dfcService = new DataFieldCorelationService();
					String tempToken = dfcService.storeMappedValues(searchResponse.getEntry().getAttribute("cn").getStringValue());
					log.info("REGISTRATION_REMINDER token created for :" +searchResponse.getEntry().getAttribute("cn").getStringValue() + " is "+tempToken);
					if (!isMobileApp) 
					{
						request.setAttribute("userStatusInActive", "INACTIVE");
						request.setAttribute("dfcToken", tempToken);
					}
					throw new RuntimeException("User "+tempToken+" is INACTIVE");
				}

				val principal = createPrincipal(upc.getUsername(), response.getLdapEntry());
				status = "success";
				if (!isMobileApp) captchaService.updateCaptcha(request, upc, answer, status,applicationProps);
				return createHandlerResult(upc, principal, messageList);
			}

			if (AuthenticationResultCode.DN_RESOLUTION_FAILURE == response.getAuthenticationResultCode()) 
			{
				log.warn(CLASS_NAME + METHOD_NAME + "DN resolution failed. [{}]", response.getDiagnosticMessage());
				if (!isMobileApp) request.setAttribute("captcha", captchaWidget);
				status = "accountDoesNotExists";
				if (!isMobileApp) captchaService.updateCaptcha(request, upc, answer, status,applicationProps);
				throw new AccountNotFoundException(upc.getUsername() + " not found.");
			}

			if (!isMobileApp) request.setAttribute("captcha", captchaWidget);
			userLockService.increaseUserLockCount(searchFactory, searchResponse, upc);
			status = "invalidPassword";
			if (!isMobileApp) captchaService.updateCaptcha(request, upc, answer, status,applicationProps);
			throw new FailedLoginException("Invalid credentials");
		}
		finally
		{
			if(searchFactory != null)
			{
				searchFactory.close();
				log.info(CLASS_NAME + METHOD_NAME + "Connection Factory Closed Successfully");
			}
		}
	}

	/**
	 * Creates a CAS principal with attributes if the LDAP entry contains principal
	 * attributes.
	 *
	 * @param username  User name that was successfully authenticated which is used
	 *                  for principal ID when principal id 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 {
		log.info("Creating LDAP principal for [{}] based on [{}] and attributes [{}]", username, ldapEntry.getDn(),
				ldapEntry.getAttributeNames());
		val id = getLdapPrincipalIdentifier(username, ldapEntry);
		log.info("LDAP principal identifier created is [{}]", id);
		val attributeMap = collectAttributesForLdapEntry(ldapEntry, id);
		log.info("Created LDAP principal for id [{}] and [{}] attributes", id, attributeMap.size());
		return this.principalFactory.createPrincipal(id, attributeMap);
	}

	/**
	 * Collect attributes for ldap entry.
	 *
	 * @param ldapEntry the ldap entry
	 * @param username  the user name
	 * @return the map
	 */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	protected Map<String, List<Object>> collectAttributesForLdapEntry(final LdapEntry ldapEntry,
			final String username) {
		val attributeMap = Maps.<String, List<Object>>newHashMapWithExpectedSize(this.principalAttributeMap.size());
		log.info("The following attributes are requested to be retrieved and mapped: [{}]", attributeMap.keySet());
		principalAttributeMap.forEach((key, names) -> {
			val attributeNames = CollectionUtils.toCollection(names, ArrayList.class);
			if (attributeNames.size() == 1 && attributeNames.stream().allMatch(s -> s.toString().endsWith(";"))) {
				val attrs = ldapEntry.getAttributes().stream()
						.filter(attr -> attr.getName().startsWith(key.concat(";"))).collect(Collectors.toList());
				attrs.forEach(attr -> attributeMap
						.putAll(collectAttributeValueForEntry(ldapEntry, attr.getName(), List.of())));
			} else {
				attributeMap.putAll(collectAttributeValueForEntry(ldapEntry, key, attributeNames));
			}
		});
		if (this.collectDnAttribute) {
			log.info("Recording principal DN attribute as [{}]", this.principalDnAttributeName);
			attributeMap.put(this.principalDnAttributeName, CollectionUtils.wrapList(ldapEntry.getDn()));
		}
		return 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)) {
			val principalAttr = ldapEntry.getAttribute(this.principalIdAttribute);
			if (principalAttr == null || principalAttr.size() == 0) {
				if (this.allowMissingPrincipalAttributeValue) {
					log.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;
				}
				log.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);
			}
			val value = principalAttr.getStringValue();
			if (principalAttr.size() > 1) {
				if (!this.allowMultiplePrincipalAttributeValues) {
					throw new LoginException("Multiple principal values are not allowed: " + principalAttr);
				}
				log.warn("Found multiple values for principal id attribute: [{}]. Using first value=[{}].",
						principalAttr, value);
			}
			log.info("Retrieved principal id attribute [{}]", value);
			return value;
		}
		log.info("Principal id attribute is not defined. Using the default provided user id [{}]", username);
		return username;
	}

	private AuthenticationResponse getLdapAuthenticationResponse(final UsernamePasswordCredential upc)
			throws PreventedException {
		try {
			log.info(
					"Attempting LDAP authentication for [{}]. Authenticator pre-configured attributes are [{}], "
							+ "additional requested attributes for this authentication request are [{}]",
					upc, authenticator.getReturnAttributes(), authenticatedEntryAttributes);
			var ldaptiveCred = new Credential(upc.getPassword());
			val request = new AuthenticationRequest(upc.getUsername(), ldaptiveCred, authenticatedEntryAttributes);
			return authenticator.authenticate(request);
		} catch (final LdapException e) {
			log.trace(e.getMessage(), e);
			throw new PreventedException(e);
		}
	}

	private static Map<String, List<Object>> collectAttributeValueForEntry(final LdapEntry ldapEntry, final String key,
			final Collection<String> attributeNames) {
		val attributeMap = new HashMap<String, List<Object>>();
		val attr = ldapEntry.getAttribute(key);
		if (attr != null) {
			log.info("Found principal attribute: [{}]", attr);

			if (attributeNames.isEmpty()) {
				log.info("Principal attribute [{}] is collected as [{}]", attr, key);
				attributeMap.put(key, CollectionUtils.wrap(attr.getStringValues()));
			} else {
				attributeNames.forEach(s -> {
					log.info("Principal attribute [{}] is virtually remapped/renamed to [{}]", attr, s);
					attributeMap.put(s, CollectionUtils.wrap(attr.getStringValues()));
				});
			}
		} else {
			log.warn("Requested LDAP attribute [{}] could not be found on LDAP entry for [{}]", key, ldapEntry.getDn());
		}
		return attributeMap;
	}
	
}

Reply via email to