Sharing a solution approach on the following use case:
Reference: http://www.wemblog.com/2013/03/how-to-create-custom-authentication.html
The following parts combine to form the solution.
- The usual out of the box facility to configure LDAP authentication
- A pluggable Login module and Authentication handler that works on specific request method (POST) and user type (external users).
- The handler's extractCredentials method is used to do the authentication
- Its authenticationSucceeded method is used to transform the AEM generated login-token from session to persistent cookie based on keep-me-logged-in. Then, the user is redirected to the resource (page) he/she original requested for.
- A content structure with appropriate CUG configurations
- A login page that submits the relevant parameters
Login page
At its basic form, it could look as shown below.
Its rendering code is inspired from the author instance login screen, with additional input fields needed for the use case – the radio buttons for type of user, the checkbox for keep- me-logged-in feature. One can put the entire presentation logic in a login page component or put it in a login component which is hardwired into the login page component.
a) /apps/myapp/components/pages/login-page/login-page.html
<body>
<div data-sly-resource="${@path='login', resourceType='myapp/components/user/login'}" data-sly-unwrap></div>
</body>
</html>
b) /apps/myapp/components/user/login/login.jsp
contentType="text/html"
pageEncoding="utf-8"
import="com.adobe.granite.xss.XSSAPI,
org.apache.sling.auth.core.AuthUtil"%>
<%@taglib prefix="sling" uri="http://sling.apache.org/taglibs/sling/1.0"%>
<%@ taglib prefix="ui" uri="http://www.adobe.com/taglibs/granite/ui/1.0" %>
<sling:defineObjects />
<%
final XSSAPI xssAPI = sling.getService(XSSAPI.class).getRequestSpecificAPI(slingRequest);
final String contextPath = slingRequest.getContextPath();
String redirect = request.getParameter("resource");
if (redirect == null || !AuthUtil.isRedirectValid(request, redirect)) {
redirect = "/";
}
if (!redirect.startsWith(contextPath)) {
redirect = contextPath + redirect;
}
String urlLogin = request.getContextPath() + resource.getPath() + ".html/j_security_check";
%>
<form method="post" action="<%= xssAPI.getValidHref(urlLogin) %>" novalidate="novalidate">
<label for="j_username">Username</label> <input type="text" name="j_username"><br>
<label for="j_password">Password</label> <input type="password" name="j_password"><br>
<input type="radio" name="userType" value="AD" checked> AD
<input type="radio" name="userType" value="CODS"> CODS <br>
<input type="checkbox" name="keepMeLoggedIn" value="true" checked> Keep me logged in<br>
<input type="hidden" name="resource" id="resource" value="<%= xssAPI.encodeForHTMLAttr(redirect) %>"/>
<button type="submit">Login</button>
</form>
Sample content structure
|- mysite-bp* (blueprint)
|- en
|- some-page
|- mysite-country (live copy)
|- en
|- some-page
|- signin
* The following CUG related configuration is done at site root. It can be done at the blueprint level and live copies will inherit them automatically.
- CUG enabled for groups "contributor,administrators"
- cugLoginPage property set to /content/signin – a page created with the login page template discussed in the previous point
Login Module and Authentication handler
a) Login Module
import java.security.Principal;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import javax.jcr.Credentials;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.security.auth.callback.CallbackHandler;
import org.apache.sling.jcr.jackrabbit.server.security.AuthenticationPlugin;
import org.apache.sling.jcr.jackrabbit.server.security.LoginModulePlugin;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceRegistration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SASLoginModule implements LoginModulePlugin{
private static final Logger LOGGER = LoggerFactory.getLogger(SASLoginModule.class);
private final SASAuthnHandler authHandler;
static ServiceRegistration register(
final SASAuthnHandler authHandler,
final BundleContext bundleContext) {
LOGGER.debug("SASLoginModule : register : start");
SASLoginModule plugin = new SASLoginModule(
authHandler);
Hashtable<String, Object> properties = new Hashtable<String, Object>();
properties.put(Constants.SERVICE_DESCRIPTION,
"SAS LoginModule plugin ");
properties.put(Constants.SERVICE_VENDOR, "TCS");
LOGGER.debug("SASLoginModule : register : before registering");
return bundleContext.registerService(LoginModulePlugin.class.getName(),
plugin, properties);
}
private SASLoginModule(
final SASAuthnHandler authHandler) {
LOGGER.debug("SASLoginModule : SASLoginModule : constructor");
this.authHandler = authHandler;
}
@SuppressWarnings("unchecked")
public void doInit(final CallbackHandler callbackHandler,
final Session session, final Map options) {
return;
}
public boolean canHandle(Credentials credentials) {
LOGGER.debug("SASLoginModule : canHandle : start");
return authHandler.canHandle(credentials);
}
public AuthenticationPlugin getAuthentication(final Principal principal,
final Credentials creds) {
LOGGER.debug("SASLoginModule : getAuthentication : start");
return new AuthenticationPlugin() {
public boolean authenticate(Credentials credentials)
throws RepositoryException {
return authHandler.authenticate(credentials);
}
};
}
public Principal getPrincipal(final Credentials credentials) {
LOGGER.debug("SASLoginModule : getPrincipal : start");
return null;
}
@SuppressWarnings("unchecked")
public void addPrincipals(final Set principals) {
LOGGER.debug("SASLoginModule : addPrincipals : start");
}
public int impersonate(final Principal principal,
final Credentials credentials) {
LOGGER.debug("SASLoginModule : impersonate : start");
return LoginModulePlugin.IMPERSONATION_DEFAULT;
}
}
b) Authentication Handler that will authenticate against external user base
import java.io.IOException;
import java.util.Dictionary;
import java.util.UUID;
import javax.jcr.Credentials;
import javax.jcr.RepositoryException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.auth.core.spi.AuthenticationHandler;
import org.apache.sling.auth.core.spi.AuthenticationInfo;
import org.apache.sling.auth.core.spi.DefaultAuthenticationFeedbackHandler;
import org.apache.sling.jcr.api.SlingRepository;
import org.apache.sling.settings.SlingSettingsService;
import org.osgi.framework.ServiceRegistration;
import org.osgi.framework.Constants;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.day.crx.security.token.TokenCookie;
import com.day.crx.security.token.TokenUtil;
@Component(metatype = true, immediate = true, label = "SAS Authentication Handler", description = "Custom Authentication handler for SAS")
@Service
@Properties({
@Property(name = AuthenticationHandler.PATH_PROPERTY, value = {
"/content"
}),
@Property(name = Constants.SERVICE_RANKING, intValue = 10000),
@Property(name = Constants.SERVICE_DESCRIPTION, value = "SAS Authentication Handler")
})
public class SASAuthnHandler extends DefaultAuthenticationFeedbackHandler
implements AuthenticationHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(SASAuthnHandler.class);
private static final String REPO_DESC_ID = "crx.repository.systemid";
private static final String REPO_DESC_CLUSTER_ID = "crx.cluster.id";
// Standard params expected to support OotB login mechanism
private static final String REQUEST_METHOD = "POST";
private static final String USERNAME = "j_username";
private static final String PASSWORD = "j_password";
private static final String REQUEST_URL_SUFFIX = "/j_security_check";
// Custom params expected to support extra features
private static final String KEEP_ME_LOGGED_IN = "keepMeLoggedIn";
private static final String USERTYPE = "userType";
private ServiceRegistration loginModule;
private String repositoryId;
@Reference
private SlingRepository slingRepository;
@Reference
private SlingSettingsService slingSettings;
@Reference
private CODSService codsService;
@Activate
protected void activate(ComponentContext componentContext) {
LOGGER.debug("SASAuthnHandler : activate");
this.repositoryId = slingRepository.getDescriptor(REPO_DESC_CLUSTER_ID);
if (this.repositoryId == null || this.repositoryId.equalsIgnoreCase("")) {
this.repositoryId = slingRepository.getDescriptor(REPO_DESC_ID);
}
if (this.repositoryId == null || this.repositoryId.equalsIgnoreCase("")) {
this.repositoryId = slingSettings.getSlingId();
}
if (this.repositoryId == null || this.repositoryId.equalsIgnoreCase("")) {
this.repositoryId = UUID.randomUUID().toString();
LOGGER.warn("SASAuthnHandler : activate : Unable to get Repository ID; falling back to a random UUID.");
}
configure(componentContext.getProperties());
this.loginModule = null;
try {
this.loginModule = SASLoginModule.register(this,
componentContext.getBundleContext());
LOGGER.debug("SASAuthnHandler : activate : Registered SAS Login Module");
} catch (Throwable t) {
LOGGER.error("SASAuthnHandler : activate : Error in activation", t);
}
}
@Deactivate
protected void deactivate(
@SuppressWarnings("unused") ComponentContext componentContext) {
LOGGER.debug("SASAuthnHandler : deactivate");
if (loginModule != null) {
loginModule.unregister();
LOGGER.debug("SASAuthnHandler : deactivate : Unregistered SAS Login Module");
loginModule = null;
}
}
protected void configure(Dictionary < ? , ? > properties) {
}
public void dropCredentials(HttpServletRequest request,
HttpServletResponse response) {
LOGGER.debug("SASAuthnHandler : dropCredentials : start");
// Remove the CRX Login Token cookie from the request
TokenCookie.update(request, response, this.repositoryId, null, null, true);
}
public AuthenticationInfo extractCredentials(HttpServletRequest request,
HttpServletResponse response) {
LOGGER.debug("SASAuthnHandler : extractCredentials : start : " + request.getRequestURI());
final String usertype = request.getParameter(USERTYPE);
// Check if the request should be acted on
if (!REQUEST_METHOD.equals(request.getMethod()) || !request.getRequestURI().endsWith(REQUEST_URL_SUFFIX) || usertype == null) {
LOGGER.debug("SASAuthnHandler : extractCredentials : request ignored");
return null;
}
final String username = request.getParameter(USERNAME);
final String password = request.getParameter(PASSWORD);
// Check for mandatory params, although these should be validated in front-end
if (username == null || username.length() == 0 || password == null || password.length() == 0) {
LOGGER.debug("SASAuthnHandler : extractCredentials : mandatory params missing, returning fail auth");
return AuthenticationInfo.FAIL_AUTH;
}
if (!usertype.equalsIgnoreCase("CODS")) {
LOGGER.debug("SASAuthnHandler : extractCredentials : usertype not CODS");
return null;
}
LOGGER.info("SASAuthnHandler : extractCredentials : proceeding with authentication of [{}]", username);
// Authenticate against CODS through a service facade
boolean authenticated = false;
try {
authenticated = codsService.authenticate(username, password);
} catch (CODSServiceException e) {
LOGGER.error("SASAuthnHandler : extractCredentials : error authenticating against CODS : ", e);
}
if (authenticated) {
LOGGER.info("SASAuthnHandler : extractCredentials : user [{}] authenticated externally", username);
try {
LOGGER.debug("SASAuthnHandler : extractCredentials : create token and return");
AuthenticationInfo authnInfo = TokenUtil.createCredentials(request, response, slingRepository, username, true);
return authnInfo;
} catch (RepositoryException e) {
LOGGER.error("SASAuthnHandler : extractCredentials : Repository error authenticating user: {} ~> {}", username, e);
} finally {
}
}
LOGGER.debug("SASAuthnHandler : extractCredentials : CODS user not authenticated, returning fail auth");
return AuthenticationInfo.FAIL_AUTH;
}
public boolean requestCredentials(HttpServletRequest arg0,
HttpServletResponse arg1) {
LOGGER.debug("SASAuthnHandler : requestCredentials : start");
return false;
}
public boolean canHandle(Credentials credentials) {
LOGGER.debug("SASAuthnHandler : canHandle : start");
return true;
}
public boolean authenticate(Credentials credentials) {
LOGGER.debug("SASAuthnHandler : authenticate : start");
return true;
}
@Override
public boolean authenticationSucceeded(HttpServletRequest request, HttpServletResponse response, AuthenticationInfo authInfo) {
LOGGER.debug("SASAuthnHandler : authenticationSucceeded : start");
// Change the persistence mechanism
if (request.getParameter(KEEP_ME_LOGGED_IN) != null && request.getParameter(KEEP_ME_LOGGED_IN).equalsIgnoreCase("true")) {
LOGGER.info("SASAuthnHandler : authenticationSucceeded : keep the user logged-in");
TokenCookie tokenCookie = TokenCookie.fromRequest(request);
if (tokenCookie != null) {
LOGGER.info("SASAuthnHandler : authenticationSucceeded : token : " + tokenCookie.toString());
TokenCookie.setCookie(response, "login-token", tokenCookie.toString(), 63072000, "/", request.getServerName(), true, false);
}
}
// Redirect to resource
String resource = request.getParameter("resource");
LOGGER.info("SASAuthnHandler : authenticationSucceeded : resource = " + resource);
try {
response.sendRedirect(resource);
} catch (IOException e) {
LOGGER.error("SASAuthnHandler : authenticationSucceeded : send redirect", e);
}
return true;
}
}

