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

}

Reply via email to