My desire was to keep all standard features of the default authenticator, allow it to run first to perform the standard validations then run my custom validation after. To do this I created my custom authentication interceptor which extends AuthenticationInterceptor, then I updated the list of interceptors and changed the class name for the AuthenticationInterceptor from the standard class to my custom class. Attached is the source code for my custom authenticator. For now I will try to move the call to super just after I perform my status check and see if that works but if you have a better solution I would be happy to hear it. If you have any other feedback on this custom authenticator (problems I may be causing for myself that I am not aware of) please let me know as well.
Thanks, Justin Isenhour | Lead Developer, Systems and Technology Group | Compass Group USA | 2400 Yorkmont Road | Charlotte, NC 28217 | 704.328.5804 | justin.isenh...@compass-usa.com -----Original Message----- From: Emmanuel Lécharny [mailto:elecha...@gmail.com] Sent: Wednesday, December 6, 2017 9:51 AM To: users@directory.apache.org Subject: [Ext] Re: [ApacheDS] How to clear cached authentication on change of custom attribute Le 06/12/2017 à 14:16, Isenhour, Justin a écrit : > We have a use case where we need to have a custom status attribute for user > identities. We also have created a custom authentication interceptor that > will check the status attribute on bind, depending on the status we will > throw a LdapAuthenticationException and report the status in the message. > Our SSO solution is then using this during the authentication process. This > is all working as needed. The issue we run into is related to the caching > policies within ApacheDS. My guess is that you are talking about the credentials cache, right ? The first time a user identity attempts to login into our SSO application the bind event is triggered and the status is checked, after that the result of the bind is cached,the next time the user logs in the bind event is not triggered,because of this if the users status is changed after they have logged in then that new status is not reported until the cache clears. After reviewing the ApacheDS code I see there is some logic within ApacheDS to remove the user object from cache when the users password is changed, is there a way to also do this for a custom attribute like we have for status either through configuration or through custom code? If we have to we will set the expectation with our customers that any changes to status could take up to x amount of time to take effect but I would prefer to have these changes be real time if possible. Also what is the caching time for authentication and does it use sliding expiration? Thank you in advance. There is a public invalidateCache() method declared in the Authenticator interface and implemented in the SimpleAuthenticator. The thing is that you have implemented your how interceptor, but still have the standard autheticationInterceptor running, which means when a BindRequest is proceced, this standard interceptor will kick in and use the cached credentials. If you disable the standard interceptor, that should work as you intend it to wrk, assuming you keep the features this standard interceptor brings. What you should also try to do is to move your interceptor *before* the default authn interceptor, so that it's called first, and you can check the status beforehand. I would need a bit more information on how your interceptor is working to give you more precise directions, though... -- Emmanuel Lecharny Symas.com directory.apache.org
package com.cga.aaims.ldap.apacheds.interceptor; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.SimpleTimeZone; import org.apache.directory.api.ldap.model.constants.SchemaConstants; import org.apache.directory.api.ldap.model.entry.Attribute; import org.apache.directory.api.ldap.model.entry.DefaultAttribute; import org.apache.directory.api.ldap.model.entry.DefaultModification; import org.apache.directory.api.ldap.model.entry.Entry; import org.apache.directory.api.ldap.model.entry.Modification; import org.apache.directory.api.ldap.model.entry.ModificationOperation; import org.apache.directory.api.ldap.model.exception.LdapAuthenticationException; import org.apache.directory.api.ldap.model.exception.LdapException; import org.apache.directory.api.ldap.model.name.Dn; import org.apache.directory.api.ldap.model.schema.AttributeType; import org.apache.directory.server.core.api.CoreSession; import org.apache.directory.server.core.api.DirectoryService; import org.apache.directory.server.core.api.entry.ClonedServerEntry; import org.apache.directory.server.core.api.interceptor.context.AddOperationContext; import org.apache.directory.server.core.api.interceptor.context.BindOperationContext; import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext; import org.apache.directory.server.core.authn.AuthenticationInterceptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.cga.aaims.ldap.apacheds.AAIMSSchemaConstants; import com.cga.aaims.ldap.apacheds.Status; /** * Custom ApacheDS interceptor designed to bridge the gaps between the out-of-the-box * features/functionality of ApacheDS and the business requirements of AAIMS. * * <br><br> * * <b>Add Operation:</b><br> * On add we will check to see if the user object already has the status attribute, * if not then we will add it with the default value of active. * If it does exist then we will leave it as is and move on. * * <br><br> * * <b>Bind Operation:</b><br> * On bind we will perform 3 different customs actions:<br> * <ol> * <li> * <b>Check User Status:</b><br> * Look for the status attribute and check to see if it is active or not. * If active the user will be allowed, if not active then we will throw an LdapAuthenticationException. * </li> * <li> * <b>Check Must Change Password Flag:</b><br> * Check the pwdReset attribute to see if the user is required to reset their password or not. * A value of true will force the user to go through the password reset process before they can * successfully authenticate again. * </li> * <li> * <b>Update Last Login Date:</b><br> * Set the lastLogonDate attribute for the user to the current time. This attribute can then be used in * audit process to disable user accounts that are not used within a certain time frame. * </li> * </ol> * * * @author Justin Isenhour * */ public class AAIMSAuthenticationInterceptor extends AuthenticationInterceptor { private static final Logger LOGGER = LoggerFactory.getLogger(AAIMSAuthenticationInterceptor.class); /** Admin session used for making modifications to the entry during authentication **/ private CoreSession adminSession; /** * Initialize the interceptor and sets up an admin session that can be used in other * event handlers to make modification to the entry. */ @Override public void init( DirectoryService directoryService ) throws LdapException { adminSession = directoryService.getAdminSession(); super.init( directoryService ); } /** * Intercepts the add operation in order to add the status attribute if it doesn't already exist. */ @Override public void add(AddOperationContext addContext) throws LdapException { LOGGER.debug("Intercepting add operation"); ClonedServerEntry entry = (ClonedServerEntry) addContext.getEntry(); Attribute uIdAT = entry.get(SchemaConstants.UID_AT); boolean isStgBasicAccountObject = entry.hasObjectClass(AAIMSSchemaConstants.STG_BASIC_ACCOUNT_OBJECT_CLASS); if (isStgBasicAccountObject && null != uIdAT) { String uId = uIdAT.getString(); //set the status attribute value if needed setStatusAttribute(uId, entry); } else { LOGGER.debug("This is not a user object or not one that extends stgBasicAccount, ignoring"); } super.add(addContext); } /** * * * @param uId * @param entry * @throws LdapException */ private void setStatusAttribute(String uId, ClonedServerEntry entry) throws LdapException { LOGGER.debug("Attempting to add status attribute to uId {}", uId); AttributeType statusAT; Attribute statusAttribute; statusAT = schemaManager.lookupAttributeTypeRegistry(AAIMSSchemaConstants.STATUS_AT); if (entry.get(statusAT) == null) { LOGGER.debug("Status was null, defaulting to active"); statusAttribute = new DefaultAttribute(statusAT); statusAttribute.add(Status.ACTIVE.status()); entry.add(statusAttribute); } else { statusAttribute = entry.get(statusAT); String status = statusAttribute.getString(); LOGGER.debug("Status attribute for {} has already been set to {}, leaving it alone", uId, status); } } /** * Intercepts the bind operation to check to see if the users account status it active or not. */ @Override public void bind(BindOperationContext bindContext) throws LdapException { LOGGER.info("Intercepting bind operation"); LOGGER.info("Executing parent level bind events first"); super.bind(bindContext); LOGGER.info("Executing custom bind event"); Entry entry = bindContext.getEntry(); Attribute uIdAT = entry.get(SchemaConstants.UID_AT); String uId = uIdAT.getString(); boolean isStgBasicAccountObject = entry.hasObjectClass(AAIMSSchemaConstants.STG_BASIC_ACCOUNT_OBJECT_CLASS); if (isStgBasicAccountObject && null != uIdAT) { checkUserStatus(uId, entry); checkMustChangePasswordFlag(uId, entry); try { setLastLogonAttribute(bindContext, uId, entry); } catch (Exception e) { LOGGER.error("Error setting last logon time for {}", uId, e); } } LOGGER.info("Done with custom bind action, calling next operation"); next(bindContext); } /** * Will attempt to get the status attribute for the LDAP object. * If the attribute is not present then this logic will be ignored. * If it is present and the value of it is anything other than active * then we will throw and LdapExecption. * * @param uId - user id of the LDAP account * @param entry - LDAP entry object being evaluated * @throws LdapException Account is not active */ private void checkUserStatus(String uId, Entry entry) throws LdapException { LOGGER.info("Attempting to validate status attribute for uId {}", uId); AttributeType statusAT; Attribute statusAttribute; statusAT = schemaManager.lookupAttributeTypeRegistry(AAIMSSchemaConstants.STATUS_AT); if (entry.get(statusAT) != null) { statusAttribute = entry.get(statusAT); String status = statusAttribute.getString(); LOGGER.info("Status for {} is {}", uId, status); if (!Status.ACTIVE.status().equalsIgnoreCase(status)) { throw new LdapAuthenticationException("Account is not active"); } } else { LOGGER.info("No status attribute was found for {}, continuing", uId); } } /** * Will attempt to get the pwdReset attribute for the LDAP object. * If the attribute is not present then this logic will be ignored. * If it is present and the value of it is true then we will throw * an LdapExecption. * * @param uId - user id of the LDAP account * @param entry - LDAP entry object being evaluated * @throws LdapException User must change password */ private void checkMustChangePasswordFlag(String uId, Entry entry) throws LdapException { LOGGER.info("Attempting to validate pwdReset attribute for uId {}", uId); AttributeType pwdResetAT; Attribute pwdResetAttribute; pwdResetAT = schemaManager.lookupAttributeTypeRegistry(SchemaConstants.PWD_RESET_AT); if (entry.get(pwdResetAT) != null) { pwdResetAttribute = entry.get(pwdResetAT); String pwdReset = pwdResetAttribute.getString(); LOGGER.info("pwdReset for {} is {}", uId, pwdReset); if (Boolean.valueOf(pwdReset)) { throw new LdapAuthenticationException("User must change password"); } } else { LOGGER.info("No pwdReset attribute was found for {}, continuing", uId); } } /** * Will attempt to set the lastLogon attribute for the user to the current time * * @param uId * @param entry * @throws Exception */ private void setLastLogonAttribute(BindOperationContext bindContext, String uId, Entry entry) throws Exception { LOGGER.info("Attempting to set lastLogon attribute for uId {}", uId); Dn bindDn = bindContext.getDn(); List<Modification> mods = new ArrayList<Modification>(); AttributeType lastLogonAT; Attribute lastLogonAttribute; SimpleDateFormat dateFormat = new SimpleDateFormat(AAIMSSchemaConstants.DATE_FORMAT); dateFormat.setTimeZone(new SimpleTimeZone(SimpleTimeZone.UTC_TIME, "UTC")); String currentTime = dateFormat.format(new Date()); lastLogonAT = schemaManager.lookupAttributeTypeRegistry(AAIMSSchemaConstants.LAST_LOGON_AT); lastLogonAttribute = new DefaultAttribute(lastLogonAT); lastLogonAttribute.add(currentTime); Modification lastLogonTimeMod = new DefaultModification(ModificationOperation.REPLACE_ATTRIBUTE, lastLogonAT, currentTime); mods.add(lastLogonTimeMod); Attribute attrMods = lastLogonTimeMod.getAttribute(); attrMods.getAttributeType(); ModifyOperationContext bindModCtx = new ModifyOperationContext(adminSession); bindModCtx.setDn(bindDn); bindModCtx.setEntry(entry); bindModCtx.setModItems(mods); bindModCtx.setPushToEvtInterceptor(true); directoryService.getPartitionNexus().modify(bindModCtx); LOGGER.info("lastLogon should be set now"); } }