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 |
[email protected]
-----Original Message-----
From: Emmanuel Lécharny [mailto:[email protected]]
Sent: Wednesday, December 6, 2017 9:51 AM
To: [email protected]
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");
}
}