AEM Cloud Service - Custom Authentication Handler for Excluding Paths under (SAML) Protected Paths for Anonymous Access


When a Root path is enabled on publish for authentication, accessing any child pages prompts the user to login. Custom authentication handler in this post allows anonymous access (so no login) for pages under the protected (authentication required) root paths

Demo | Package Install | Github



1) For setting up a protected root path create component eaem-publish-ims-login\ui.apps\src\main\content\jcr_root\apps\eaem-publish-ims-login-cust-auth\components\imslogin with the following code in .content.xml

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
    jcr:primaryType="nt:unstructured"
    jcr:title="Properties"
    sling:resourceType="cq/gui/components/authoring/dialog">
    <content
        jcr:primaryType="nt:unstructured"
        sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns">
        <items jcr:primaryType="nt:unstructured">
            <column
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/container">
                <items jcr:primaryType="nt:unstructured">
                    <text
                        jcr:primaryType="nt:unstructured"
                        sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
                        fieldLabel="Enter Sign-in button text"
                        name="./buttonText"/>
                </items>
            </column>
        </items>
    </content>
</jcr:root>


2) 4) Add the following code (HTL scripting preferred) in eaem-publish-ims-login\ui.apps\src\main\content\jcr_root\apps\eaem-publish-ims-login-cust-auth\components\imslogin\imslogin.jsp

<%@page session="false"
        contentType="text/html"
        pageEncoding="utf-8"
        import="com.adobe.granite.auth.ims.ImsConfigProvider,
                com.adobe.granite.xss.XSSAPI"%>
<%@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);
    ImsConfigProvider imsConfigProvider = sling.getService(ImsConfigProvider.class);
    String imsLoginUrl = "test";

    if (imsConfigProvider != null) {
        imsLoginUrl = imsConfigProvider.getImsLoginUrl(slingRequest);
        imsLoginUrl = xssAPI.getValidHref(imsLoginUrl);
    }
%>

<div class="eaem-centered coral--dark">
    <button id="eaem-ims-submit-button" is="coral-button" variant="primary" type="submit"
            data-ims-url='<%=imsLoginUrl%>'
            class="_coral-Button--block _coral-Button _coral-Button--cta" size="M">
        <coral-button-label class="_coral-Button-label"><%= resource.getValueMap().get("buttonText", "Sign-in with Adobe")%></coral-button-label>
    </button>
</div>


3)  Create the custom login page /content/eaem-publish-ims-login-cust-auth/us/en/custom-login.html, drag and drop the IMS Login component, publish page...


4) Create site home page /content/eaem-publish-ims-login-cust-auth/us/en/home.html,  open Page Properties Advanced > Authentication Requirement > Enable, select the login page /content/eaem-publish-ims-login-cust-auth/us/en/custom-login and publish the page. Doing so sets the mixin granite:AuthenticationRequired on page making it (and the sub pages) protected...


5) To make few pages anonymous under the site home (which is protected) eg. /content/eaem-publish-ims-login-cust-auth/us/en/home/no-auth create a custom authentication handler with following code. This works based on read only service user session

package apps.experienceaem.sites.core.servlets;

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.auth.core.spi.DefaultAuthenticationFeedbackHandler;
import org.apache.sling.jcr.resource.api.JcrResourceConstants;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Session;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

@Component(
    service = AuthenticationHandler.class,
    immediate = true,
    property = {
        AuthenticationHandler.PATH_PROPERTY + "=/",
        AuthenticationHandler.TYPE_PROPERTY + "=Experience AEM - No Auth Path Handler",
        "service.ranking:Integer=5000"
    }
)
@Designate(ocd = NoAuthPathsAuthenticationHandler.Config.class)
public class NoAuthPathsAuthenticationHandler extends DefaultAuthenticationFeedbackHandler implements AuthenticationHandler {
   
    private static final Logger LOG = LoggerFactory.getLogger(NoAuthPathsAuthenticationHandler.class);

    private static final String AUTH_TYPE_EAEM_NO_AUTH_PATHS = "AUTH_TYPE_EAEM_NO_AUTH_PATHS";
   
