Authentication Handler in AEM: custom approach

Bobby MavrovAEM Implementations5 Comments

AEM offers developers the opportunity to implement their custom Authentication Handler with a full range of customization using the Sling Authentication APIs. To create a custom handler, we need to implement the AuthenticationHandler interface. 

Why Create Custom Authentication?

There are many possible cases where users’ authentication could be necessary besides the default Form authentication on the default login page. Of course, we could create our login page, but we’ll be looking into different approaches to tackle additional requirements in this example.

The following scenario presents an interesting example. In an organization where no Sigle Sing-On (SSO) has been implemented, the requirement is to have our website running on AEM be accessible only for users already logged in into another internal web application via a direct link generated by this second application.

The idea here is that these users will never access the actual default login page or any other login page. One way for them to log in would be to use the already authenticated application’s link.

Solving The Problem With Custom Authentication Handler

A possible solution that we’ll be delving into here is to have the second application external to AEM and generate a user token, passed as a URL parameter in the link to our AEM website. We want users to be authenticated to access our website as well. After receiving and verifying the request, our custom authenticator would then forward the token to a web service endpoint where it will be confirmed, and then user details will be returned upon success. Subsequently, our custom authenticator will then sign the user if it has already been created in AEM. If not, it will create it on the fly and then sign it in.

Solution Diagram

Complete code of the custom authentication handler

package com.myproject.auth;

import com.day.crx.security.token.TokenUtil;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.apache.jackrabbit.api.JackrabbitSession;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.Group;
import org.apache.jackrabbit.api.security.user.User;
import org.apache.jackrabbit.api.security.user.UserManager;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.auth.core.spi.AuthenticationHandler;
import org.apache.sling.auth.core.spi.AuthenticationInfo;
import org.apache.sling.jcr.api.SlingRepository;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.SimpleCredentials;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Component(service = CustomAuthenticator.class, immediate = true, property = { "path=/content/mywebsite",
        Constants.SERVICE_RANKING +":Integer=60000", Constants.SERVICE_DESCRIPTION +"=Custom Authenticator" })
public class CustomAuthenticator implements AuthenticationHandler {

    private static final String REQUEST_METHOD = "GET";
    static final String TOKEN_PARAMETER = "token";;
    private final Logger log = LoggerFactory.getLogger(CustomAuthenticator.class);

    @Reference
    private ResourceResolverFactory resourceResolverFactory;
    @Reference
    private SlingRepository repository;

    public AuthenticationInfo extractCredentials(HttpServletRequest request, HttpServletResponse response) {
        if (REQUEST_METHOD.equals(request.getMethod()) && (request.getParameter(TOKEN_PARAMETER) != null)) {
            Map<String, Object> param = new HashMap<>();
            param.put(ResourceResolverFactory.USER, "my-system-user");
            ResourceResolver resolver = null;
            try {
                String userId = obtainUserId(request.getParameter(TOKEN_PARAMETER));
                if(userId != null) {
                    resolver = resourceResolverFactory.getServiceResourceResolver(param);
                    UserManager userManager = ((JackrabbitSession) resolver.adaptTo(Session.class)).getUserManager();
                    Authorizable user = userManager.getAuthorizable(userId);
                    if(user == null) {
                        createNewUser(userManager, userId);
                        resolver.commit();
                    }

                    Session session = this.repository.login(new SimpleCredentials(userId, userId.toCharArray()));

                    if (session != null) {
                        return createAuthenticationInfo(request, response, session.getUserID());
                    }
                }
            } catch (Exception e) {
                log.error("Exception in extractCredentials while processing the request {}", e);
            }finally {
                if(resolver != null && resolver.isLive())
                    resolver.close();
            }
        }
        return null;
    }

    private AuthenticationInfo createAuthenticationInfo(HttpServletRequest request, HttpServletResponse response,
                                                        String userId) throws RepositoryException {
        return TokenUtil.createCredentials(request, response, this.repository, userId, true);
    }

    public void dropCredentials(HttpServletRequest arg0, HttpServletResponse arg1) {}

    public boolean requestCredentials(HttpServletRequest request, HttpServletResponse arg1) {return true;}

    public String obtainUserId(String token) {
        HttpPost httpPost = new HttpPost("http://localhost:8080/api/user/getUserId");
        CloseableHttpClient httpClient = HttpClients.createDefault();
        try (CloseableHttpResponse response = httpClient.execute(httpPost)){
            List<NameValuePair> urlParameters = new ArrayList<>();
            urlParameters.add(new BasicNameValuePair(TOKEN_PARAMETER, token));
            httpPost.setEntity(new UrlEncodedFormEntity(urlParameters));
            HttpEntity entity = response.getEntity();
            if (response.getStatusLine().getStatusCode() != 200) {
                log.error("Unable to obtain user id from web service! Status: " + response.getStatusLine().getStatusCode());
                return null;
            }

            String userId = StringUtils.EMPTY;
            String output;
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
            while ((output = bufferedReader.readLine()) != null) {
                userId = userId + output;
            }
            EntityUtils.consume(entity);
            return userId;
        } catch (Exception e) {
            return null;
        }
    }

