Sometimes you need a report of when each user last logged in to AEM. Many of the approaches published online do not work; the one below does, and it is tested on AEM 6.5.
Approach
An AuthenticationInfoPostProcessor service records the last login on each user's profile, and an ACS AEM Commons report reads it.
Step 1: deploy a service that records the last login
This Java class captures the user's last login and updates the user node. Adjust the conditions to your project's requirements.
package yourpackage.core.services;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.jcr.Session;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.UserManager;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.auth.core.spi.AuthenticationInfo;
import org.apache.sling.auth.core.spi.AuthenticationInfoPostProcessor;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component(name = "UserProfileService", service = AuthenticationInfoPostProcessor.class, immediate = true)
public class UserProfileService implements AuthenticationInfoPostProcessor {
/**
* Records the last login time on the user's profile.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(UserProfileService.class);
@Reference
private ResourceResolverFactory resourceResolverFactory;
@Override
public void postProcess(AuthenticationInfo info, HttpServletRequest request, HttpServletResponse response)
throws LoginException {
/*
* The user's last login is their last active time in AEM.
* Skip requests that should not update it, e.g. the logout request.
* Update these conditions for your project.
*/
if ((info != null && info.getAuthType() == null) || (request != null && request.getServletPath() != null
&& (/* your condition 1 */))) {
LOGGER.debug("AuthenticationInfo is null, skipping post-processing for this request.");
return;
}
ResourceResolver resourceResolver = null;
Session session = null;
UserManager userManager = null;
Authorizable auth = null;
try {
resourceResolver = resourceResolverFactory.getResourceResolver(info);
session = resourceResolver.adaptTo(Session.class);
userManager = resourceResolver.adaptTo(UserManager.class);
auth = userManager.getAuthorizable(session.getUserID());
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
// Anonymous users need not be recorded
if (auth.getID() != null && (/* your condition 2 */)) {
LOGGER.info("Recording user login");
// The profile gets a new property
auth.setProperty("profile/lastLoggedIn", session.getValueFactory().createValue(sdf.format(new Date())));
session.save();
session.logout();
}
} catch (Exception exception) {
LOGGER.error("Could not record the last login", exception);
}
}
}
The conditions used here were:
// your condition 1
!request.getServletPath().equals("/system/sling/logout.html")
// your condition 2
!auth.getID().equals("anonymous")
Step 2: build the report in ACS AEM Commons
In ACS AEM Commons Reports, create a new report with a JCR-SQL2 query:

SELECT * FROM [rep:User] AS nodes WHERE ISDESCENDANTNODE("/home/users")
AND nodes.[profile/lastLoggedIn] IS NOT NULL
AND NOT ISDESCENDANTNODE([/home/users/community])
AND NOT ISDESCENDANTNODE([/home/users/mac])
AND NOT ISDESCENDANTNODE([/home/users/rep:policy])
AND NOT ISDESCENDANTNODE([/home/users/screens])
AND NOT ISDESCENDANTNODE([/home/users/system])
Configure the report columns like this:


The report now lists each user's last login:
