Sunday, March 3, 2013

How to Create Custom Authentication Handler in CQ

Use Case:
  • You want to use custom Authentication handler instead of OOTB one for authentication. 
  • Custom User Registration
Pre requisite:
Available Authentication Handler in CQ:



How to create your Own:


1) Create custom class extending Sling Authentication Handler and override available methods


import org.apache.sling.auth.core.spi.AuthenticationFeedbackHandler;
import org.apache.sling.auth.core.spi.AuthenticationHandler;
import org.apache.sling.auth.core.spi.DefaultAuthenticationFeedbackHandler;
@Component(metatype = true, immediate = true, label = "My Custom Authentication Handelr",
description="Authenticates User Against Citrix One Web Service")
@Service
@Properties({
@Property(name = AuthenticationHandler.PATH_PROPERTY, value = "/"),
@Property(name = Constants.SERVICE_DESCRIPTION, value = "My Custom Authentication Handler") })
public class MyCustomAuthenticationHandler extends DefaultAuthenticationFeedbackHandler implements AuthenticationHandler,
AuthenticationFeedbackHandler {
private static final String REQUEST_METHOD = "POST";
private static final String USER_NAME = "j_username";
private static final String PASSWORD = "j_password";
static final String AUTH_TYPE = "YOGESH";
static final String REQUEST_URL_SUFFIX = "/j_mycustom_security_check";
/**
If you see most of the method under sling authentication handler, They have request and response object available. You can use that object to get information about user (Either by reading cookie or some other way).
*/
//Important methods
//Return true if succesful
public boolean authenticationSucceeded(HttpServletRequest request, HttpServletResponse response,
AuthenticationInfo authInfo) {
}
//Extract data from request Object
public AuthenticationInfo extractCredentials(HttpServletRequest request, HttpServletResponse response) {
//You can have logic like. This will read user name and password from form post and set credentials
if (REQUEST_METHOD.equals(request.getMethod()) && request.getRequestURI().endsWith(REQUEST_URL_SUFFIX)
&& request.getParameter(USER_NAME) != null) {
if (!AuthUtil.isValidateRequest(request)) {
AuthUtil.setLoginResourceAttribute(request, request.getContextPath());
}
SimpleCredentials creds = new SimpleCredentials(request.getParameter(USER_NAME), request.getParameter(PASSWORD).toCharArray());
//ATTR_HOST_NAME_FROM_REQUEST can be any thing this is just an example
creds.setAttribute(ATTR_HOST_NAME_FROM_REQUEST, request.getServerName());
return createAuthenticationInfo(creds);
}
return null;
}
//Custom Create AuthInfo. Not required but you can create
private AuthenticationInfo createAuthenticationInfo(Credentials creds) {
//Note that there is different signature of this method. Use one that you need.
AuthenticationInfo info = new AuthenticationInfo(AUTH_TYPE);
//this you can use it later in auth process
info.put("Your Custom Attribute", creds);
return info;
}
//Do something when authentication failed.
public void authenticationFailed(HttpServletRequest request, HttpServletResponse response,
AuthenticationInfo authInfo) {
}
2) Create a form (Your custom Login form) 

which will be something like this

String action = currentPage.getPath() +"/j_mycustom_security_check";

<form method="POST" action="<%= xssAPI.getValidHref(action) %>"> 
Enter User Name: <input  name="j_username" type="text" />
Enter Passord: <input  name="j_password" type="text" />
<input type="button" name="Click Here to login">
</form>

You can also use Ajax post or something to see if response is 200 (Which mean successful login)

3) Then under apache sling post servlet, Make sure that you allow parameter you are posting. In this case j_*


4) Add your custom authentication prefix to sling authenticator service


5) Once you have your bundle deployed, You should see your additional authentication handler. 




Integrate it with Custom Pluggable Login Module (AEM 6)