provider.name: Provide a suitable name
For the following, seek help from your AD admin:-


Use the identity provider and the synch handler names which were used in the previous 2 steps. It’s best to let the JAAS ranking remain at its default value.
http://publish-host:port/system/console/slinglog
Click on Add new logger, and add a logger with the following detail
LDAP Configuration
1) Apache Jackrabbit Oak LDAP Identity Provider
provider.name: Provide a suitable name
For the following, seek help from your AD admin:-
- host.name
- host.port
- bind.dn: A service/system user account that will be used to connect to AD
- bind.password:
- user.baseDN: The hierarchy under which the users exist in AD
- user.idAttribute: The attribute that provides the unique userid for each user account
2) Apache Jackrabbit Oak Default Sync Handler
- handler.name: Provide a suitable name
- user.autoMembership: Provide a group to which the root page(s) in the protected site has been granted access to using the CUG option. This group needs to be available in the AEM server, although it is not necessary to assign any permission to it for the CUG mechanism to work
- user.propertyMapping: as-per-requirement
- user.pathPrefix: Provide a path under /home/users under which these set of users will be created upon synch
3) Apache Jackrabbit Oak External Login Module
Use the identity provider and the synch handler names which were used in the previous 2 steps. It’s best to let the JAAS ranking remain at its default value.
4) (OPTIONAL step) Add logging for LDAP:
http://publish-host:port/system/console/slinglog
Click on Add new logger, and add a logger with the following detail
- Log Level: DEBUG
- Log file: logs\error.log
- Logger:
- org.apache.jackrabbit.oak.spi.security.authentication.external
- org.apache.jackrabbit.oak.security.authentication.ldap
what is @Reference
ReplyDeleteprivate CODSService codsService;?