    private boolean createNewUser(UserManager userManager, String userId) {

        try {
            Group group = (Group) userManager.getAuthorizable("dam-users");
            Authorizable user = userManager.getAuthorizable(userId);
            if (user == null) {
                user = userManager.createUser(userId,userId);
                group.addMember(user);
            } else if (!group.isMember(user)) {
                group.addMember(user);
                if(((User) user).isDisabled())
                    ((User) user).disable(null);
            }
        }catch (Exception e){
            log.error("Error while creating new user! ", e);
            return false;
        }
        return true;
    }

    protected void bindRepository(SlingRepository paramSlingRepository) {
        this.repository = paramSlingRepository;
    }

    protected void unbindRepository(SlingRepository paramSlingRepository) {
        if (this.repository == paramSlingRepository) {
            this.repository = null;
        }
    }
}

Check if the Handler is Active After Deployment

We can verify our successful deployment here by going to http://localhost:4502/system/console/slingauth

When the New Handler Will Come Into Place

Let’s first review under what conditions the authenticator will be activated. When we declare the OSGi component we set the following properties:

@Component(service = CustomAuthenticator.class, immediate = true, property = { "path=/content/mywebsite",
        Constants.SERVICE_RANKING +":Integer=60000", Constants.SERVICE_DESCRIPTION +"=Custom Authenticator" })

path=/content/mywebsite – the authenticator will be used for requests to the website only

service.ranking:Integer=60000 – we need higher value to make sure the service is involked to handle the authentication requiest

After the component is hit another check is performed:

if (REQUEST_METHOD.equals(request.getMethod()) && (request.getParameter(TOKEN_PARAMETER) != null)) 

the handler will proceed with its execution only if the request is of type GET and if it contains parameter token. This will prevent users who have been logged out accessing the website without the necessary token if they have the link stored somewhere for example.

In case any of those conditions are not met the authenticator would be ignored but users could still login directly via the login page. If we want to avoid that we can disable the anonymous access to it via Apache Sling Authentication Service. We can also filter the access to it via our Publish Dispatcher. We could also modify the default login page for the default handler via Adobe Granite Login Selector Authentication Handler.

Once The Handler Has Been Triggered…

After obtaining the token parameter, our authenticator calls obtainUserId() where another request is created to the REST Web Service with the token again added as URL parameter. The function of this service is not part of the scope of this blog. The service does its job, and if the token is verified it returns whatever user details are be necessary. In our example only user name. If the token is not verified, then null is returned. 

Time to sign in the user

After we have the user name we need to check if the user already exists in our AEM user repository using UserManager API.

Authorizable user = userManager.getAuthorizable(userId);
                    if(user == null) {
                        createNewUser(userManager, userId);
                        resolver.commit();
                    }

User here could have been manually created earlier or via some service obtaining them from LDAP or database.

If the user does not exist, though, createNewUser() is called where they are created with the username as parameter. For simplicity we’ll use the username as password as well utilizing UserManager API again.

user = userManager.createUser(userId,userId);

This example illustrates how useful custom authentication handlers in AEM can be and how easy it is to implement one for the needs of your organization.

KBWEB Consult specializes in customizing and integrating the Adobe Experience Manager (AEM) Platform. Contact us for a free consultation.

Loading

5 Comments on “Authentication Handler in AEM: custom approach”

  1. We are storing AEM users same way as you have mentioned above. In our application 1000 of users will login everyday. So we are creating 1000 of users.
    We are using userManager.getAuthorizable to check if the user is exits in AEM and this is taking more time when we have 1000’s of users.
    We are facing performance issue. What do you think can help here. I have created index for users but it did not help.

    1. Hi mahaboobalishah,

      If I understand correctly you are actually creating 1000’s of unique users every day. Is that indeed the requirement for your application? One thing that could improve the performance in your case might be to store the user’s data somewhere else as well inside the AEM repository like in a ValueMap (like .properties file) or JSON. Then check against that if your user exist or it is active. In your custom authenticator you could also add functionality to update that file after user is successfully created but only if you don’t notice performance issues. If you do then the update could be done by additional service. You might need 2 of them. One to monitor for user updates from other and recreate them in your file, one to expose isUserActive(String userId) method to check against that file, as you can use that method in your authenticator. You also have to be cautious not to expose the user list to the public.

Leave a Reply