Step1 : create pluggable 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.commons.lang3.StringUtils;
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;
/**
* The <code>OpenIDLoginModulePlugin</code> is a simple Sling LoginModulePlugin
* enabling authentication of OpenID identifiers as Jackrabbit Repository users
*/
class CustomPluggableLoginModule implements LoginModulePlugin {
private final CustomAuthenticationHandler authHandler;
/** default log */
private final Logger log = LoggerFactory.getLogger(getClass());
/**
* Creates an instance of this class and registers it as a
* <code>LoginModulePlugin</code> service to handle login requests with
* <code>SimpleCredentials</code> provided by the
* {@link OpenIDAuthenticationHandler}.
*
* @param authHandler
* The {@link OpenIDAuthenticationHandler} providing support to
* validate the credentials
* @param bundleContext
* The <code>BundleContext</code> to register the service
* @return The <code>ServiceRegistration</code> of the registered service for
* the {@link OpenIDAuthenticationHandler} to unregister the service
* on shutdown.
*/
static ServiceRegistration register(final CustomAuthenticationHandler authHandler, final BundleContext bundleContext) {
CustomPluggableLoginModule plugin = new CustomPluggableLoginModule(authHandler);
Hashtable<String, Object> properties = new Hashtable<String, Object>();
properties.put(Constants.SERVICE_DESCRIPTION, "LoginModulePlugin Support for OpenIDAuthenticationHandler");
properties.put(Constants.SERVICE_VENDOR, bundleContext.getBundle().getHeaders().get(Constants.BUNDLE_VENDOR));
return bundleContext.registerService(LoginModulePlugin.class.getName(), plugin, properties);
}
private CustomPluggableLoginModule(final CustomAuthenticationHandler authHandler) {
this.authHandler = authHandler;
}
/**
* This implementation does nothing.
*/
@SuppressWarnings("unchecked")
public void doInit(final CallbackHandler callbackHandler, final Session session, final Map options) {
return;
}
/**
* Returns <code>true</code> indicating support if the credentials is a
* <code>SimplerCredentials</code> object and has an authentication data
* attribute.
* <p>
* This method does not validate the data just checks its presence.
*
* @see CookieAuthenticationHandler#hasAuthData(Credentials)
*/
public boolean canHandle(Credentials credentials) {
//this is custom method that you will write in auth handler
return StringUtils.isNotBlank(authHandler.getUserId(credentials));
}
/**
* Returns an authentication plugin which validates the authentication data
* contained as an attribute in the credentials object. The
* <code>authenticate</code> method returns <code>true</code> only if
* authentication data is contained in the credentials (expected because this
* method should only be called if {@link #canHandle(Credentials)} returns
* <code>true</code>) and the authentication data is valid.
*/
public AuthenticationPlugin getAuthentication(final Principal principal, final Credentials creds) {
return new AuthenticationPlugin() {
public boolean authenticate(Credentials credentials) throws RepositoryException {
//You will be implementing this in your auth handler
return StringUtils.isNotBlank(authHandler.getUserId(credentials));
}
};
}
/**
* Returns <code>null</code> to have the <code>DefaultLoginModule</code>
* provide a principal based on an existing user defined in the repository.
*/
public Principal getPrincipal(final Credentials credentials) {
return null;
}
/**
* This implementation does nothing.
*/
@SuppressWarnings("unchecked")
public void addPrincipals(final Set principals) {
}
/**
* Returns <code>LoginModulePlugin.IMPERSONATION_DEFAULT</code> to indicate
* that this plugin does not itself handle impersonation requests.
*/
public int impersonate(final Principal principal, final Credentials credentials) {
return LoginModulePlugin.IMPERSONATION_DEFAULT;
}
}

Step2 : Plug it in your custom auth handler