    private Set<String> noAuthPaths = new HashSet<>();
    private boolean enabled = false;

    @Reference
    ResourceResolverFactory resourceResolverFactory;    
   
    @ObjectClassDefinition(
        name = "Experience AEM - No Auth Paths Authentication Handler",
        description = "Custom authentication handler to allow specific paths without authentication"
    )
    public @interface Config {
       
        @AttributeDefinition(
            name = "Enabled",
            description = "Enable/disable the authentication handler"
        )
        boolean enabled() default true;
       
        @AttributeDefinition(
            name = "No Auth Paths",
            description = "List of paths that do not require authentication"
        )
        String[] noAuthPaths() default {
            "/content/eaem-publish-ims-login-cust-auth/us/en/home/no-auth"
        };
    }
   
    @Activate
    @Modified
    protected void activate(Config config) {
        this.enabled = config.enabled();
        this.noAuthPaths = new HashSet<>(Arrays.asList(config.noAuthPaths()));
        LOG.info("NoAuthPathsAuthenticationHandler activated. Enabled: {}, No-auth paths: {}", enabled, noAuthPaths);
    }
   
    @Override
    public AuthenticationInfo extractCredentials(HttpServletRequest request, HttpServletResponse response) {
        if (!enabled) {
            return null;
        }
       
        String requestPath = request.getPathInfo();

        if (requestPath == null) {
            requestPath = request.getRequestURI();
        }

        if(requestPath.endsWith(".html")) {
            requestPath = requestPath.substring(0, requestPath.length() - 5);
        }
       
        LOG.info("Checking authentication for requested resource: {}", requestPath);
       
        if (isNoAuthPath(requestPath)) {
            LOG.info("Path {} matches cust auth handler no-auth pattern, allowing anonymous access", requestPath);

            ResourceResolver serviceResolver = null;

            try {
                serviceResolver = getServiceResolver();
            } catch (Exception e) {
                LOG.error("Error getting service resolver", e);
                return null;
            }

            if(serviceResolver == null) {
                LOG.error("Service resolver is null");
                return null;
            }

            AuthenticationInfo authInfo = new AuthenticationInfo(AUTH_TYPE_EAEM_NO_AUTH_PATHS);
            authInfo.put(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, serviceResolver.adaptTo(Session.class));

            return authInfo;
        }
       
        LOG.info("Path {} requires authentication", requestPath);
        return null;
    }
   
    @Override
    public boolean requestCredentials(HttpServletRequest request, HttpServletResponse response) throws IOException {
        return false;
    }
   
    @Override
    public void dropCredentials(HttpServletRequest request, HttpServletResponse response) throws IOException {
    }
   
    private boolean isNoAuthPath(String path) {
        if (path == null || noAuthPaths.isEmpty()) {
            return false;
        }
       
        for (String pattern : noAuthPaths) {
            if (path.startsWith(pattern)) {
                return true;
            }
        }
       
        return false;
    }

    private ResourceResolver getServiceResolver() throws Exception{
        Map<String, Object> SERVICE_MAP = new HashMap<>();
        SERVICE_MAP.put(ResourceResolverFactory.SUBSERVICE, "eaem-no-auth-paths-service");

        return resourceResolverFactory.getServiceResourceResolver(SERVICE_MAP);
    }
}

6) Create the readonly service user in ui.config\src\main\content\jcr_root\apps\eaem-publish-ims-login-cust-auth\osgiconfig\config\org.apache.sling.jcr.repoinit.RepositoryInitializer~eaem.cfg.json

{
    "scripts": [
        "create service user eaem-no-auth-paths-service-user with path system/experience-aem",
        "set ACL for eaem-no-auth-paths-service-user \n  allow jcr:read on /content \n  allow jcr:read on /conf \n end"
    ]
}

7) Provide the user to service mapping in ui.config\src\main\content\jcr_root\apps\eaem-publish-ims-login-cust-auth\osgiconfig\config\org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~eaem.cfg.json

{
    "user.mapping": [
      "eaem-publish-ims-login-cust-auth.core:eaem-no-auth-paths-service=[eaem-no-auth-paths-service-user]"
    ]
}

No comments:

Post a Comment