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;
}
}