//Import
import org.osgi.framework.ServiceRegistration;
import org.apache.sling.auth.core.spi.AuthenticationHandler;
import org.apache.sling.auth.core.spi.DefaultAuthenticationFeedbackHandler;
//Your Custom Auth handler class
public class CustomAuthHandler extends DefaultAuthenticationFeedbackHandler implements AuthenticationHandler {
//All Your Logic
private ServiceRegistration loginModule;
//All Your Logic
@Activate
protected void activate(ComponentContext componentContext) {
context = componentContext;
this.loginModule = null;
try {
this.loginModule = CustomPluggableLoginModule.register(this, componentContext.getBundleContext());
} catch (Throwable t) {
//Error handling
}
}
@Deactivate
protected void deactivate(@SuppressWarnings("unused") ComponentContext componentContext) {
if (loginModule != null) {
loginModule.unregister();
loginModule = null;
}
}
}


Example: https://svn.apache.org/repos/asf/sling/trunk/bundles/auth/form/src/main/java/org/apache/sling/auth/form/impl/

Example of Open Source Extended Authentication Handler:



CQ OOTB Extended authentication Handler

Day CRX Sling - Token Authenticationcom.day.crx.sling.crx-auth-token)Adobe Granite SSO Authentication Handlercom.adobe.granite.auth.sso)Day Communique 5 PIN Authentication Handlercom.day.cq.cq-pinauthhandler

There are a lot of things needed for creating your custom user registration process (You might / Might not) Need following,

1) Custom Login Module http://www.wemblog.com/2012/06/how-to-add-custom-login-module-in-cq55.html to sync users / group in CQ from third party system
2) Custom Authentication handler as above
3) Reverse replication to sync user across (If user registration is in publish)

Note: Above code is just Pseudo code. Please test, You might have to add your custom logic for this to work.

Let me know if you have any question or comment.

38 comments:

  1. Hi Yogesh,

    I really like this article and this blog, so thank you for putting this information together and taking the time to maintain and update this blog. I just wanted to call out one point I noticed:

    "And more ..... If you can decompile these bundle, You should be able to see example of how it is implemented."

    If a client decompiles an OOTB proprietary jar, then technically they're in violation of their license agreement and could lose support and/or license. As such, I would really not suggest decompiling the jars, even if they are good examples.

    That being said, again I really do like this blog and appreciate the effort.

    ReplyDelete
    Replies
    1. Doug,

      Thank you very much for your comment. When I said decompiled, I meant Open source (Sling) authentication handler (Including form based). But I can see confusion here. Thus deleted that portion.

      Again thanks for review.

      Yogesh

      Delete
  2. Hi Yogesh,

    This post is what I was looking for. I am bit confused about some of the parameters and methods you have defined in the code though. Constant ATTR_HOST_NAME_FROM_REQUEST is not defined. Is it same to assume it as "hostname"?

    Regards,
    Anup

    ReplyDelete
    Replies
    1. Anup,

      You do not need that variable. This is just an example of how you can set custom attribute in credential object

      Yogesh

      Delete
  3. Can we have a sample package to get into the details?

    ReplyDelete
    Replies
    1. Hello,

      Sample Package is difficult in this case because each custom authentication requirement is different. Let me know if you have any specific question.

      Yogesh

      Delete
  4. Hello Yogesh,

    Great article. If I were to create a Custom authentication module, which uses a simple CSV file for User name and Password, can I still use the OOTB login page ? I dont want users to Register and I prefer the CSV approach as it is just the list of users who I want to control access for a very short period of time.

    ReplyDelete
    Replies
    1. Silican,

      You do not need custom authentication handler to create user and group. You can use Java API to parse CSV and then Jackrabbit API to create user. Here is example of creating user and group in CQ http://wemcode.wemblog.com/user-group-management. That site also have example of how you can create Role.

      Yogesh

      Delete
  5. Hi Yogesh,

    Thank you for the above tips. I have implemented something similar in order to do some additional checks on the user who is authenticating (e.g. check their account has not expired).
    Do you know how I might then call the default AuthenticationHandler from my extractCredentials? In other words, revert to the normal "j_security_check" ? I am trying to call the default handler's extractCredentials() and return the AuthenticationInfo object if my own handler is satisfied with it's additional checks. Any ideas?

    ReplyDelete
    Replies
    1. John,
      You can just return null from custom authentication handler and then next authentication handler will pick up.

      Yogesh

      Delete
    2. Thanks Yogesh, I guess what I am asking is after I have authenticated in my custom AuthenticationHandler and return AuthenticationInfo, how do I then get CQ to create the logged in session cookie? When I return the AuthenticationInfo , it fires the authenticationSucceeded, but there is no session cookie created.

      Delete
    3. I ended up using com.day.crx.security.token.TokenUtil.createCredentials() in my authenticationSucceeded() to create the token, just in case anybody is trying to do something similar.

      Delete
    4. Thank you John Sharing this. This will work if Order of Token Auth Handler comes after your custom Auth handler.

      Yogesh

      Delete
  6. Thanks Yogesh,
    if I return null from my custom handler, wouldn't this mean that my custom conditions are not included? I want to include my conditions PLUS the default conditions handled by j_security_ check..

    ReplyDelete
  7. I have question related to same custom login approach that you have suggested. Say I want to authenticate user against DataBase, Say I do that in authenticationSucceeded and it returns true for the first time. So the Question is how do you maintain a session then? like how would you ensure access to your secure area is being authenticated against logged in user each time.

    Apologies but I am little unaware of the login token purpose. Also I dont want to maintain users in CQ Repository what to do then, how their access to repository pages will be handled?

    ReplyDelete
  8. I have question related to Auto login in CQ5.6. I have created registration form for registring new user in my site. I am using a custom post servlet for creating users.

    I want my users to get logged in once they have completed registration process.

    How can we make user logged in automatically once registration process is completed?

    ReplyDelete
    Replies
    1. Ankur,

      Try something like
      TokenUtil.createCredentials(request, response, slingRepository, userID, true) in your servlet service method.

      Maven dependency for TokenUtil:

      com.day.crx.sling
      crx-auth-token
      2.4.23
      provided


      Also this could be useful for your use case:
      http://www.cqcon.eu/content/dam/cqcon/Pr%C3%A4sentation_Antonio_Sanso.pdf

      Please confrim that this works for you.

      Delete
  9. I tried to follow the above steps to implement the Custom Authentication handler but was stuck at some point.

    1. When I implemented using the component properies "@Property(name = AuthenticationHandler.PATH_PROPERTY, value = "/")," I was not able to reach to the Custom Authentication Handler. But when I used "@Property(name = AuthenticationHandler.PATH_PROPERTY, value = "/content")" . What is the correct way to do so?

    2. Even if I reach the authenticationSucceeded() function I am obtaining a 403 Forbidden from the "j_mycustom_security_check" handler. Has anyone faced similar problem?

    3. What does the value "static final String AUTH_TYPE" symbolizes.

    Thanks for helping in advance.

    ReplyDelete
    Replies
    1. Just to add I am using CQ5.6.1 and version of jar for org.apache.sling.auth.core is 1.1.6

      Delete
    2. @HK, did you get any resolution to 403 error? I am facing the same issue

      Delete
  10. Hi Yogesh,
    I am trying to authenticate from open am , In request cookie value is available once user get authenticate from open am , We provide the filter in dispatcher like /content/mysite*. How we can aunthenticate in cq5. When we access from openam the dispatcher url it show us a prompt "Your request could not be completed becauseyou have signed out".Please let me know how to go around

    ReplyDelete
    Replies
    1. Hello Raj,

      Usually you get that error when there is some exception in login process. Can you check your error log and make sure that nothing is wrong ?

      Yogesh

      Delete
  11. Hi Yogesh,

    I am working out with LDAP Authentication, I created a login component which sends a request to your_path/j_security_check. with username and password and the user is authenticated against the LdapLoginModule i believe which is good.

    Now, if the user is authenticated i want to check whether they are authorized to use the system or not. There is a property on user profile node with which we check that.

    The problem is once i make a request to path/j_security_check it automatically logs in the user to the system. I wan to avoid that before i know if the user is authorized to use the system or not.

    Can you please let me know how can i just override the authorization mechanism.

    ReplyDelete
    Replies
    1. Just in case somebody is interested, i have got this working using form Authentication handler http://svn.apache.org/repos/asf/sling/trunk/bundles/auth/form/src/main/java/org/apache/sling/auth/form/impl/FormAuthenticationHandler.java

      I almost ended up using most parts of this handler as i wanted to avoid writing any security code because it might just have flaws.

      Since i kept my handler service ranking to be 0 and Token Authentication handler rank was higher, i ended up creating com.day.crx.security.token.TokenUtil.createCredentials() in the authenticationSucceeded method. If you happen to have custom Authentications before you login the user you can do it in authenticationSucceeded method as well.

      Note in case your form has got a resource for redirection in form of an input hidden field i saw a weird behavior where even with valid creds i was seeing INVALID_CREDNETIALS error but as soon i put a call to TokenAuth the login redirection started working.

      Delete
    2. Sam,

      Thank a lot of sharing your problem and solution in Blog. Really appreciated.

      Yogesh

      Delete
  12. Hi Yogesh,

    I am working with LDAP Authentication, Siteminder authenticates the user with credentials from LDAP and sends user data to response.

    So I want to store that user data in session bean so that I can add more details specific to that user in that bean.
    For it I created a user bean in my top header component jsp which is in every page.
    And I am trying to make the page "session=true" it sends "500: Internal Server error".

    Can you please help

    ReplyDelete
    Replies
    1. You can simply use request.getRemoteuser() right ? Not sure why you need to store this in session. Note that session is not scalable in case you are planning to use caching in future.

      Delete
  13. Hi Yogesh,

    I built my authentication handler by following your instructions and the sample code, https://github.com/davidjgonzalez/com.activecq.samples/blob/master/core/src/main/java/com/activecq/samples/slingauthenticationhandler/impl/TokenSlingAuthenticationHandler.java.

    in the method "authenticationSucceeded", I call "return false" to continue the framework. however, I see an error is reported in the error.log: org.apache.sling.auth.core.impl.SlingAuthenticator handleSecurity: AuthenticationHandler did not block request; access denied
    did I miss any configurations?

    could you please help?

    ReplyDelete
  14. Hi, I have imported org.apache.sling.auth.core.AuthUtil in the java class. But this is showing an error.
    Kindly help.

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
  15. In addition to the above comment

    http://sling.apache.org/apidocs/sling5/org/apache/sling/jcr/resource/JcrResourceConstants.html package dos't contains "JcrResourceConstants.AUTHENTICATION_INFO_CREDENTIALS"

    ReplyDelete
    Replies
    1. Hello Renju,

      Thanks for your Feedback. I updated document, also added how you can create your pluggable login module.

      Yogesh

      Delete
  16. is it possible to call the j_security_check from my own servlet? instead of from action

    ReplyDelete
    Replies
    1. Hello Fabian,

      What is the use case for this ? You might be able to achieve your use case using some other method.

      Yogesh

      Delete
  17. Hi Yogesh,

    I tried to follow the above steps to implement the Custom Authentication handler in CQ5.5 but was stuck at some point. I did configuration mentioned in above but Custom Authentication handler is not getting invoke. I got below warning:

    “org.apache.sling.auth.core.impl.SlingAuthenticator handleSecurity: AuthenticationHandler did not block request; access denied”.

    Can u help, will it require any other configuration and how to resolve it.

    ReplyDelete
  18. Hi Yogesh,

    Great article. I have one question here - Do we not need to create a login-tokrn cookie in the authenticationSucceeded method before sending the response as true? Otherwise how would the further requests be authenticated?

    ReplyDelete
    Replies
    1. Hello Karttik,

      Not in this case. You can have such mechanism to avoid call to server (Chose any cookie you want). In this case all request will go through authentication handler.

      Yogesh

      Delete